From 937b7b743d06cbd80f44aef025c9f8969d33413b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Z=C3=BCnd?= Date: Tue, 3 Feb 2026 12:32:14 +0100 Subject: [PATCH] chore: implement formatting for UncaughtError --- src/DevtoolsUtils.ts | 16 ++-- src/formatters/ConsoleFormatter.ts | 74 ++++++++++++------- .../ConsoleFormatter.test.js.snapshot | 14 ++++ tests/formatters/ConsoleFormatter.test.ts | 60 +++++++++++++++ 4 files changed, 132 insertions(+), 32 deletions(-) diff --git a/src/DevtoolsUtils.ts b/src/DevtoolsUtils.ts index 826f93f98..a5903728a 100644 --- a/src/DevtoolsUtils.ts +++ b/src/DevtoolsUtils.ts @@ -236,14 +236,20 @@ export async function createStackTraceForConsoleMessage( _targetId(): string | undefined; }; const rawStackTrace = message._rawStackTrace(); - if (!rawStackTrace) { - return undefined; + if (rawStackTrace) { + return createStackTrace(devTools, rawStackTrace, message._targetId()); } + return undefined; +} +export async function createStackTrace( + devTools: TargetUniverse, + rawStackTrace: Protocol.Runtime.StackTrace, + targetId: string | undefined, +): Promise { const targetManager = devTools.universe.context.get(DevTools.TargetManager); - const messageTargetId = message._targetId(); - const target = messageTargetId - ? targetManager.targetById(messageTargetId) || devTools.target + const target = targetId + ? targetManager.targetById(targetId) || devTools.target : devTools.target; const model = target.model(DevTools.DebuggerModel) as DevTools.DebuggerModel; diff --git a/src/formatters/ConsoleFormatter.ts b/src/formatters/ConsoleFormatter.ts index 4e0fa74f3..9baaa7c34 100644 --- a/src/formatters/ConsoleFormatter.ts +++ b/src/formatters/ConsoleFormatter.ts @@ -6,8 +6,10 @@ import { createStackTraceForConsoleMessage, + createStackTrace, type TargetUniverse, } from '../DevtoolsUtils.js'; +import type {UncaughtError} from '../PageCollector.js'; import type * as DevTools from '../third_party/index.js'; import type {ConsoleMessage} from '../third_party/index.js'; @@ -19,13 +21,13 @@ export interface ConsoleFormatterOptions { } export class ConsoleFormatter { - #msg: ConsoleMessage | Error; + #msg: ConsoleMessage | Error | UncaughtError; #resolvedArgs: unknown[] = []; #resolvedStackTrace?: DevTools.DevTools.StackTrace.StackTrace.StackTrace; #id?: number; private constructor( - msg: ConsoleMessage | Error, + msg: ConsoleMessage | Error | UncaughtError, options?: ConsoleFormatterOptions, ) { this.#msg = msg; @@ -34,7 +36,7 @@ export class ConsoleFormatter { } static async from( - msg: ConsoleMessage | Error, + msg: ConsoleMessage | Error | UncaughtError, options?: ConsoleFormatterOptions, ): Promise { const formatter = new ConsoleFormatter(msg, options); @@ -44,27 +46,44 @@ export class ConsoleFormatter { return formatter; } + #isConsoleMessage( + msg: ConsoleMessage | Error | UncaughtError, + ): msg is ConsoleMessage { + // No `instanceof` as tests mock `ConsoleMessage`. + return 'args' in msg && typeof msg.args === 'function'; + } + async #loadDetailedData(devTools?: TargetUniverse): Promise { if (this.#msg instanceof Error) { return; } - this.#resolvedArgs = await Promise.all( - this.#msg.args().map(async (arg, i) => { - try { - return await arg.jsonValue(); - } catch { - return ``; - } - }), - ); + if (this.#isConsoleMessage(this.#msg)) { + this.#resolvedArgs = await Promise.all( + this.#msg.args().map(async (arg, i) => { + try { + return await arg.jsonValue(); + } catch { + return ``; + } + }), + ); + } if (devTools) { try { - this.#resolvedStackTrace = await createStackTraceForConsoleMessage( - devTools, - this.#msg, - ); + if (this.#isConsoleMessage(this.#msg)) { + this.#resolvedStackTrace = await createStackTraceForConsoleMessage( + devTools, + this.#msg, + ); + } else if (this.#msg.stackTrace) { + this.#resolvedStackTrace = await createStackTrace( + devTools, + this.#msg.stackTrace, + this.#msg.targetId, + ); + } } catch { // ignore } @@ -75,10 +94,7 @@ export class ConsoleFormatter { toString(): string { const type = this.#getType(); const text = this.#getText(); - const argsCount = - this.#msg instanceof Error - ? 0 - : this.#resolvedArgs.length || this.#msg.args().length; + const argsCount = this.#getArgsCount(); const idPart = this.#id !== undefined ? `msgid=${this.#id} ` : ''; return `${idPart}[${type}] ${text} (${argsCount} args)`; } @@ -95,21 +111,21 @@ export class ConsoleFormatter { } #getType(): string { - if (this.#msg instanceof Error) { + if (!this.#isConsoleMessage(this.#msg)) { return 'error'; } return this.#msg.type(); } #getText(): string { - if (this.#msg instanceof Error) { + if (!this.#isConsoleMessage(this.#msg)) { return this.#msg.message; } return this.#msg.text(); } #getArgs(): unknown[] { - if (this.#msg instanceof Error) { + if (!this.#isConsoleMessage(this.#msg)) { return []; } if (this.#resolvedArgs.length > 0) { @@ -123,6 +139,13 @@ export class ConsoleFormatter { return []; } + #getArgsCount(): number { + if (!this.#isConsoleMessage(this.#msg)) { + return 0; + } + return this.#resolvedArgs.length || this.#msg.args().length; + } + #formatArg(arg: unknown) { return typeof arg === 'object' ? JSON.stringify(arg) : String(arg); } @@ -185,10 +208,7 @@ export class ConsoleFormatter { return { type: this.#getType(), text: this.#getText(), - argsCount: - this.#msg instanceof Error - ? 0 - : this.#resolvedArgs.length || this.#msg.args().length, + argsCount: this.#getArgsCount(), id: this.#id, }; } diff --git a/tests/formatters/ConsoleFormatter.test.js.snapshot b/tests/formatters/ConsoleFormatter.test.js.snapshot index 00520af77..f42a4b1f2 100644 --- a/tests/formatters/ConsoleFormatter.test.js.snapshot +++ b/tests/formatters/ConsoleFormatter.test.js.snapshot @@ -10,6 +10,10 @@ exports[`ConsoleFormatter > toString > formats a console.log message with one ar msgid=2 [log] Processing file: (1 args) `; +exports[`ConsoleFormatter > toString > formats an UncaughtError 1`] = ` +msgid=4 [error] Uncaught TypeError: Cannot read properties of undefined (0 args) +`; + exports[`ConsoleFormatter > toStringDetailed > formats a console message with a stack trace 1`] = ` ID: 5 Message: log> Hello stack trace! @@ -45,6 +49,16 @@ Message: log> Processing file: Arg #0: file.txt `; +exports[`ConsoleFormatter > toStringDetailed > formats an UncaughtError with a stack trace 1`] = ` +ID: 7 +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) +`; + 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 4720ff613..aaea6bbe4 100644 --- a/tests/formatters/ConsoleFormatter.test.ts +++ b/tests/formatters/ConsoleFormatter.test.ts @@ -8,6 +8,7 @@ import assert from 'node:assert'; import {describe, it} from 'node:test'; import {ConsoleFormatter} from '../../src/formatters/ConsoleFormatter.js'; +import {UncaughtError} from '../../src/PageCollector.js'; import type {ConsoleMessage} from '../../src/third_party/index.js'; import type {DevTools} from '../../src/third_party/index.js'; @@ -66,6 +67,18 @@ describe('ConsoleFormatter', () => { ).toString(); t.assert.snapshot?.(result); }); + + it('formats an UncaughtError', async t => { + const error = new UncaughtError( + 'Uncaught TypeError: Cannot read properties of undefined', + undefined, + '', + ); + const result = ( + await ConsoleFormatter.from(error, {id: 4, fetchDetailedData: true}) + ).toString(); + t.assert.snapshot?.(result); + }); }); describe('toStringDetailed', () => { @@ -184,6 +197,53 @@ describe('ConsoleFormatter', () => { t.assert.snapshot?.(result); assert.ok(result.includes('')); }); + + it('formats an UncaughtError with a stack trace', 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( + 'Uncaught TypeError: Cannot read properties of undefined', + undefined, + '', + ); + + const result = ( + await ConsoleFormatter.from(error, { + id: 7, + resolvedStackTraceForTesting: stackTrace, + }) + ).toStringDetailed(); + t.assert.snapshot?.(result); + }); }); describe('toJSON', () => { it('formats a console.log message', async () => {