feat(pwt): serialize and expose Error.cause from Worker process (#32833)

This commit is contained in:
Max Schmitt 2024-10-29 00:01:59 +01:00 committed by GitHub
parent 5c0fdfed50
commit 9eb4fe5546
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 90 additions and 7 deletions

View File

@ -4,6 +4,12 @@
Information about an error thrown during test execution.
## property: TestInfoError.cause
* since: v1.49
- type: ?<[TestInfoError]>
Error cause. Set when there is a [cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) for the error. Will be `undefined` if there is no cause or if the cause is not an instance of [Error].
## property: TestInfoError.message
* since: v1.10
- type: ?<[string]>

View File

@ -4,6 +4,12 @@
Information about an error thrown during test execution.
## property: TestError.cause
* since: v1.49
- type: ?<[TestError]>
Error cause. Set when there is a [cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) for the error. Will be `undefined` if there is no cause or if the cause is not an instance of [Error].
## property: TestError.message
* since: v1.10
- type: ?<[string]>

View File

@ -434,15 +434,16 @@ export function formatError(error: TestError, highlightCode: boolean): ErrorDeta
tokens.push(snippet);
}
if (parsedStack && parsedStack.stackLines.length) {
tokens.push('');
if (parsedStack && parsedStack.stackLines.length)
tokens.push(colors.dim(parsedStack.stackLines.join('\n')));
}
let location = error.location;
if (parsedStack && !location)
location = parsedStack.location;
if (error.cause)
tokens.push(colors.dim('[cause]: ') + formatError(error.cause, highlightCode).message);
return {
location,
message: tokens.join('\n'),

View File

@ -29,15 +29,17 @@ import type { TestInfoErrorImpl } from './common/ipc';
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json'));
export function filterStackTrace(e: Error): { message: string, stack: string } {
export function filterStackTrace(e: Error): { message: string, stack: string, cause?: ReturnType<typeof filterStackTrace> } {
const name = e.name ? e.name + ': ' : '';
const cause = e.cause instanceof Error ? filterStackTrace(e.cause) : undefined;
if (process.env.PWDEBUGIMPL)
return { message: name + e.message, stack: e.stack || '' };
return { message: name + e.message, stack: e.stack || '', cause };
const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || []));
return {
message: name + e.message,
stack: `${name}${e.message}${stackLines.map(line => '\n' + line).join('')}`
stack: `${name}${e.message}${stackLines.map(line => '\n' + line).join('')}`,
cause,
};
}

View File

@ -224,11 +224,18 @@ export class TestTracing {
const stack = rawStack ? filteredStackTrace(rawStack) : [];
this._appendTraceEvent({
type: 'error',
message: error.message || String(error.value),
message: this._formatError(error),
stack,
});
}
_formatError(error: TestInfoErrorImpl) {
const parts: string[] = [error.message || String(error.value)];
if (error.cause)
parts.push('[cause]: ' + this._formatError(error.cause));
return parts.join('\n');
}
appendStdioToTrace(type: 'stdout' | 'stderr', chunk: string | Buffer) {
this._appendTraceEvent({
type,

View File

@ -9152,6 +9152,13 @@ export interface TestInfo {
* Information about an error thrown during test execution.
*/
export interface TestInfoError {
/**
* Error cause. Set when there is a
* [cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) for the
* error. Will be `undefined` if there is no cause or if the cause is not an instance of [Error].
*/
cause?: TestInfoError;
/**
* Error message. Set when [Error] (or its subclass) has been thrown.
*/

View File

@ -554,6 +554,13 @@ export interface TestCase {
* Information about an error thrown during test execution.
*/
export interface TestError {
/**
* Error cause. Set when there is a
* [cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) for the
* error. Will be `undefined` if there is no cause or if the cause is not an instance of [Error].
*/
cause?: TestError;
/**
* Error location in the source code.
*/

View File

@ -118,6 +118,53 @@ for (const useIntermediateMergeReport of [false, true] as const) {
expect(output).toContain(`a.spec.ts:5:13`);
});
test('should print error with a nested cause', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('foobar', async ({}) => {
try {
try {
const error = new Error('my-message');
error.name = 'SpecialError';
throw error;
} catch (e) {
try {
throw new Error('inner-message', { cause: e });
} catch (e) {
throw new Error('outer-message', { cause: e });
}
}
} catch (e) {
throw new Error('wrapper-message', { cause: e });
}
});
test.afterAll(() => {
expect(test.info().errors.length).toBe(1);
expect(test.info().errors[0]).toBe(test.info().error);
expect(test.info().error.message).toBe('Error: wrapper-message');
expect(test.info().error.cause.message).toBe('Error: outer-message');
expect(test.info().error.cause.cause.message).toBe('Error: inner-message');
expect(test.info().error.cause.cause.cause.message).toBe('SpecialError: my-message');
expect(test.info().error.cause.cause.cause.cause).toBe(undefined);
console.log('afterAll executed successfully');
})
`
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
const testFile = path.join(result.report.config.rootDir, result.report.suites[0].specs[0].file);
expect(result.output).toContain(`${testFile}:18:21`);
expect(result.output).toContain(`[cause]: Error: outer-message`);
expect(result.output).toContain(`${testFile}:14:25`);
expect(result.output).toContain(`[cause]: Error: inner-message`);
expect(result.output).toContain(`${testFile}:12:25`);
expect(result.output).toContain(`[cause]: SpecialError: my-message`);
expect(result.output).toContain(`${testFile}:7:31`);
expect(result.output).toContain('afterAll executed successfully');
});
test('should print codeframe from a helper', async ({ runInlineTest }) => {
const result = await runInlineTest({
'helper.ts': `