From b825ef87c5aa7b36f56d8a265f50a31f0b66b05d Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 6 Aug 2025 18:03:32 +0200 Subject: [PATCH] fix: make sure test errors always have `stacks` property in Node.js context (#8392) --- .../components/views/ViewReport.spec.ts | 2 ++ packages/ui/client/composables/error.ts | 2 ++ packages/utils/src/source-map.ts | 4 +++- packages/utils/src/types.ts | 2 +- packages/vitest/src/api/setup.ts | 22 +---------------- packages/vitest/src/node/logger.ts | 2 +- packages/vitest/src/node/printError.ts | 17 +++++++++++++ packages/vitest/src/node/test-run.ts | 24 +++++++++++++++++++ packages/vitest/src/utils/source-map.ts | 1 + test/browser/specs/runner.test.ts | 21 ++++++++++++++++ test/reporters/src/data.ts | 1 + .../tests/__snapshots__/html.test.ts.snap | 8 +++++++ 12 files changed, 82 insertions(+), 24 deletions(-) diff --git a/packages/ui/client/components/views/ViewReport.spec.ts b/packages/ui/client/components/views/ViewReport.spec.ts index 4181c9d6d..3a8cd2ff6 100644 --- a/packages/ui/client/components/views/ViewReport.spec.ts +++ b/packages/ui/client/components/views/ViewReport.spec.ts @@ -104,6 +104,7 @@ describe('ViewReport', () => { errors: [ { name: 'Do some test', + stacks: [], stack: '\x1B[33mtest/plain-stack-trace.ts\x1B[0m', message: 'Error: Transform failed with 1 error:', diff, @@ -163,6 +164,7 @@ describe('ViewReport', () => { { name: 'Do some test', stack: '\x1B[33mtest/plain-stack-trace.ts\x1B[0m', + stacks: [], message: '\x1B[44mError: Transform failed with 1 error:\x1B[0m', diff, }, diff --git a/packages/ui/client/composables/error.ts b/packages/ui/client/composables/error.ts index 311f8a8c1..9bb4db0ea 100644 --- a/packages/ui/client/composables/error.ts +++ b/packages/ui/client/composables/error.ts @@ -40,6 +40,7 @@ export function parseError(e: unknown) { message: String(error).split(/\n/g)[0], stack: String(error), name: '', + stacks: [], } } @@ -49,6 +50,7 @@ export function parseError(e: unknown) { message: err.message, stack: err.stack, name: '', + stacks: [], } } diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index 0b66d0432..52baddbc7 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -23,7 +23,7 @@ export interface StackTraceParserOptions { const CHROME_IE_STACK_REGEXP = /^\s*at .*(?:\S:\d+|\(native\))/m const SAFARI_NATIVE_CODE_REGEXP = /^(?:eval@)?(?:\[native code\])?$/ -const stackIgnorePatterns = [ +const stackIgnorePatterns: (string | RegExp)[] = [ 'node:internal', /\/packages\/\w+\/dist\//, /\/@vitest\/\w+\/dist\//, @@ -47,6 +47,8 @@ const stackIgnorePatterns = [ /\/deps\/vitest_/, ] +export { stackIgnorePatterns as defaultStackIgnorePatterns } + function extractLocation(urlLike: string) { // Fail-fast but return locations like "(native)" if (!urlLike.includes(':')) { diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index fb39b8175..980c5c65c 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -30,9 +30,9 @@ export interface ParsedStack { export interface SerializedError { message: string + stacks?: ParsedStack[] stack?: string name?: string - stacks?: ParsedStack[] cause?: SerializedError [key: string]: unknown } diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 828fd03ff..ce6f803d6 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -15,14 +15,13 @@ import type { WebSocketRPC, } from './types' import { existsSync, promises as fs } from 'node:fs' -import { isPrimitive, noop } from '@vitest/utils' +import { noop } from '@vitest/utils' import { createBirpc } from 'birpc' import { parse, stringify } from 'flatted' import { WebSocketServer } from 'ws' import { API_PATH } from '../constants' import { getModuleGraph } from '../utils/graph' import { stringifyReplace } from '../utils/serialization' -import { parseErrorStacktrace } from '../utils/source-map' import { isValidApiRequest } from './check' export function setup(ctx: Vitest, _server?: ViteDevServer): void { @@ -201,25 +200,6 @@ export class WebSocketReporter implements Reporter { return } - packs.forEach(([taskId, result]) => { - const task = this.ctx.state.idMap.get(taskId) - const isBrowser = task && task.file.pool === 'browser' - - result?.errors?.forEach((error) => { - if (isPrimitive(error)) { - return - } - - if (isBrowser) { - const project = this.ctx.getProjectByName(task!.file.projectName || '') - error.stacks = project.browser?.parseErrorStacktrace(error) - } - else { - error.stacks = parseErrorStacktrace(error) - } - }) - }) - this.clients.forEach((client) => { client.onTaskUpdate?.(packs, events)?.catch?.(noop) }) diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts index 97fd36fb5..2b25846b0 100644 --- a/packages/vitest/src/node/logger.ts +++ b/packages/vitest/src/node/logger.ts @@ -256,7 +256,7 @@ export class Logger { ) } - printUnhandledErrors(errors: unknown[]): void { + printUnhandledErrors(errors: ReadonlyArray): void { const errorMessage = c.red( c.bold( `\nVitest caught ${errors.length} unhandled error${ diff --git a/packages/vitest/src/node/printError.ts b/packages/vitest/src/node/printError.ts index fcda0aeeb..3a42578ed 100644 --- a/packages/vitest/src/node/printError.ts +++ b/packages/vitest/src/node/printError.ts @@ -12,6 +12,7 @@ import { normalize, relative } from 'pathe' import c from 'tinyrainbow' import { TypeCheckError } from '../typecheck/typechecker' import { + defaultStackIgnorePatterns, lineSplitRE, parseErrorStacktrace, positionToOffset, @@ -75,10 +76,26 @@ export function printError( screenshotPaths: options.screenshotPaths, printProperties: options.verbose, parseErrorStacktrace(error) { + if (error.stacks) { + const stacks = [...error.stacks.filter(stack => + project.config.onStackTrace?.(error, stack) !== false, + )] + + if (options.fullStack) { + return stacks + } + else { + return stacks.filter((stack) => { + return !defaultStackIgnorePatterns.some(p => stack.file.match(p)) + }) + } + } + // browser stack trace needs to be processed differently, // so there is a separate method for that if (options.task?.file.pool === 'browser' && project.browser) { return project.browser.parseErrorStacktrace(error, { + frameFilter: project.config.onStackTrace, ignoreStackEntries: options.fullStack ? [] : undefined, }) } diff --git a/packages/vitest/src/node/test-run.ts b/packages/vitest/src/node/test-run.ts index 7deb1326e..24326bab6 100644 --- a/packages/vitest/src/node/test-run.ts +++ b/packages/vitest/src/node/test-run.ts @@ -17,7 +17,9 @@ import type { TestRunEndReason } from './types/reporter' import assert from 'node:assert' import { createHash } from 'node:crypto' import { copyFile, mkdir } from 'node:fs/promises' +import { isPrimitive } from '@vitest/utils' import { serializeError } from '@vitest/utils/error' +import { parseErrorStacktrace } from '@vitest/utils/source-map' import mime from 'mime/lite' import { basename, dirname, extname, resolve } from 'pathe' @@ -68,6 +70,7 @@ export class TestRun { } async updated(update: TaskResultPack[], events: TaskEventPack[]): Promise { + this.syncUpdateStacks(update) this.vitest.state.updateTasks(update) for (const [id, event, data] of events) { @@ -113,6 +116,27 @@ export class TestRun { return modules.some(m => !m.ok()) } + private syncUpdateStacks(update: TaskResultPack[]): void { + update.forEach(([taskId, result]) => { + const task = this.vitest.state.idMap.get(taskId) + const isBrowser = task && task.file.pool === 'browser' + + result?.errors?.forEach((error) => { + if (isPrimitive(error)) { + return + } + + const project = this.vitest.getProjectByName(task!.file.projectName || '') + if (isBrowser) { + error.stacks = project.browser?.parseErrorStacktrace(error, { frameFilter: project.config.onStackTrace }) || [] + } + else { + error.stacks = parseErrorStacktrace(error, { frameFilter: project.config.onStackTrace }) + } + }) + }) + } + private async reportEvent(id: string, event: TaskUpdateEvent, data: TaskEventData | undefined) { const task = this.vitest.state.idMap.get(id) const entity = task && this.vitest.state.getReportedEntity(task) diff --git a/packages/vitest/src/utils/source-map.ts b/packages/vitest/src/utils/source-map.ts index 3f282ba7d..7ff88e49f 100644 --- a/packages/vitest/src/utils/source-map.ts +++ b/packages/vitest/src/utils/source-map.ts @@ -4,6 +4,7 @@ export { positionToOffset, } from '@vitest/utils' export { + defaultStackIgnorePatterns, parseErrorStacktrace, parseSingleStack, parseStacktrace, diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 1024ec5f4..5a0d17b01 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -170,8 +170,29 @@ error with a stack }) test(`stack trace points to correct file in every browser when failed`, async () => { + expect.assertions(15) const { stderr } = await runBrowserTests({ root: './fixtures/failing', + reporters: [ + 'default', + { + onTestCaseReady(testCase) { + if (testCase.name !== 'correctly fails and prints a diff') { + return + } + if (testCase.project.name === 'chromium' || testCase.project.name === 'chrome') { + expect(testCase.result().errors[0].stacks).toEqual([ + { + line: 11, + column: 12, + file: testCase.module.moduleId, + method: '', + }, + ]) + } + }, + }, + ], }) expect(stderr).toContain('expected 1 to be 2') diff --git a/test/reporters/src/data.ts b/test/reporters/src/data.ts index 2e5f4bf19..4577a46f2 100644 --- a/test/reporters/src/data.ts +++ b/test/reporters/src/data.ts @@ -54,6 +54,7 @@ const error: TestError = { actual: '2.23606797749979', expected: '2', operator: 'strictEqual', + stacks: undefined!, } error.showDiff = true error.stack = 'AssertionError: expected 2.23606797749979 to equal 2\n' diff --git a/test/reporters/tests/__snapshots__/html.test.ts.snap b/test/reporters/tests/__snapshots__/html.test.ts.snap index 27a67bf05..4ee65f10f 100644 --- a/test/reporters/tests/__snapshots__/html.test.ts.snap +++ b/test/reporters/tests/__snapshots__/html.test.ts.snap @@ -62,6 +62,14 @@ exports[`html reporter > resolves to "failing" status for test file "json-fail" "operator": "strictEqual", "showDiff": true, "stack": "AssertionError: expected 2 to deeply equal 1", + "stacks": [ + { + "column": 13, + "file": "/test/reporters/fixtures/json-fail.test.ts", + "line": 8, + "method": "", + }, + ], }, ], "repeatCount": 0, -- 2.51.2