[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix!(browser): print correct stacktrace (#3698)

authored by

Vladimir and committed by
GitHub
(Jul 19, 2023, 3:41 PM +0200) f56de2a4 a90d64fe

+299 -216
+4 -3
pnpm-lock.yaml
··· 1206 1206 pretty-format: 1207 1207 specifier: ^29.5.0 1208 1208 version: 29.5.0 1209 + devDependencies: 1210 + '@jridgewell/trace-mapping': 1211 + specifier: ^0.3.18 1212 + version: 0.3.18 1209 1213 1210 1214 packages/vite-node: 1211 1215 dependencies: ··· 1328 1332 '@edge-runtime/vm': 1329 1333 specifier: 3.0.3 1330 1334 version: 3.0.3 1331 - '@jridgewell/trace-mapping': 1332 - specifier: ^0.3.18 1333 - version: 0.3.18 1334 1335 '@sinonjs/fake-timers': 1335 1336 specifier: ^11.0.0 1336 1337 version: 11.0.0
+1 -1
packages/browser/package.json
··· 39 39 "prepublishOnly": "pnpm build" 40 40 }, 41 41 "peerDependencies": { 42 - "vitest": ">=0.32.3" 42 + "vitest": ">=0.34.0" 43 43 }, 44 44 "dependencies": { 45 45 "estree-walker": "^3.0.3",
+7
packages/utils/package.json
··· 32 32 "types": "./dist/helpers.d.ts", 33 33 "import": "./dist/helpers.js" 34 34 }, 35 + "./source-map": { 36 + "types": "./dist/source-map.d.ts", 37 + "import": "./dist/source-map.js" 38 + }, 35 39 "./*": "./*" 36 40 }, 37 41 "main": "./dist/index.js", ··· 50 54 "diff-sequences": "^29.4.3", 51 55 "loupe": "^2.3.6", 52 56 "pretty-format": "^29.5.0" 57 + }, 58 + "devDependencies": { 59 + "@jridgewell/trace-mapping": "^0.3.18" 53 60 } 54 61 }
+6 -5
packages/utils/rollup.config.js
··· 7 7 import pkg from './package.json' assert { type: 'json' } 8 8 9 9 const entries = { 10 - index: 'src/index.ts', 11 - helpers: 'src/helpers.ts', 12 - diff: 'src/diff/index.ts', 13 - error: 'src/error.ts', 14 - types: 'src/types.ts', 10 + 'index': 'src/index.ts', 11 + 'helpers': 'src/helpers.ts', 12 + 'diff': 'src/diff/index.ts', 13 + 'error': 'src/error.ts', 14 + 'source-map': 'src/source-map.ts', 15 + 'types': 'src/types.ts', 15 16 } 16 17 17 18 const external = [
+1
packages/utils/source-map.ts
··· 1 + export * from './dist/source-map.js'
-83
packages/vitest/LICENSE.md
··· 58 58 59 59 --------------------------------------- 60 60 61 - ## @jridgewell/resolve-uri 62 - License: MIT 63 - By: Justin Ridgewell 64 - Repository: https://github.com/jridgewell/resolve-uri 65 - 66 - > Copyright 2019 Justin Ridgewell <jridgewell@google.com> 67 - > 68 - > Permission is hereby granted, free of charge, to any person obtaining a copy 69 - > of this software and associated documentation files (the "Software"), to deal 70 - > in the Software without restriction, including without limitation the rights 71 - > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 72 - > copies of the Software, and to permit persons to whom the Software is 73 - > furnished to do so, subject to the following conditions: 74 - > 75 - > The above copyright notice and this permission notice shall be included in 76 - > all copies or substantial portions of the Software. 77 - > 78 - > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 - > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 80 - > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 81 - > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 82 - > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 83 - > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 84 - > SOFTWARE. 85 - 86 - --------------------------------------- 87 - 88 - ## @jridgewell/sourcemap-codec 89 - License: MIT 90 - By: Rich Harris 91 - Repository: git+https://github.com/jridgewell/sourcemap-codec.git 92 - 93 - > The MIT License 94 - > 95 - > Copyright (c) 2015 Rich Harris 96 - > 97 - > Permission is hereby granted, free of charge, to any person obtaining a copy 98 - > of this software and associated documentation files (the "Software"), to deal 99 - > in the Software without restriction, including without limitation the rights 100 - > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 101 - > copies of the Software, and to permit persons to whom the Software is 102 - > furnished to do so, subject to the following conditions: 103 - > 104 - > The above copyright notice and this permission notice shall be included in 105 - > all copies or substantial portions of the Software. 106 - > 107 - > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 108 - > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 109 - > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 110 - > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 111 - > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 112 - > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 113 - > THE SOFTWARE. 114 - 115 - --------------------------------------- 116 - 117 - ## @jridgewell/trace-mapping 118 - License: MIT 119 - By: Justin Ridgewell 120 - Repository: git+https://github.com/jridgewell/trace-mapping.git 121 - 122 - > Copyright 2022 Justin Ridgewell <justin@ridgewell.name> 123 - > 124 - > Permission is hereby granted, free of charge, to any person obtaining a copy 125 - > of this software and associated documentation files (the "Software"), to deal 126 - > in the Software without restriction, including without limitation the rights 127 - > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 128 - > copies of the Software, and to permit persons to whom the Software is 129 - > furnished to do so, subject to the following conditions: 130 - > 131 - > The above copyright notice and this permission notice shall be included in 132 - > all copies or substantial portions of the Software. 133 - > 134 - > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 135 - > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 136 - > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 137 - > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 138 - > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 139 - > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 140 - > SOFTWARE. 141 - 142 - --------------------------------------- 143 - 144 61 ## @nodelib/fs.scandir 145 62 License: MIT 146 63 Repository: https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.scandir
-1
packages/vitest/package.json
··· 163 163 "@ampproject/remapping": "^2.2.1", 164 164 "@antfu/install-pkg": "^0.1.1", 165 165 "@edge-runtime/vm": "3.0.3", 166 - "@jridgewell/trace-mapping": "^0.3.18", 167 166 "@sinonjs/fake-timers": "^11.0.0", 168 167 "@types/diff": "^5.0.3", 169 168 "@types/estree": "^1.0.1",
+1 -1
test/browser/package.json
··· 6 6 "test": "pnpm run test:webdriverio && pnpm run test:playwright", 7 7 "test:webdriverio": "PROVIDER=webdriverio node --test specs/", 8 8 "test:playwright": "PROVIDER=playwright node --test specs/", 9 - "coverage": "vitest --coverage.enabled --coverage.provider=istanbul" 9 + "coverage": "vitest --coverage.enabled --coverage.provider=istanbul --browser.headless=yes" 10 10 }, 11 11 "devDependencies": { 12 12 "@vitest/browser": "workspace:*",
+1 -1
test/browser/vitest.config.mts
··· 11 11 include: ['test/**.test.{ts,js}'], 12 12 browser: { 13 13 enabled: true, 14 - name: 'chrome', 14 + name: process.env.BROWSER || 'chrome', 15 15 headless: false, 16 16 provider: process.env.PROVIDER || 'webdriverio', 17 17 },
+1 -1
packages/utils/src/index.ts
··· 7 7 export * from './constants' 8 8 export * from './colors' 9 9 export * from './base' 10 - export * from './source-map' 10 + export * from './offset'
+42
packages/utils/src/offset.ts
··· 1 + export const lineSplitRE = /\r?\n/ 2 + 3 + export function positionToOffset( 4 + source: string, 5 + lineNumber: number, 6 + columnNumber: number, 7 + ): number { 8 + const lines = source.split(lineSplitRE) 9 + const nl = /\r\n/.test(source) ? 2 : 1 10 + let start = 0 11 + 12 + if (lineNumber > lines.length) 13 + return source.length 14 + 15 + for (let i = 0; i < lineNumber - 1; i++) 16 + start += lines[i].length + nl 17 + 18 + return start + columnNumber 19 + } 20 + 21 + export function offsetToLineNumber( 22 + source: string, 23 + offset: number, 24 + ): number { 25 + if (offset > source.length) { 26 + throw new Error( 27 + `offset is longer than source length! offset ${offset} > length ${source.length}`, 28 + ) 29 + } 30 + const lines = source.split(lineSplitRE) 31 + const nl = /\r\n/.test(source) ? 2 : 1 32 + let counted = 0 33 + let line = 0 34 + for (; line < lines.length; line++) { 35 + const lineLength = lines[line].length + nl 36 + if (counted + lineLength >= offset) 37 + break 38 + 39 + counted += lineLength 40 + } 41 + return line + 1 42 + }
+95 -59
packages/utils/src/source-map.ts
··· 1 1 import { resolve } from 'pathe' 2 + import type { SourceMapInput } from '@jridgewell/trace-mapping' 3 + import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping' 2 4 import type { ErrorWithDiff, ParsedStack } from './types' 3 5 import { isPrimitive, notNullish } from './helpers' 4 6 5 - export const lineSplitRE = /\r?\n/ 7 + export { TraceMap, originalPositionFor, generatedPositionFor } from '@jridgewell/trace-mapping' 8 + export type { SourceMapInput } from '@jridgewell/trace-mapping' 9 + 10 + export interface StackTraceParserOptions { 11 + ignoreStackEntries?: (RegExp | string)[] 12 + getSourceMap?: (file: string) => unknown 13 + } 14 + 15 + const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m 16 + const SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/ 6 17 7 18 const stackIgnorePatterns = [ 8 19 'node:internal', ··· 15 26 '/node_modules/chai/', 16 27 '/node_modules/tinypool/', 17 28 '/node_modules/tinyspy/', 29 + '/deps/chai.js', 30 + /__vitest_browser__/, 18 31 ] 19 32 20 33 function extractLocation(urlLike: string) { ··· 26 39 const parts = regExp.exec(urlLike.replace(/^\(|\)$/g, '')) 27 40 if (!parts) 28 41 return [urlLike] 29 - return [parts[1], parts[2] || undefined, parts[3] || undefined] 42 + let url = parts[1] 43 + if (url.startsWith('http:') || url.startsWith('https:')) { 44 + const urlObj = new URL(url) 45 + url = urlObj.pathname 46 + } 47 + if (url.startsWith('/@fs/')) { 48 + url 49 + = url.slice(typeof process !== 'undefined' && process.platform === 'win32' ? 5 : 4) 50 + } 51 + return [url, parts[2] || undefined, parts[3] || undefined] 52 + } 53 + 54 + export function parseSingleFFOrSafariStack(raw: string): ParsedStack | null { 55 + let line = raw.trim() 56 + 57 + if (SAFARI_NATIVE_CODE_REGEXP.test(line)) 58 + return null 59 + 60 + if (line.includes(' > eval')) 61 + line = line.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ':$1') 62 + 63 + if (!line.includes('@') && !line.includes(':')) 64 + return null 65 + 66 + const functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/ 67 + const matches = line.match(functionNameRegex) 68 + const functionName = matches && matches[1] ? matches[1] : undefined 69 + const [url, lineNumber, columnNumber] = extractLocation(line.replace(functionNameRegex, '')) 70 + 71 + if (!url || !lineNumber || !columnNumber) 72 + return null 73 + 74 + return { 75 + file: url, 76 + method: functionName || '', 77 + line: Number.parseInt(lineNumber), 78 + column: Number.parseInt(columnNumber), 79 + } 80 + } 81 + 82 + export function parseSingleStack(raw: string) { 83 + const line = raw.trim() 84 + if (!CHROME_IE_STACK_REGEXP.test(line)) 85 + return parseSingleFFOrSafariStack(line) 86 + return parseSingleV8Stack(line) 30 87 } 31 88 32 89 // Based on https://github.com/stacktracejs/error-stack-parser 33 90 // Credit to stacktracejs 34 - export function parseSingleStack(raw: string): ParsedStack | null { 91 + export function parseSingleV8Stack(raw: string): ParsedStack | null { 35 92 let line = raw.trim() 93 + 94 + if (!CHROME_IE_STACK_REGEXP.test(line)) 95 + return null 36 96 37 97 if (line.includes('(eval ')) 38 98 line = line.replace(/eval code/g, 'eval').replace(/(\(eval at [^()]*)|(,.*$)/g, '') ··· 75 135 } 76 136 } 77 137 78 - export function parseStacktrace(stack: string, ignore = stackIgnorePatterns): ParsedStack[] { 79 - const stackFrames = stack 80 - .split('\n') 81 - .map((raw): ParsedStack | null => { 82 - const stack = parseSingleStack(raw) 83 - 84 - if (!stack || (ignore.length && ignore.some(p => stack.file.match(p)))) 85 - return null 86 - 138 + export function parseStacktrace(stack: string, options: StackTraceParserOptions = {}): ParsedStack[] { 139 + const { ignoreStackEntries = stackIgnorePatterns } = options 140 + let stacks = !CHROME_IE_STACK_REGEXP.test(stack) 141 + ? parseFFOrSafariStackTrace(stack) 142 + : parseV8Stacktrace(stack) 143 + if (ignoreStackEntries.length) 144 + stacks = stacks.filter(stack => !ignoreStackEntries.some(p => stack.file.match(p))) 145 + return stacks.map((stack) => { 146 + const map = options.getSourceMap?.(stack.file) as SourceMapInput | null | undefined 147 + if (!map || typeof map !== 'object' || !map.version) 87 148 return stack 88 - }) 89 - .filter(notNullish) 90 - 91 - return stackFrames 149 + const traceMap = new TraceMap(map) 150 + const { line, column } = originalPositionFor(traceMap, stack) 151 + if (line != null && column != null) 152 + return { ...stack, line, column } 153 + return stack 154 + }) 92 155 } 93 156 94 - export function parseErrorStacktrace(e: ErrorWithDiff, ignore = stackIgnorePatterns): ParsedStack[] { 157 + function parseFFOrSafariStackTrace(stack: string): ParsedStack[] { 158 + return stack 159 + .split('\n') 160 + .map(line => parseSingleFFOrSafariStack(line)) 161 + .filter(notNullish) 162 + } 163 + 164 + function parseV8Stacktrace(stack: string): ParsedStack[] { 165 + return stack 166 + .split('\n') 167 + .map(line => parseSingleV8Stack(line)) 168 + .filter(notNullish) 169 + } 170 + 171 + export function parseErrorStacktrace(e: ErrorWithDiff, options: StackTraceParserOptions = {}): ParsedStack[] { 95 172 if (!e || isPrimitive(e)) 96 173 return [] 97 174 ··· 99 176 return e.stacks 100 177 101 178 const stackStr = e.stack || e.stackStr || '' 102 - const stackFrames = parseStacktrace(stackStr, ignore) 179 + const stackFrames = parseStacktrace(stackStr, options) 103 180 104 181 e.stacks = stackFrames 105 182 return stackFrames 106 - } 107 - 108 - export function positionToOffset( 109 - source: string, 110 - lineNumber: number, 111 - columnNumber: number, 112 - ): number { 113 - const lines = source.split(lineSplitRE) 114 - const nl = /\r\n/.test(source) ? 2 : 1 115 - let start = 0 116 - 117 - if (lineNumber > lines.length) 118 - return source.length 119 - 120 - for (let i = 0; i < lineNumber - 1; i++) 121 - start += lines[i].length + nl 122 - 123 - return start + columnNumber 124 - } 125 - 126 - export function offsetToLineNumber( 127 - source: string, 128 - offset: number, 129 - ): number { 130 - if (offset > source.length) { 131 - throw new Error( 132 - `offset is longer than source length! offset ${offset} > length ${source.length}`, 133 - ) 134 - } 135 - const lines = source.split(lineSplitRE) 136 - const nl = /\r\n/.test(source) ? 2 : 1 137 - let counted = 0 138 - let line = 0 139 - for (; line < lines.length; line++) { 140 - const lineLength = lines[line].length + nl 141 - if (counted + lineLength >= offset) 142 - break 143 - 144 - counted += lineLength 145 - } 146 - return line + 1 147 183 }
+14 -6
test/browser/specs/runner.test.mjs
··· 14 14 reject: false, 15 15 }) 16 16 17 + const browserResult = await readFile('./browser.json', 'utf-8') 18 + const browserResultJson = JSON.parse(browserResult) 19 + 20 + const getPassed = results => results.filter(result => result.status === 'passed') 21 + const getFailed = results => results.filter(result => result.status === 'failed') 22 + 23 + const passedTests = getPassed(browserResultJson.testResults) 24 + const failedTests = getFailed(browserResultJson.testResults) 25 + 17 26 await test('tests are actually running', async () => { 18 - const browserResult = await readFile('./browser.json', 'utf-8') 19 - const browserResultJson = JSON.parse(browserResult) 20 - 21 - const passedTests = browserResultJson.testResults.filter(result => result.status === 'passed') 22 - const failedTests = browserResultJson.testResults.filter(result => result.status === 'failed') 23 - 24 27 assert.ok(browserResultJson.testResults.length === 8, 'Not all the tests have been run') 25 28 assert.ok(passedTests.length === 7, 'Some tests failed') 26 29 assert.ok(failedTests.length === 1, 'Some tests have passed but should fail') ··· 59 62 assert.match(stderr, /hello from console.warn/, 'prints console.info') 60 63 assert.match(stderr, /Timer "invalid timeLog" does not exist/, 'prints errored timeLog') 61 64 assert.match(stderr, /Timer "invalid timeEnd" does not exist/, 'prints errored timeEnd') 65 + }) 66 + 67 + await test('stack trace points to correct file in every browser', () => { 68 + // dependeing on the browser it references either `.toBe()` or `expect()` 69 + assert.match(stderr, /test\/failing.test.ts:4:(12|17)/, 'prints stack trace') 62 70 }) 63 71 64 72 await test('popup apis should log a warning', () => {
+1
test/reporters/src/context.ts
··· 32 32 state: state as StateManager, 33 33 config: config as ResolvedConfig, 34 34 server: server as ViteDevServer, 35 + getProjectByTaskId: () => ({ getBrowserSourceMapModuleById: () => undefined }) as any, 35 36 } 36 37 37 38 context.logger = {
+1
packages/browser/src/client/vite.config.ts
··· 12 12 minify: false, 13 13 outDir: '../../dist/client', 14 14 emptyOutDir: false, 15 + assetsDir: '__vitest_browser__', 15 16 }, 16 17 plugins: [ 17 18 {
+2 -2
packages/snapshot/src/port/state.ts
··· 6 6 */ 7 7 8 8 import type { ParsedStack } from '@vitest/utils' 9 - import { parseErrorStacktrace } from '@vitest/utils' 9 + import { parseErrorStacktrace } from '@vitest/utils/source-map' 10 10 import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format' 11 11 import type { SnapshotData, SnapshotEnvironment, SnapshotMatchOptions, SnapshotResult, SnapshotStateOptions, SnapshotUpdateState } from '../types' 12 12 import type { InlineSnapshot } from './inlineSnapshot' ··· 128 128 ): void { 129 129 this._dirty = true 130 130 if (options.isInline) { 131 - const stacks = parseErrorStacktrace(options.error || new Error('snapshot'), []) 131 + const stacks = parseErrorStacktrace(options.error || new Error('snapshot'), { ignoreStackEntries: [] }) 132 132 const stack = this._inferInlineSnapshotStack(stacks) 133 133 if (!stack) { 134 134 throw new Error(
+10 -3
packages/vitest/src/api/setup.ts
··· 7 7 import type { WebSocket } from 'ws' 8 8 import { WebSocketServer } from 'ws' 9 9 import type { ViteDevServer } from 'vite' 10 + import type { StackTraceParserOptions } from '@vitest/utils/source-map' 10 11 import { API_PATH } from '../constants' 11 12 import type { Vitest } from '../node' 12 13 import type { File, ModuleGraphData, Reporter, TaskResultPack, UserConsoleLog } from '../types' ··· 158 159 if (this.clients.size === 0) 159 160 return 160 161 161 - packs.forEach(([, result]) => { 162 + packs.forEach(([taskId, result]) => { 163 + const project = this.ctx.getProjectByTaskId(taskId) 164 + 165 + const parserOptions: StackTraceParserOptions = { 166 + getSourceMap: file => project.getBrowserSourceMapModuleById(file), 167 + } 168 + 162 169 // TODO remove after "error" deprecation is removed 163 170 if (result?.error && !isPrimitive(result.error)) 164 - result.error.stacks = parseErrorStacktrace(result.error) 171 + result.error.stacks = parseErrorStacktrace(result.error, parserOptions) 165 172 result?.errors?.forEach((error) => { 166 173 if (!isPrimitive(error)) 167 - error.stacks = parseErrorStacktrace(error) 174 + error.stacks = parseErrorStacktrace(error, parserOptions) 168 175 }) 169 176 }) 170 177
+1 -1
packages/vitest/src/node/cli-api.ts
··· 98 98 } 99 99 catch (e) { 100 100 process.exitCode = 1 101 - await ctx.logger.printError(e, true, 'Unhandled Error') 101 + await ctx.logger.printError(e, { fullStack: true, type: 'Unhandled Error' }) 102 102 ctx.logger.error('\n\n') 103 103 return ctx 104 104 }
+11 -4
packages/vitest/src/node/core.ts
··· 8 8 import { normalizeRequestId } from 'vite-node/utils' 9 9 import { ViteNodeRunner } from 'vite-node/client' 10 10 import { SnapshotManager } from '@vitest/snapshot/manager' 11 - import type { CancelReason } from '@vitest/runner' 11 + import type { CancelReason, File } from '@vitest/runner' 12 12 import { ViteNodeServer } from 'vite-node/server' 13 13 import type { ArgumentsType, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, VitestRunMode } from '../types' 14 14 import { hasFailed, noop, slash, toArray } from '../utils' ··· 151 151 152 152 public getCoreWorkspaceProject(): WorkspaceProject | null { 153 153 return this.coreWorkspace || null 154 + } 155 + 156 + public getProjectByTaskId(taskId: string): WorkspaceProject { 157 + const task = this.state.idMap.get(taskId) 158 + const projectName = (task as File).projectName || task?.file?.projectName 159 + return this.projects.find(p => p.getName() === projectName) 160 + || this.getCoreWorkspaceProject() 161 + || this.projects[0] 154 162 } 155 163 156 164 private async resolveWorkspace(options: UserConfig, cliOptions: UserConfig) { ··· 557 565 558 566 public getModuleProjects(id: string) { 559 567 return this.projects.filter((project) => { 560 - return project.server.moduleGraph.getModuleById(id) 561 - || project.browser?.moduleGraph.getModuleById(id) 562 - || project.browser?.moduleGraph.getModulesByFile(id)?.size 568 + return project.getModuleById(id) 569 + // TODO: reevaluate || project.browser?.moduleGraph.getModulesByFile(id)?.size 563 570 }) 564 571 } 565 572
+35 -24
packages/vitest/src/node/error.ts
··· 3 3 import { normalize, relative } from 'pathe' 4 4 import c from 'picocolors' 5 5 import cliTruncate from 'cli-truncate' 6 - import { stringify } from '@vitest/utils' 6 + import type { StackTraceParserOptions } from '@vitest/utils/source-map' 7 + import { inspect } from '@vitest/utils' 7 8 import type { ErrorWithDiff, ParsedStack } from '../types' 8 9 import { lineSplitRE, parseErrorStacktrace, positionToOffset } from '../utils/source-map' 9 10 import { F_POINTER } from '../utils/figures' ··· 12 13 import type { Vitest } from './core' 13 14 import { divider } from './reporters/renderers/utils' 14 15 import type { Logger } from './logger' 16 + import type { WorkspaceProject } from './workspace' 15 17 16 18 interface PrintErrorOptions { 17 19 type?: string 20 + logger: Logger 18 21 fullStack?: boolean 19 22 showCodeFrame?: boolean 20 23 } 21 24 22 - export async function printError(error: unknown, ctx: Vitest, options: PrintErrorOptions = {}) { 25 + export async function printError(error: unknown, project: WorkspaceProject | undefined, options: PrintErrorOptions) { 23 26 const { showCodeFrame = true, fullStack = false, type } = options 27 + const logger = options.logger 24 28 let e = error as ErrorWithDiff 25 29 26 30 if (isPrimitive(e)) { ··· 39 43 } 40 44 41 45 // Error may have occured even before the configuration was resolved 42 - if (!ctx.config) 43 - return printErrorMessage(e, ctx.logger) 46 + if (!project) 47 + return printErrorMessage(e, logger) 44 48 45 - const stacks = parseErrorStacktrace(e, fullStack ? [] : undefined) 49 + const parserOptions: StackTraceParserOptions = { 50 + // only browser stack traces require remapping 51 + getSourceMap: file => project.getBrowserSourceMapModuleById(file), 52 + } 53 + 54 + if (fullStack) 55 + parserOptions.ignoreStackEntries = [] 56 + const stacks = parseErrorStacktrace(e, parserOptions) 46 57 47 58 const nearest = error instanceof TypeCheckError 48 59 ? error.stacks[0] 49 60 : stacks.find(stack => 50 - ctx.getModuleProjects(stack.file).length 61 + project.getModuleById(stack.file) 51 62 && existsSync(stack.file), 52 63 ) 53 64 54 65 const errorProperties = getErrorProperties(e) 55 66 56 67 if (type) 57 - printErrorType(type, ctx) 58 - printErrorMessage(e, ctx.logger) 68 + printErrorType(type, project.ctx) 69 + printErrorMessage(e, logger) 59 70 60 71 // E.g. AssertionError from assert does not set showDiff but has both actual and expected properties 61 72 if (e.diff) 62 - displayDiff(e.diff, ctx.logger.console) 73 + displayDiff(e.diff, logger.console) 63 74 64 75 // if the error provide the frame 65 76 if (e.frame) { 66 - ctx.logger.error(c.yellow(e.frame)) 77 + logger.error(c.yellow(e.frame)) 67 78 } 68 79 else { 69 - printStack(ctx, stacks, nearest, errorProperties, (s) => { 80 + printStack(project, stacks, nearest, errorProperties, (s) => { 70 81 if (showCodeFrame && s === nearest && nearest) { 71 82 const sourceCode = readFileSync(nearest.file, 'utf-8') 72 - ctx.logger.error(generateCodeFrame(sourceCode, 4, s.line, s.column)) 83 + logger.error(generateCodeFrame(sourceCode, 4, s.line, s.column)) 73 84 } 74 85 }) 75 86 } ··· 79 90 const afterEnvTeardown = (e as any).VITEST_AFTER_ENV_TEARDOWN 80 91 // testName has testPath inside 81 92 if (testPath) 82 - ctx.logger.error(c.red(`This error originated in "${c.bold(testPath)}" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.`)) 93 + logger.error(c.red(`This error originated in "${c.bold(testPath)}" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.`)) 83 94 if (testName) { 84 - ctx.logger.error(c.red(`The latest test that might've caused the error is "${c.bold(testName)}". It might mean one of the following:` 95 + logger.error(c.red(`The latest test that might've caused the error is "${c.bold(testName)}". It might mean one of the following:` 85 96 + '\n- The error was thrown, while Vitest was running this test.' 86 97 + '\n- This was the last recorded test before the error was thrown, if error originated after test finished its execution.')) 87 98 } 88 99 if (afterEnvTeardown) { 89 - ctx.logger.error(c.red('This error was caught after test environment was torn down. Make sure to cancel any running tasks before test finishes:' 100 + logger.error(c.red('This error was caught after test environment was torn down. Make sure to cancel any running tasks before test finishes:' 90 101 + '\n- cancel timeouts using clearTimeout and clearInterval' 91 102 + '\n- wait for promises to resolve using the await keyword')) 92 103 } 93 104 94 105 if (typeof e.cause === 'object' && e.cause && 'name' in e.cause) { 95 106 (e.cause as any).name = `Caused by: ${(e.cause as any).name}` 96 - await printError(e.cause, ctx, { fullStack, showCodeFrame: false }) 107 + await printError(e.cause, project, { fullStack, showCodeFrame: false, logger: options.logger }) 97 108 } 98 109 99 - handleImportOutsideModuleError(e.stack || e.stackStr || '', ctx) 110 + handleImportOutsideModuleError(e.stack || e.stackStr || '', logger) 100 111 } 101 112 102 113 function printErrorType(type: string, ctx: Vitest) { ··· 139 150 'Unexpected token \'export\'', 140 151 ] 141 152 142 - function handleImportOutsideModuleError(stack: string, ctx: Vitest) { 153 + function handleImportOutsideModuleError(stack: string, logger: Logger) { 143 154 if (!esmErrors.some(e => stack.includes(e))) 144 155 return 145 156 ··· 151 162 name = name.split('/')[0] 152 163 153 164 if (name) 154 - printModuleWarningForPackage(ctx.logger, path, name) 165 + printModuleWarningForPackage(logger, path, name) 155 166 else 156 - printModuleWarningForSourceCode(ctx.logger, path) 167 + printModuleWarningForSourceCode(logger, path) 157 168 } 158 169 159 170 function printModuleWarningForPackage(logger: Logger, path: string, name: string) { ··· 205 216 } 206 217 207 218 function printStack( 208 - ctx: Vitest, 219 + project: WorkspaceProject, 209 220 stack: ParsedStack[], 210 221 highlight: ParsedStack | undefined, 211 222 errorProperties: Record<string, unknown>, 212 223 onStack?: ((stack: ParsedStack) => void), 213 224 ) { 214 - const logger = ctx.logger 225 + const logger = project.ctx.logger 215 226 216 227 for (const frame of stack) { 217 228 const color = frame === highlight ? c.cyan : c.gray 218 - const path = relative(ctx.config.root, frame.file) 229 + const path = relative(project.config.root, frame.file) 219 230 220 231 logger.error(color(` ${c.dim(F_POINTER)} ${[frame.method, `${path}:${c.dim(`${frame.line}:${frame.column}`)}`].filter(Boolean).join(' ')}`)) 221 232 onStack?.(frame) ··· 225 236 const hasProperties = Object.keys(errorProperties).length > 0 226 237 if (hasProperties) { 227 238 logger.error(c.red(c.dim(divider()))) 228 - const propertiesString = stringify(errorProperties, 10, { printBasicPrototype: false }) 239 + const propertiesString = inspect(errorProperties) 229 240 logger.error(c.red(c.bold('Serialized Error:')), c.gray(propertiesString)) 230 241 } 231 242 }
+14 -4
packages/vitest/src/node/logger.ts
··· 7 7 import { RandomSequencer } from './sequencers/RandomSequencer' 8 8 import type { Vitest } from './core' 9 9 import { printError } from './error' 10 + import type { WorkspaceProject } from './workspace' 11 + 12 + interface ErrorOptions { 13 + type?: string 14 + fullStack?: boolean 15 + project?: WorkspaceProject 16 + } 10 17 11 18 export class Logger { 12 19 outputStream = process.stdout ··· 68 75 this.console.log(`\u001B[1;1H\u001B[J${log}`) 69 76 } 70 77 71 - printError(err: unknown, fullStack = false, type?: string) { 72 - return printError(err, this.ctx, { 78 + printError(err: unknown, options: ErrorOptions = {}) { 79 + const { fullStack = false, type } = options 80 + const project = options.project ?? this.ctx.getCoreWorkspaceProject() ?? this.ctx.projects[0] 81 + return printError(err, project, { 73 82 fullStack, 74 83 type, 75 84 showCodeFrame: true, 85 + logger: this, 76 86 }) 77 87 } 78 88 ··· 137 147 this.log(c.red(divider(c.bold(c.inverse(' Unhandled Errors '))))) 138 148 this.log(errorMessage) 139 149 await Promise.all(errors.map(async (err) => { 140 - await this.printError(err, true, (err as ErrorWithDiff).type || 'Unhandled Error') 150 + await this.printError(err, { fullStack: true, type: (err as ErrorWithDiff).type || 'Unhandled Error' }) 141 151 })) 142 152 this.log(c.red(divider())) 143 153 } ··· 149 159 this.log(c.red(divider(c.bold(c.inverse(' Source Errors '))))) 150 160 this.log(errorMessage) 151 161 await Promise.all(errors.map(async (err) => { 152 - await this.printError(err, true) 162 + await this.printError(err, { fullStack: true }) 153 163 })) 154 164 this.log(c.red(divider())) 155 165 }
+15 -1
packages/vitest/src/node/workspace.ts
··· 67 67 server!: ViteDevServer 68 68 vitenode!: ViteNodeServer 69 69 runner!: ViteNodeRunner 70 - browser: ViteDevServer = undefined! 70 + browser?: ViteDevServer 71 71 typechecker?: Typechecker 72 72 73 73 closingPromise: Promise<unknown> | undefined ··· 84 84 85 85 isCore() { 86 86 return this.ctx.getCoreWorkspaceProject() === this 87 + } 88 + 89 + getModuleById(id: string) { 90 + return this.server.moduleGraph.getModuleById(id) 91 + || this.browser?.moduleGraph.getModuleById(id) 92 + } 93 + 94 + getSourceMapModuleById(id: string) { 95 + const mod = this.server.moduleGraph.getModuleById(id) 96 + return mod?.ssrTransformResult?.map || mod?.transformResult?.map 97 + } 98 + 99 + getBrowserSourceMapModuleById(id: string) { 100 + return this.browser?.moduleGraph.getModuleById(id)?.transformResult?.map 87 101 } 88 102 89 103 get reporters() {
+1 -1
packages/vitest/src/typecheck/typechecker.ts
··· 2 2 import type { ExecaChildProcess } from 'execa' 3 3 import { execa } from 'execa' 4 4 import { basename, extname, resolve } from 'pathe' 5 - import { TraceMap, generatedPositionFor } from '@jridgewell/trace-mapping' 5 + import { TraceMap, generatedPositionFor } from '@vitest/utils/source-map' 6 6 import type { RawSourceMap } from '@ampproject/remapping' 7 7 import { getTasks } from '../utils' 8 8 import { ensurePackageInstalled } from '../node/pkg'
+5 -3
packages/vitest/src/utils/source-map.ts
··· 1 1 export { 2 2 lineSplitRE, 3 - parseSingleStack, 4 - parseStacktrace, 5 - parseErrorStacktrace, 6 3 positionToOffset, 7 4 offsetToLineNumber, 8 5 } from '@vitest/utils' 6 + export { 7 + parseSingleStack, 8 + parseStacktrace, 9 + parseErrorStacktrace, 10 + } from '@vitest/utils/source-map'
+1 -1
packages/vitest/src/node/plugins/index.ts
··· 160 160 (await import('../../api/setup')).setup(ctx) 161 161 } 162 162 catch (err) { 163 - await ctx.logger.printError(err, true) 163 + await ctx.logger.printError(err, { fullStack: true }) 164 164 process.exit(1) 165 165 } 166 166
+1 -1
packages/vitest/src/node/plugins/workspace.ts
··· 110 110 await project.setServer(options, server) 111 111 } 112 112 catch (err) { 113 - await project.ctx.logger.printError(err, true) 113 + await project.ctx.logger.printError(err, { fullStack: true }) 114 114 process.exit(1) 115 115 } 116 116
+1 -1
packages/vitest/src/node/pools/browser.ts
··· 38 38 const provider = project.browserProvider! 39 39 providers.add(provider) 40 40 41 - const origin = `http://${ctx.config.browser.api?.host || 'localhost'}:${project.browser.config.server.port}` 41 + const origin = `http://${ctx.config.browser.api?.host || 'localhost'}:${project.browser!.config.server.port}` 42 42 const paths = files.map(file => relative(project.config.root, file)) 43 43 44 44 const isolate = project.config.isolate
+1 -1
packages/vitest/src/node/pools/rpc.ts
··· 6 6 const ctx = project.ctx 7 7 return { 8 8 async onWorkerExit(error, code) { 9 - await ctx.logger.printError(error, false, 'Unexpected Exit') 9 + await ctx.logger.printError(error, { type: 'Unexpected Exit' }) 10 10 process.exit(code || 1) 11 11 }, 12 12 snapshotSaved(snapshot) {
+11 -3
packages/vitest/src/node/reporters/base.ts
··· 335 335 for (const task of tasks) { 336 336 // merge identical errors 337 337 task.result?.errors?.forEach((error) => { 338 - const errorItem = error?.stackStr && errorsQueue.find(i => i[0]?.stackStr === error.stackStr) 338 + const errorItem = error?.stackStr && errorsQueue.find((i) => { 339 + const hasStr = i[0]?.stackStr === error.stackStr 340 + if (!hasStr) 341 + return false 342 + const currentProjectName = (task as File)?.projectName || task.file?.projectName 343 + const projectName = (i[1][0] as File)?.projectName || i[1][0].file?.projectName 344 + return projectName === currentProjectName 345 + }) 339 346 if (errorItem) 340 347 errorItem[1].push(task) 341 348 else ··· 352 359 353 360 this.ctx.logger.error(`${c.red(c.bold(c.inverse(' FAIL ')))} ${formatProjectName(projectName)}${name}`) 354 361 } 355 - await this.ctx.logger.printError(error) 362 + const project = this.ctx.getProjectByTaskId(tasks[0].id) 363 + await this.ctx.logger.printError(error, { project }) 356 364 errorDivider() 357 365 await Promise.resolve() 358 366 } ··· 361 369 registerUnhandledRejection() { 362 370 process.on('unhandledRejection', async (err) => { 363 371 process.exitCode = 1 364 - await this.ctx.logger.printError(err, true, 'Unhandled Rejection') 372 + await this.ctx.logger.printError(err, { fullStack: true, type: 'Unhandled Rejection' }) 365 373 this.ctx.logger.error('\n\n') 366 374 process.exit(1) 367 375 })
+4 -1
packages/vitest/src/node/reporters/json.ts
··· 183 183 if (!error) 184 184 return 185 185 186 - const stack = parseErrorStacktrace(error) 186 + const project = this.ctx.getProjectByTaskId(test.id) 187 + const stack = parseErrorStacktrace(error, { 188 + getSourceMap: file => project.getBrowserSourceMapModuleById(file), 189 + }) 187 190 const frame = stack[0] 188 191 if (!frame) 189 192 return
+6 -3
packages/vitest/src/node/reporters/junit.ts
··· 125 125 await this.logger.log(`</${name}>`) 126 126 } 127 127 128 - async writeErrorDetails(error: ErrorWithDiff): Promise<void> { 128 + async writeErrorDetails(task: Task, error: ErrorWithDiff): Promise<void> { 129 129 const errorName = error.name ?? error.nameStr ?? 'Unknown Error' 130 130 const errorDetails = `${errorName}: ${error.message}` 131 131 132 132 // Be sure to escape any XML in the error Details 133 133 await this.baseLog(escapeXML(errorDetails)) 134 134 135 - const stack = parseErrorStacktrace(error) 135 + const project = this.ctx.getProjectByTaskId(task.id) 136 + const stack = parseErrorStacktrace(error, { 137 + getSourceMap: file => project.getBrowserSourceMapModuleById(file), 138 + }) 136 139 137 140 // TODO: This is same as printStack but without colors. Find a way to reuse code. 138 141 for (const frame of stack) { ··· 185 188 if (!error) 186 189 return 187 190 188 - await this.writeErrorDetails(error) 191 + await this.writeErrorDetails(task, error) 189 192 }) 190 193 } 191 194 }
+5 -1
packages/vitest/src/node/reporters/tap.ts
··· 68 68 else { 69 69 this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment}`) 70 70 71 + const project = this.ctx.getProjectByTaskId(task.id) 72 + 71 73 if (task.result?.state === 'fail' && task.result.errors) { 72 74 this.logger.indent() 73 75 74 76 task.result.errors.forEach((error) => { 75 - const stacks = parseErrorStacktrace(error) 77 + const stacks = parseErrorStacktrace(error, { 78 + getSourceMap: file => project.getBrowserSourceMapModuleById(file), 79 + }) 76 80 const stack = stacks[0] 77 81 78 82 this.logger.log('---')