Skip to content

Commit

Permalink
chore: box step w/o modifying runtime errors
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Dec 22, 2023
1 parent 5d9e08a commit 09087a5
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 102 deletions.
31 changes: 11 additions & 20 deletions packages/playwright/src/matchers/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/

import {
captureRawStack,
isString,
pollAgainstDeadline } from 'playwright-core/lib/utils';
import type { ExpectZone } from 'playwright-core/lib/utils';
Expand Down Expand Up @@ -48,7 +47,7 @@ import {
import { toMatchSnapshot, toHaveScreenshot, toHaveScreenshotStepTitle } from './toMatchSnapshot';
import type { Expect } from '../../types/test';
import { currentTestInfo, currentExpectTimeout, setCurrentExpectConfigureTimeout } from '../common/globals';
import { filteredStackTrace, trimLongString } from '../util';
import { trimLongString } from '../util';
import {
expect as expectLibrary,
INVERTED_COLOR,
Expand Down Expand Up @@ -248,45 +247,37 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
if (!testInfo)
return matcher.call(target, ...args);

const rawStack = captureRawStack();
const stackFrames = filteredStackTrace(rawStack);
const customMessage = this._info.message || '';
const argsSuffix = computeArgsSuffix(matcherName, args);

const defaultTitle = `expect${this._info.isPoll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`;
const title = customMessage || defaultTitle;
const wallTime = Date.now();
const step = matcherName !== 'toPass' ? testInfo._addStep({
location: stackFrames[0],
const step = testInfo._addStep({
category: 'expect',
title: trimLongString(title, 1024),
params: args[0] ? { expected: args[0] } : undefined,
wallTime,
infectParentStepsWithError: this._info.isSoft,
laxParent: true,
}) : undefined;
isSoft: this._info.isSoft,
});

const reportStepError = (jestError: ExpectError) => {
const error = new ExpectError(jestError, customMessage, stackFrames);
const serializedError = {
message: error.message,
stack: error.stack,
};
step?.complete({ error: serializedError });
if (this._info.isSoft)
testInfo._failWithError(serializedError, false /* isHardError */);
else
const error = new ExpectError(jestError, customMessage);
step.complete({ error });
if (!this._info.isSoft)
throw error;
};

const finalizer = () => {
step?.complete({});
step.complete({});
};

const expectZone: ExpectZone = { title, wallTime };

try {
const result = zones.run<ExpectZone, any>('expectZone', expectZone, () => matcher.call(target, ...args));
const expectZone: ExpectZone | null = matcherName !== 'toPass' ? { title, wallTime } : null;
const callback = () => matcher.call(target, ...args);
const result = expectZone ? zones.run<ExpectZone, any>('expectZone', expectZone, callback) : zones.preserve(callback);
if (result instanceof Promise)
return result.then(finalizer).catch(reportStepError);
finalizer();
Expand Down
12 changes: 4 additions & 8 deletions packages/playwright/src/matchers/matcherHint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
import { colors } from 'playwright-core/lib/utilsBundle';
import type { ExpectMatcherContext } from './expect';
import type { Locator } from 'playwright-core';
import type { StackFrame } from '@protocol/channels';
import { stringifyStackFrames } from 'playwright-core/lib/utils';
import { rewriteErrorMessage } from 'playwright-core/lib/utils';

export function matcherHint(state: ExpectMatcherContext, locator: Locator | undefined, matcherName: string, expression: any, actual: any, matcherOptions: any, timeout?: number) {
let header = state.utils.matcherHint(matcherName, expression, actual, matcherOptions).replace(/ \/\/ deep equality/, '') + '\n\n';
Expand Down Expand Up @@ -49,15 +48,12 @@ export class ExpectError extends Error {
log?: string[];
timeout?: number;
};
constructor(jestError: ExpectError, customMessage: string, stackFrames: StackFrame[]) {
super('');
constructor(jestError: ExpectError, customMessage: string) {
super(jestError.message);
// Copy to erase the JestMatcherError constructor name from the console.log(error).
this.name = jestError.name;
this.message = jestError.message;
this.matcherResult = jestError.matcherResult;

if (customMessage)
this.message = customMessage + '\n\n' + this.message;
this.stack = this.name + ': ' + this.message + '\n' + stringifyStackFrames(stackFrames).join('\n');
rewriteErrorMessage(this, customMessage + '\n\n' + jestError.message);
}
}
64 changes: 24 additions & 40 deletions packages/playwright/src/matchers/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
import type { Locator, Page, APIResponse } from 'playwright-core';
import type { FrameExpectOptions } from 'playwright-core/lib/client/types';
import { colors } from 'playwright-core/lib/utilsBundle';
import { expectTypes, callLogText, filteredStackTrace } from '../util';
import { expectTypes, callLogText } from '../util';
import { toBeTruthy } from './toBeTruthy';
import { toEqual } from './toEqual';
import { toExpectedTextValues, toMatchText } from './toMatchText';
import { captureRawStack, constructURLBasedOnBaseURL, isRegExp, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils';
import { constructURLBasedOnBaseURL, isRegExp, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils';
import { currentTestInfo } from '../common/globals';
import { TestInfoImpl, type TestStepInternal } from '../worker/testInfo';
import { TestInfoImpl } from '../worker/testInfo';
import type { ExpectMatcherContext } from './expect';

interface LocatorEx extends Locator {
Expand Down Expand Up @@ -369,42 +369,26 @@ export async function toPass(
const testInfo = currentTestInfo();
const timeout = options.timeout !== undefined ? options.timeout : 0;

const rawStack = captureRawStack();
const stackFrames = filteredStackTrace(rawStack);

const runWithOrWithoutStep = async (callback: (step: TestStepInternal | undefined) => Promise<{ pass: boolean; message: () => string; }>) => {
if (!testInfo)
return await callback(undefined);
return await testInfo._runAsStep({
title: 'expect.toPass',
category: 'expect',
location: stackFrames[0],
}, callback);
};

return await runWithOrWithoutStep(async (step: TestStepInternal | undefined) => {
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
const result = await pollAgainstDeadline<Error|undefined>(async () => {
if (testInfo && currentTestInfo() !== testInfo)
return { continuePolling: false, result: undefined };
try {
await callback();
return { continuePolling: !!this.isNot, result: undefined };
} catch (e) {
return { continuePolling: !this.isNot, result: e };
}
}, deadline, options.intervals || [100, 250, 500, 1000]);

if (result.timedOut) {
const message = result.result ? [
result.result.message,
'',
`Call Log:`,
`- ${timeoutMessage}`,
].join('\n') : timeoutMessage;
step?.complete({ error: { message } });
return { message: () => message, pass: !!this.isNot };
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
const result = await pollAgainstDeadline<Error|undefined>(async () => {
if (testInfo && currentTestInfo() !== testInfo)
return { continuePolling: false, result: undefined };
try {
await callback();
return { continuePolling: !!this.isNot, result: undefined };
} catch (e) {
return { continuePolling: !this.isNot, result: e };
}
return { pass: !this.isNot, message: () => '' };
});
}, deadline, options.intervals || [100, 250, 500, 1000]);

if (result.timedOut) {
const message = result.result ? [
result.result.message,
'',
`Call Log:`,
`- ${timeoutMessage}`,
].join('\n') : timeoutMessage;
return { message: () => message, pass: !!this.isNot };
}
return { pass: !this.isNot, message: () => '' };
}
4 changes: 2 additions & 2 deletions packages/playwright/src/matchers/toMatchSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/uti
import { getComparator, sanitizeForFilePath, zones } from 'playwright-core/lib/utils';
import type { PageScreenshotOptions } from 'playwright-core/types/types';
import {
addSuffixToFilePath, serializeError,
addSuffixToFilePath,
trimLongString, callLogText,
expectTypes } from '../util';
import { colors } from 'playwright-core/lib/utilsBundle';
Expand Down Expand Up @@ -206,7 +206,7 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
return this.createMatcherResult(message, true);
}
if (this.updateSnapshots === 'missing') {
this.testInfo._failWithError(serializeError(new Error(message)), false /* isHardError */);
this.testInfo._failWithError(new Error(message), false /* isHardError */);
return this.createMatcherResult('', true);
}
return this.createMatcherResult(message, false);
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/worker/fixtureRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { formatLocation, debugTest, filterStackFile, serializeError } from '../util';
import { formatLocation, debugTest, filterStackFile } from '../util';
import { ManualPromise, zones } from 'playwright-core/lib/utils';
import type { TestInfoImpl, TestStepInternal } from './testInfo';
import type { FixtureDescription, TimeoutManager } from './timeoutManager';
Expand Down Expand Up @@ -143,7 +143,7 @@ class Fixture {
this._selfTeardownComplete?.then(() => {
afterStep?.complete({});
}).catch(e => {
afterStep?.complete({ error: serializeError(e) });
afterStep?.complete({ error: e });
});
}
testInfo._timeoutManager.setCurrentFixture(undefined);
Expand Down
50 changes: 26 additions & 24 deletions packages/playwright/src/worker/testInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import type { Attachment } from './testTracing';
import type { StackFrame } from '@protocol/channels';

export interface TestStepInternal {
complete(result: { error?: Error | TestInfoError, attachments?: Attachment[] }): void;
complete(result: { error?: Error, attachments?: Attachment[] }): void;
stepId: string;
title: string;
category: string;
Expand All @@ -45,6 +45,7 @@ export interface TestStepInternal {
error?: TestInfoError;
infectParentStepsWithError?: boolean;
box?: boolean;
isSoft?: boolean;
}

export class TestInfoImpl implements TestInfo {
Expand Down Expand Up @@ -231,17 +232,16 @@ export class TestInfoImpl implements TestInfo {
this.duration = this._timeoutManager.defaultSlotTimings().elapsed | 0;
}

async _runAndFailOnError(fn: () => Promise<void>, skips?: 'allowSkips'): Promise<TestInfoError | undefined> {
async _runAndFailOnError(fn: () => Promise<void>, skips?: 'allowSkips'): Promise<Error | undefined> {
try {
await fn();
} catch (error) {
if (skips === 'allowSkips' && error instanceof SkipError) {
if (this.status === 'passed')
this.status = 'skipped';
} else {
const serialized = serializeError(error);
this._failWithError(serialized, true /* isHardError */);
return serialized;
this._failWithError(error, true /* isHardError */);
return error;
}
}
}
Expand Down Expand Up @@ -283,24 +283,18 @@ export class TestInfoImpl implements TestInfo {
complete: result => {
if (step.endWallTime)
return;

step.endWallTime = Date.now();
let error: TestInfoError | undefined;
if (result.error instanceof Error) {
// Step function threw an error.
if (data.boxedStack) {
const errorTitle = `${result.error.name}: ${result.error.message}`;
result.error.stack = `${errorTitle}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
}
error = serializeError(result.error);
} else if (result.error) {
// Internal API step reported an error.
if (result.error) {
if (!(result.error as any)[stepSymbol])
(result.error as any)[stepSymbol] = step;
const error = serializeError(result.error);
if (data.boxedStack)
result.error.stack = `${result.error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
error = result.error;
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
step.error = error;
}
step.error = error;

if (!error) {
if (!step.error) {
// Soft errors inside try/catch will make the test fail.
// In order to locate the failing step, we are marking all the parent
// steps as failing unconditionally.
Expand All @@ -311,18 +305,20 @@ export class TestInfoImpl implements TestInfo {
break;
}
}
error = step.error;
}

const payload: StepEndPayload = {
testId: this._test.id,
stepId,
wallTime: step.endWallTime,
error,
error: step.error,
};
this._onStepEnd(payload);
const errorForTrace = error ? { name: '', message: error.message || '', stack: error.stack } : undefined;
const errorForTrace = step.error ? { name: '', message: step.error.message || '', stack: step.error.stack } : undefined;
this._tracing.appendAfterActionForStep(stepId, errorForTrace, result.attachments);

if (step.isSoft && result.error)
this._failWithError(result.error, false /* isHardError */);
}
};
const parentStepList = parentStep ? parentStep.steps : this._steps;
Expand Down Expand Up @@ -350,7 +346,7 @@ export class TestInfoImpl implements TestInfo {
this.status = 'interrupted';
}

_failWithError(error: TestInfoError, isHardError: boolean) {
_failWithError(error: Error, isHardError: boolean) {
// Do not overwrite any previous hard errors.
// Some (but not all) scenarios include:
// - expect() that fails after uncaught exception.
Expand All @@ -361,7 +357,11 @@ export class TestInfoImpl implements TestInfo {
this._hasHardError = true;
if (this.status === 'passed' || this.status === 'skipped')
this.status = 'failed';
this.errors.push(error);
const serialized = serializeError(error);
const step = (error as any)[stepSymbol] as TestStepInternal | undefined;
if (step && step.boxedStack)
serialized.stack = `${error.message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
this.errors.push(serialized);
}

async _runAsStepWithRunnable<T>(
Expand Down Expand Up @@ -486,3 +486,5 @@ export class TestInfoImpl implements TestInfo {

class SkipError extends Error {
}

const stepSymbol = Symbol('step');
8 changes: 4 additions & 4 deletions packages/playwright/src/worker/workerMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export class WorkerMain extends ProcessRunner {
// and unhandled errors - both lead to the test failing. This is good for regular tests,
// so that you can, e.g. expect() from inside an event handler. The test fails,
// and we restart the worker.
this._currentTest._failWithError(serializeError(error), true /* isHardError */);
this._currentTest._failWithError(error, true /* isHardError */);

// For tests marked with test.fail(), this might be a problem when unhandled error
// is not coming from the user test code (legit failure), but from fixtures or test runner.
Expand Down Expand Up @@ -395,7 +395,7 @@ export class WorkerMain extends ProcessRunner {
const afterHooksSlot = testInfo._didTimeout ? { timeout: this._project.project.timeout, elapsed: 0 } : undefined;
await testInfo._runAsStepWithRunnable({ category: 'hook', title: 'After Hooks', runnableType: 'afterHooks', runnableSlot: afterHooksSlot }, async step => {
testInfo._afterHooksStep = step;
let firstAfterHooksError: TestInfoError | undefined;
let firstAfterHooksError: Error | undefined;
await testInfo._runWithTimeout(async () => {
// Note: do not wrap all teardown steps together, because failure in any of them
// does not prevent further teardown steps from running.
Expand Down Expand Up @@ -541,11 +541,11 @@ export class WorkerMain extends ProcessRunner {
throw beforeAllError;
}

private async _runAfterAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl) {
private async _runAfterAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl): Promise<Error | undefined> {
if (!this._activeSuites.has(suite))
return;
this._activeSuites.delete(suite);
let firstError: TestInfoError | undefined;
let firstError: Error | undefined;
for (const hook of suite._hooks) {
if (hook.type !== 'afterAll')
continue;
Expand Down
1 change: 1 addition & 0 deletions tests/playwright-test/playwright.trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ test('should record with custom page fixture', async ({ runInlineTest }, testInf
});

test('should expand expect.toPass', async ({ runInlineTest }, testInfo) => {
test.fixme(true, 'fix the zones');
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = { use: { trace: { mode: 'on' } } };
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright-test/reporter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ for (const useIntermediateMergeReport of [false, true] as const) {
`begin {\"title\":\"expect.toBeTruthy\",\"category\":\"expect\"}`,
`end {\"title\":\"expect.toBeTruthy\",\"category\":\"expect\"}`,
`begin {\"title\":\"expect.toBeTruthy\",\"category\":\"expect\"}`,
`end {\"title\":\"expect.toBeTruthy\",\"category\":\"expect\",\"error\":{\"message\":\"\\u001b[2mexpect(\\u001b[22m\\u001b[31mreceived\\u001b[39m\\u001b[2m).\\u001b[22mtoBeTruthy\\u001b[2m()\\u001b[22m\\n\\nReceived: \\u001b[31mfalse\\u001b[39m\",\"stack\":\"<stack>\",\"location\":\"<location>\",\"snippet\":\"<snippet>\"}}`,
`end {\"title\":\"expect.toBeTruthy\",\"category\":\"expect\",\"error\":{\"message\":\"Error: \\u001b[2mexpect(\\u001b[22m\\u001b[31mreceived\\u001b[39m\\u001b[2m).\\u001b[22mtoBeTruthy\\u001b[2m()\\u001b[22m\\n\\nReceived: \\u001b[31mfalse\\u001b[39m\",\"stack\":\"<stack>\",\"location\":\"<location>\",\"snippet\":\"<snippet>\"}}`,
`begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
`end {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
`begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
Expand Down

0 comments on commit 09087a5

Please sign in to comment.