diff --git a/src/formatters/ConsoleFormatter.ts b/src/formatters/ConsoleFormatter.ts index d9a895346..b6a1f59ab 100644 --- a/src/formatters/ConsoleFormatter.ts +++ b/src/formatters/ConsoleFormatter.ts @@ -23,6 +23,8 @@ export interface ConsoleFormatterOptions { } export class ConsoleFormatter { + static readonly #STACK_TRACE_MAX_LINES = 50; + readonly #id: number; readonly #type: string; readonly #text: string; @@ -134,7 +136,6 @@ export class ConsoleFormatter { this.#formatArgs(), this.#formatStackTrace(this.#stack, this.#cause, { includeHeading: true, - includeNote: true, }), ].filter(line => !!line); return result.join('\n'); @@ -158,7 +159,6 @@ export class ConsoleFormatter { arg.message, this.#formatStackTrace(arg.stackTrace, arg.cause, { includeHeading: false, - includeNote: true, }), ] .filter(line => !!line) @@ -186,38 +186,59 @@ export class ConsoleFormatter { #formatStackTrace( stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace | undefined, cause: SymbolizedError | undefined, - opts: {includeHeading: boolean; includeNote: boolean}, + opts: {includeHeading: boolean}, ): string { if (!stackTrace) { return ''; } + const lines = this.#formatStackTraceInner(stackTrace, cause); + const includedLines = lines.slice( + 0, + ConsoleFormatter.#STACK_TRACE_MAX_LINES, + ); + const reminderCount = lines.length - includedLines.length; + return [ opts.includeHeading ? '### Stack trace' : '', - this.#formatFragment(stackTrace.syncFragment), - ...stackTrace.asyncFragments.map(this.#formatAsyncFragment.bind(this)), - this.#formatCause(cause), - opts.includeNote - ? 'Note: line and column numbers use 1-based indexing' - : '', + ...includedLines, + reminderCount > 0 ? `... and ${reminderCount} more frames` : '', + 'Note: line and column numbers use 1-based indexing', ] .filter(line => !!line) .join('\n'); } + #formatStackTraceInner( + stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace | undefined, + cause: SymbolizedError | undefined, + ): string[] { + if (!stackTrace) { + return []; + } + + return [ + ...this.#formatFragment(stackTrace.syncFragment), + ...stackTrace.asyncFragments + .map(this.#formatAsyncFragment.bind(this)) + .flat(), + ...this.#formatCause(cause), + ]; + } + #formatFragment( fragment: DevTools.DevTools.StackTrace.StackTrace.Fragment, - ): string { - return fragment.frames.map(this.#formatFrame.bind(this)).join('\n'); + ): string[] { + return fragment.frames.map(this.#formatFrame.bind(this)); } #formatAsyncFragment( fragment: DevTools.DevTools.StackTrace.StackTrace.AsyncFragment, - ): string { + ): string[] { const separatorLineLength = 40; const prefix = `--- ${fragment.description || 'async'} `; const separator = prefix + '-'.repeat(separatorLineLength - prefix.length); - return separator + '\n' + this.#formatFragment(fragment); + return [separator, ...this.#formatFragment(fragment)]; } #formatFrame(frame: DevTools.DevTools.StackTrace.StackTrace.Frame): string { @@ -231,20 +252,15 @@ export class ConsoleFormatter { return result; } - #formatCause(cause: SymbolizedError | undefined): string { + #formatCause(cause: SymbolizedError | undefined): string[] { if (!cause) { - return ''; + return []; } return [ `Caused by: ${cause.message}`, - this.#formatStackTrace(cause.stackTrace, cause.cause, { - includeHeading: false, - includeNote: false, - }), - ] - .filter(line => !!line) - .join('\n'); + ...this.#formatStackTraceInner(cause.stackTrace, cause.cause), + ]; } toJSON(): object { diff --git a/tests/formatters/ConsoleFormatter.test.js.snapshot b/tests/formatters/ConsoleFormatter.test.js.snapshot index afc9cf115..4e1e29aab 100644 --- a/tests/formatters/ConsoleFormatter.test.js.snapshot +++ b/tests/formatters/ConsoleFormatter.test.js.snapshot @@ -102,3 +102,61 @@ Message: log> Processing file: ### Arguments Arg #0: `; + +exports[`ConsoleFormatter > toStringDetailed > limits the number lines for a stack trace 1`] = ` +ID: 11 +Message: log> Hello stack trace! +### Stack trace +at fn0 (main.js:0:0) +at fn1 (main.js:1:1) +at fn2 (main.js:2:2) +at fn3 (main.js:3:3) +at fn4 (main.js:4:4) +at fn5 (main.js:5:5) +at fn6 (main.js:6:6) +at fn7 (main.js:7:7) +at fn8 (main.js:8:8) +at fn9 (main.js:9:9) +at fn10 (main.js:10:10) +at fn11 (main.js:11:11) +at fn12 (main.js:12:12) +at fn13 (main.js:13:13) +at fn14 (main.js:14:14) +at fn15 (main.js:15:15) +at fn16 (main.js:16:16) +at fn17 (main.js:17:17) +at fn18 (main.js:18:18) +at fn19 (main.js:19:19) +at fn20 (main.js:20:20) +at fn21 (main.js:21:21) +at fn22 (main.js:22:22) +at fn23 (main.js:23:23) +at fn24 (main.js:24:24) +at fn25 (main.js:25:25) +at fn26 (main.js:26:26) +at fn27 (main.js:27:27) +at fn28 (main.js:28:28) +at fn29 (main.js:29:29) +at fn30 (main.js:30:30) +at fn31 (main.js:31:31) +at fn32 (main.js:32:32) +at fn33 (main.js:33:33) +at fn34 (main.js:34:34) +at fn35 (main.js:35:35) +at fn36 (main.js:36:36) +at fn37 (main.js:37:37) +at fn38 (main.js:38:38) +at fn39 (main.js:39:39) +at fn40 (main.js:40:40) +at fn41 (main.js:41:41) +at fn42 (main.js:42:42) +at fn43 (main.js:43:43) +at fn44 (main.js:44:44) +at fn45 (main.js:45:45) +at fn46 (main.js:46:46) +at fn47 (main.js:47:47) +at fn48 (main.js:48:48) +at fn49 (main.js:49:49) +... and 50 more frames +Note: line and column numbers use 1-based indexing +`; diff --git a/tests/formatters/ConsoleFormatter.test.ts b/tests/formatters/ConsoleFormatter.test.ts index ade2886c1..5a880fc42 100644 --- a/tests/formatters/ConsoleFormatter.test.ts +++ b/tests/formatters/ConsoleFormatter.test.ts @@ -451,6 +451,34 @@ describe('ConsoleFormatter', () => { ).toStringDetailed(); t.assert.snapshot?.(result); }); + + it('limits the number lines for a stack trace', async t => { + const message = createMockMessage({ + type: () => 'log', + text: () => 'Hello stack trace!', + }); + const frames: DevTools.StackTrace.StackTrace.Frame[] = []; + for (let i = 0; i < 100; ++i) { + frames.push({ + line: i, + column: i, + url: 'main.js', + name: `fn${i}`, + }); + } + const stackTrace = { + syncFragment: {frames}, + asyncFragments: [], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace; + + const result = ( + await ConsoleFormatter.from(message, { + id: 11, + resolvedStackTraceForTesting: stackTrace, + }) + ).toStringDetailed(); + t.assert.snapshot?.(result); + }); }); describe('toJSON', () => { it('formats a console.log message', async () => {