feat(pwt): serialize and expose Error.cause from Worker process (#32833)
This commit is contained in:
parent
5c0fdfed50
commit
9eb4fe5546
|
@ -4,6 +4,12 @@
|
||||||
|
|
||||||
Information about an error thrown during test execution.
|
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
|
## property: TestInfoError.message
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
@ -4,6 +4,12 @@
|
||||||
|
|
||||||
Information about an error thrown during test execution.
|
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
|
## property: TestError.message
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
@ -434,15 +434,16 @@ export function formatError(error: TestError, highlightCode: boolean): ErrorDeta
|
||||||
tokens.push(snippet);
|
tokens.push(snippet);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedStack && parsedStack.stackLines.length) {
|
if (parsedStack && parsedStack.stackLines.length)
|
||||||
tokens.push('');
|
|
||||||
tokens.push(colors.dim(parsedStack.stackLines.join('\n')));
|
tokens.push(colors.dim(parsedStack.stackLines.join('\n')));
|
||||||
}
|
|
||||||
|
|
||||||
let location = error.location;
|
let location = error.location;
|
||||||
if (parsedStack && !location)
|
if (parsedStack && !location)
|
||||||
location = parsedStack.location;
|
location = parsedStack.location;
|
||||||
|
|
||||||
|
if (error.cause)
|
||||||
|
tokens.push(colors.dim('[cause]: ') + formatError(error.cause, highlightCode).message);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
location,
|
location,
|
||||||
message: tokens.join('\n'),
|
message: tokens.join('\n'),
|
||||||
|
|
|
@ -29,15 +29,17 @@ import type { TestInfoErrorImpl } from './common/ipc';
|
||||||
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
|
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
|
||||||
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json'));
|
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 name = e.name ? e.name + ': ' : '';
|
||||||
|
const cause = e.cause instanceof Error ? filterStackTrace(e.cause) : undefined;
|
||||||
if (process.env.PWDEBUGIMPL)
|
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') || []));
|
const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || []));
|
||||||
return {
|
return {
|
||||||
message: name + e.message,
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -224,11 +224,18 @@ export class TestTracing {
|
||||||
const stack = rawStack ? filteredStackTrace(rawStack) : [];
|
const stack = rawStack ? filteredStackTrace(rawStack) : [];
|
||||||
this._appendTraceEvent({
|
this._appendTraceEvent({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: error.message || String(error.value),
|
message: this._formatError(error),
|
||||||
stack,
|
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) {
|
appendStdioToTrace(type: 'stdout' | 'stderr', chunk: string | Buffer) {
|
||||||
this._appendTraceEvent({
|
this._appendTraceEvent({
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -9152,6 +9152,13 @@ export interface TestInfo {
|
||||||
* Information about an error thrown during test execution.
|
* Information about an error thrown during test execution.
|
||||||
*/
|
*/
|
||||||
export interface TestInfoError {
|
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.
|
* Error message. Set when [Error] (or its subclass) has been thrown.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -554,6 +554,13 @@ export interface TestCase {
|
||||||
* Information about an error thrown during test execution.
|
* Information about an error thrown during test execution.
|
||||||
*/
|
*/
|
||||||
export interface TestError {
|
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.
|
* Error location in the source code.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -118,6 +118,53 @@ for (const useIntermediateMergeReport of [false, true] as const) {
|
||||||
expect(output).toContain(`a.spec.ts:5:13`);
|
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 }) => {
|
test('should print codeframe from a helper', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'helper.ts': `
|
'helper.ts': `
|
||||||
|
|
Loading…
Reference in New Issue