From cd9a971af4c78e40dd750cbcf632e83986ea9a93 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 20 May 2026 14:08:08 +0200 Subject: [PATCH] fix(reporter): spread AggregateError sub-errors into testInfo.errors Fixes: https://github.com/microsoft/playwright/issues/40856 --- packages/playwright/src/worker/testInfo.ts | 20 ++++++--- tests/playwright-test/reporter.spec.ts | 47 ++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 6250ce936a20b..b3b33f12b707a 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -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; + 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) { diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index 8476629f03103..2c53e9c5d889d 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -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', + ]); +});