From a052cd19f16355eb4749be00f65c7f834c2369f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Z=C3=BCnd?= Date: Fri, 6 Feb 2026 11:37:47 +0100 Subject: [PATCH] chore: implement formatting for Error.cause messages --- src/DevtoolsUtils.ts | 13 +- src/formatters/ConsoleFormatter.ts | 46 +++++-- .../ConsoleFormatter.test.js.snapshot | 25 ++++ tests/formatters/ConsoleFormatter.test.ts | 126 ++++++++++++++++++ 4 files changed, 200 insertions(+), 10 deletions(-) diff --git a/src/DevtoolsUtils.ts b/src/DevtoolsUtils.ts index 018d41900..81f559b2d 100644 --- a/src/DevtoolsUtils.ts +++ b/src/DevtoolsUtils.ts @@ -255,10 +255,15 @@ export class SymbolizedError { targetId: string; includeStackAndCause?: boolean; resolvedStackTraceForTesting?: DevTools.StackTrace.StackTrace.StackTrace; + resolvedCauseForTesting?: SymbolizedError; }): Promise { const message = SymbolizedError.#getMessage(opts.details); if (!opts.includeStackAndCause || !opts.devTools) { - return new SymbolizedError(message, opts.resolvedStackTraceForTesting); + return new SymbolizedError( + message, + opts.resolvedStackTraceForTesting, + opts.resolvedCauseForTesting, + ); } let stackTrace: DevTools.StackTrace.StackTrace.StackTrace | undefined; @@ -278,7 +283,11 @@ export class SymbolizedError { // TODO: Turn opts.details.exception into a JSHandle and retrieve the 'cause' property. // If its an Error, recursively create a SymbolizedError. - return new SymbolizedError(message, stackTrace); + let cause: SymbolizedError | undefined; + if (opts.resolvedCauseForTesting) { + cause = opts.resolvedCauseForTesting; + } + return new SymbolizedError(message, stackTrace, cause); } static async fromError(opts: { diff --git a/src/formatters/ConsoleFormatter.ts b/src/formatters/ConsoleFormatter.ts index c0f5ad8a2..d9a895346 100644 --- a/src/formatters/ConsoleFormatter.ts +++ b/src/formatters/ConsoleFormatter.ts @@ -19,6 +19,7 @@ export interface ConsoleFormatterOptions { devTools?: TargetUniverse; resolvedArgsForTesting?: unknown[]; resolvedStackTraceForTesting?: DevTools.DevTools.StackTrace.StackTrace.StackTrace; + resolvedCauseForTesting?: SymbolizedError; } export class ConsoleFormatter { @@ -30,7 +31,7 @@ export class ConsoleFormatter { readonly #resolvedArgs: unknown[]; readonly #stack?: DevTools.DevTools.StackTrace.StackTrace.StackTrace; - readonly #cause?: SymbolizedError; // eslint-disable-line no-unused-private-class-members + readonly #cause?: SymbolizedError; private constructor(params: { id: number; @@ -61,6 +62,7 @@ export class ConsoleFormatter { targetId: msg.targetId, includeStackAndCause: options?.fetchDetailedData, resolvedStackTraceForTesting: options?.resolvedStackTraceForTesting, + resolvedCauseForTesting: options?.resolvedCauseForTesting, }); return new ConsoleFormatter({ id: options.id, @@ -130,7 +132,10 @@ export class ConsoleFormatter { `ID: ${this.#id}`, `Message: ${this.#type}> ${this.#text}`, this.#formatArgs(), - this.#formatStackTrace(this.#stack), + this.#formatStackTrace(this.#stack, this.#cause, { + includeHeading: true, + includeNote: true, + }), ].filter(line => !!line); return result.join('\n'); } @@ -151,7 +156,10 @@ export class ConsoleFormatter { if (arg instanceof SymbolizedError) { return [ arg.message, - this.#formatStackTrace(arg.stackTrace, /* includeHeading */ false), + this.#formatStackTrace(arg.stackTrace, arg.cause, { + includeHeading: false, + includeNote: true, + }), ] .filter(line => !!line) .join('\n'); @@ -177,19 +185,24 @@ export class ConsoleFormatter { #formatStackTrace( stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace | undefined, - includeHeading = true, + cause: SymbolizedError | undefined, + opts: {includeHeading: boolean; includeNote: boolean}, ): string { if (!stackTrace) { return ''; } - const heading = includeHeading ? ['### Stack trace'] : []; return [ - ...heading, + opts.includeHeading ? '### Stack trace' : '', this.#formatFragment(stackTrace.syncFragment), ...stackTrace.asyncFragments.map(this.#formatAsyncFragment.bind(this)), - 'Note: line and column numbers use 1-based indexing', - ].join('\n'); + this.#formatCause(cause), + opts.includeNote + ? 'Note: line and column numbers use 1-based indexing' + : '', + ] + .filter(line => !!line) + .join('\n'); } #formatFragment( @@ -217,6 +230,23 @@ export class ConsoleFormatter { } return result; } + + #formatCause(cause: SymbolizedError | undefined): string { + if (!cause) { + return ''; + } + + return [ + `Caused by: ${cause.message}`, + this.#formatStackTrace(cause.stackTrace, cause.cause, { + includeHeading: false, + includeNote: false, + }), + ] + .filter(line => !!line) + .join('\n'); + } + toJSON(): object { return { type: this.#type, diff --git a/tests/formatters/ConsoleFormatter.test.js.snapshot b/tests/formatters/ConsoleFormatter.test.js.snapshot index 87c16e94b..afc9cf115 100644 --- a/tests/formatters/ConsoleFormatter.test.js.snapshot +++ b/tests/formatters/ConsoleFormatter.test.js.snapshot @@ -35,6 +35,18 @@ at bar (foo.ts:20:2) Note: line and column numbers use 1-based indexing `; +exports[`ConsoleFormatter > toStringDetailed > formats a console message with an Error object with cause 1`] = ` +ID: 9 +Message: log> JSHandle@error +### Arguments +Arg #0: AppError: Compute failed +at foo (foo.ts:10:2) +at bar (foo.ts:20:2) +Caused by: TypeError: Cannot read properties of undefined +at compute (library.js:5:10) +Note: line and column numbers use 1-based indexing +`; + exports[`ConsoleFormatter > toStringDetailed > formats a console.error message 1`] = ` ID: 4 Message: error> Something went wrong @@ -71,6 +83,19 @@ at schedule (util.ts:5:2) Note: line and column numbers use 1-based indexing `; +exports[`ConsoleFormatter > toStringDetailed > formats an UncaughtError with a stack trace and a cause 1`] = ` +ID: 10 +Message: error> Uncaught TypeError: Cannot read properties of undefined +### Stack trace +at foo (foo.ts:10:2) +at bar (foo.ts:20:2) +--- setTimeout ------------------------- +at schedule (util.ts:5:2) +Caused by: TypeError: Cannot read properties of undefined +at compute (library.js:5:8) +Note: line and column numbers use 1-based indexing +`; + exports[`ConsoleFormatter > toStringDetailed > handles \"Execution context is not available\" error in args 1`] = ` ID: 6 Message: log> Processing file: diff --git a/tests/formatters/ConsoleFormatter.test.ts b/tests/formatters/ConsoleFormatter.test.ts index 1cd2ed2a9..ade2886c1 100644 --- a/tests/formatters/ConsoleFormatter.test.ts +++ b/tests/formatters/ConsoleFormatter.test.ts @@ -325,6 +325,132 @@ describe('ConsoleFormatter', () => { ).toStringDetailed(); t.assert.snapshot?.(result); }); + + it('formats a console message with an Error object with cause', async t => { + const message = createMockMessage({ + type: () => 'log', + text: () => 'JSHandle@error', + }); + const stackTrace = { + syncFragment: { + frames: [ + { + line: 10, + column: 2, + url: 'foo.ts', + name: 'foo', + }, + { + line: 20, + column: 2, + url: 'foo.ts', + name: 'bar', + }, + ], + }, + asyncFragments: [], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace; + const error = SymbolizedError.createForTesting( + 'AppError: Compute failed', + stackTrace, + SymbolizedError.createForTesting( + 'TypeError: Cannot read properties of undefined', + { + syncFragment: { + frames: [ + { + line: 5, + column: 10, + url: 'library.js', + name: 'compute', + }, + ], + }, + asyncFragments: [], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace, + ), + ); + + const result = ( + await ConsoleFormatter.from(message, { + id: 9, + resolvedArgsForTesting: [error], + }) + ).toStringDetailed(); + t.assert.snapshot?.(result); + }); + + it('formats an UncaughtError with a stack trace and a cause', async t => { + const stackTrace = { + syncFragment: { + frames: [ + { + line: 10, + column: 2, + url: 'foo.ts', + name: 'foo', + }, + { + line: 20, + column: 2, + url: 'foo.ts', + name: 'bar', + }, + ], + }, + asyncFragments: [ + { + description: 'setTimeout', + frames: [ + { + line: 5, + column: 2, + url: 'util.ts', + name: 'schedule', + }, + ], + }, + ], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace; + const error = new UncaughtError( + { + exceptionId: 1, + lineNumber: 0, + columnNumber: 5, + exception: { + type: 'object', + description: 'TypeError: Cannot read properties of undefined', + }, + text: 'Uncaught', + }, + '', + ); + const cause = SymbolizedError.createForTesting( + 'TypeError: Cannot read properties of undefined', + { + syncFragment: { + frames: [ + { + line: 5, + column: 8, + url: 'library.js', + name: 'compute', + }, + ], + }, + asyncFragments: [], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace, + ); + + const result = ( + await ConsoleFormatter.from(error, { + id: 10, + resolvedStackTraceForTesting: stackTrace, + resolvedCauseForTesting: cause, + }) + ).toStringDetailed(); + t.assert.snapshot?.(result); + }); }); describe('toJSON', () => { it('formats a console.log message', async () => {