Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions packages/playwright/src/worker/testInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,12 +416,20 @@ export class TestInfoImpl implements TestInfo {
_failWithError(error: Error | unknown) {
if (this.status === 'passed' || this.status === 'skipped')
this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed';
const serialized = testInfoError(error);
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
if (step && step.boxedStack)
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
this.errors.push(serialized);
this._tracing.appendForError(serialized);
const visit = (error: Error | unknown) => {
const serialized = testInfoError(error);
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the step should be gathered only once, for the top-level error. Inner errors are unlikely to have the step attached.

if (step && step.boxedStack)
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
this.errors.push(serialized);
this._tracing.appendForError(serialized);
const children = (error as any)?.errors;
if (Array.isArray(children)) {
for (const child of children)
visit(child);
}
};
visit(error);
}

async _runAsStep(stepInfo: { title: string, category: 'hook' | 'fixture', location?: Location, group?: string }, cb: () => Promise<any>) {
Expand Down
47 changes: 47 additions & 0 deletions tests/playwright-test/reporter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -915,3 +915,50 @@ test('should have static annotations on result when all tests are skipped', asyn
'annotation: skip',
]);
});

test('AggregateError sub-errors are spread into testInfo.errors', async ({ runInlineTest }) => {
class TestReporter implements Reporter {
onTestEnd(test: TestCase, result: TestResult): void {
for (const error of result.errors)
console.log(`%%${error.message ?? error.value}`);
}
}

const result = await runInlineTest({
'reporter.ts': `module.exports = ${TestReporter.toString()}`,
'playwright.config.ts': `module.exports = { reporter: './reporter' };`,
'a.spec.ts': `
import { test } from '@playwright/test';
test('basic', () => {
throw new AggregateError([new Error('a'), new Error('b')], 'parent');
});
test('nested', () => {
throw new AggregateError([
new AggregateError([new Error('a'), new Error('b')], 'inner'),
new Error('c'),
], 'outer');
});
test('non-error entries', () => {
const err: any = new Error('parent');
err.errors = ['oops', { foo: 1 }, new Error('real')];
throw err;
});
`,
}, { 'reporter': '', 'workers': 1 });

expect(result.exitCode).toBe(1);
expect(result.outputLines).toEqual([
'AggregateError: parent',
'Error: a',
'Error: b',
'AggregateError: outer',
'AggregateError: inner',
'Error: a',
'Error: b',
'Error: c',
'Error: parent',
`'oops'`,
'{ foo: 1 }',
'Error: real',
]);
});
Loading