Skip to content

Commit d627e20

Browse files
authoredMar 14, 2024··
feat: add a flag to include test location in tasks (#5342)
1 parent 0bea224 commit d627e20

File tree

16 files changed

+145
-12
lines changed

16 files changed

+145
-12
lines changed
 

‎docs/config/index.md

+13
Original file line numberDiff line numberDiff line change
@@ -2108,3 +2108,16 @@ Disabling this option might [improve performance](/guide/improving-performance)
21082108
::: tip
21092109
You can disable isolation for specific pools by using [`poolOptions`](#pooloptions) property.
21102110
:::
2111+
2112+
### includeTaskLocation <Badge type="info">1.4.0+</Badge> {#includeTaskLocation}
2113+
2114+
- **Type:** `boolean`
2115+
- **Default:** `false`
2116+
2117+
Should `location` property be included when Vitest API receives tasks in [reporters](#reporters). If you have a lot of tests, this might cause a small performance regression.
2118+
2119+
The `location` property has `column` and `line` values that correspond to the `test` or `describe` position in the original file.
2120+
2121+
::: tip
2122+
This option has no effect if you do not use custom code that relies on this.
2123+
:::

‎packages/browser/src/client/runner.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,13 @@ export function createBrowserRunner(
5555
}
5656
}
5757

58-
onCollected = (files: File[]): unknown => {
58+
onCollected = async (files: File[]): Promise<unknown> => {
59+
if (this.config.includeTaskLocation) {
60+
try {
61+
await updateFilesLocations(files)
62+
}
63+
catch (_) {}
64+
}
5965
return rpc().onCollected(files)
6066
}
6167

@@ -107,3 +113,28 @@ export async function initiateRunner() {
107113
cachedRunner = runner
108114
return runner
109115
}
116+
117+
async function updateFilesLocations(files: File[]) {
118+
const { loadSourceMapUtils } = await importId('vitest/utils') as typeof import('vitest/utils')
119+
const { TraceMap, originalPositionFor } = await loadSourceMapUtils()
120+
121+
const promises = files.map(async (file) => {
122+
const result = await rpc().getBrowserFileSourceMap(file.filepath)
123+
if (!result)
124+
return null
125+
const traceMap = new TraceMap(result as any)
126+
function updateLocation(task: Task) {
127+
if (task.location) {
128+
const { line, column } = originalPositionFor(traceMap, task.location)
129+
if (line != null && column != null)
130+
task.location = { line, column: column + 1 }
131+
}
132+
if ('tasks' in task)
133+
task.tasks.forEach(updateLocation)
134+
}
135+
file.tasks.forEach(updateLocation)
136+
return null
137+
})
138+
139+
await Promise.all(promises)
140+
}

‎packages/runner/src/collect.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export async function collectTests(paths: string[], runner: VitestRunner): Promi
2828
projectName: config.name,
2929
}
3030

31-
clearCollectorContext(runner)
31+
clearCollectorContext(filepath, runner)
3232

3333
try {
3434
const setupStart = now()

‎packages/runner/src/run.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export async function startTests(paths: string[], runner: VitestRunner) {
393393

394394
const files = await collectTests(paths, runner)
395395

396-
runner.onCollected?.(files)
396+
await runner.onCollected?.(files)
397397
await runner.onBeforeRunFiles?.(files)
398398

399399
await runFiles(files, runner)

‎packages/runner/src/suite.ts

+51-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { format, isObject, objDisplay, objectAttr } from '@vitest/utils'
2+
import { parseSingleStack } from '@vitest/utils/source-map'
23
import type { Custom, CustomAPI, File, Fixtures, RunMode, Suite, SuiteAPI, SuiteCollector, SuiteFactory, SuiteHooks, Task, TaskCustomOptions, Test, TestAPI, TestFunction, TestOptions } from './types'
34
import type { VitestRunner } from './types/runner'
45
import { createChainable } from './utils/chain'
@@ -25,19 +26,25 @@ export const it = test
2526

2627
let runner: VitestRunner
2728
let defaultSuite: SuiteCollector
29+
let currentTestFilepath: string
2830

2931
export function getDefaultSuite() {
3032
return defaultSuite
3133
}
3234

35+
export function getTestFilepath() {
36+
return currentTestFilepath
37+
}
38+
3339
export function getRunner() {
3440
return runner
3541
}
3642

37-
export function clearCollectorContext(currentRunner: VitestRunner) {
43+
export function clearCollectorContext(filepath: string, currentRunner: VitestRunner) {
3844
if (!defaultSuite)
3945
defaultSuite = currentRunner.config.sequence.shuffle ? suite.shuffle('') : currentRunner.config.sequence.concurrent ? suite.concurrent('') : suite('')
4046
runner = currentRunner
47+
currentTestFilepath = filepath
4148
collectorContext.tasks.length = 0
4249
defaultSuite.clear()
4350
collectorContext.currentSuite = defaultSuite
@@ -103,7 +110,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
103110

104111
let suite: Suite
105112

106-
initSuite()
113+
initSuite(true)
107114

108115
const task = function (name = '', options: TaskCustomOptions = {}) {
109116
const task: Custom = {
@@ -140,6 +147,17 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
140147
))
141148
}
142149

150+
if (runner.config.includeTaskLocation) {
151+
const limit = Error.stackTraceLimit
152+
// custom can be called from any place, let's assume the limit is 10 stacks
153+
Error.stackTraceLimit = 10
154+
const error = new Error('stacktrace').stack!
155+
Error.stackTraceLimit = limit
156+
const stack = findStackTrace(error)
157+
if (stack)
158+
task.location = stack
159+
}
160+
143161
tasks.push(task)
144162
return task
145163
}
@@ -183,7 +201,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
183201
getHooks(suite)[name].push(...fn as any)
184202
}
185203

186-
function initSuite() {
204+
function initSuite(includeLocation: boolean) {
187205
if (typeof suiteOptions === 'number')
188206
suiteOptions = { timeout: suiteOptions }
189207

@@ -199,13 +217,27 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
199217
projectName: '',
200218
}
201219

220+
if (runner && includeLocation && runner.config.includeTaskLocation) {
221+
const limit = Error.stackTraceLimit
222+
Error.stackTraceLimit = 5
223+
const error = new Error('stacktrace').stack!
224+
Error.stackTraceLimit = limit
225+
const stack = parseSingleStack(error.split('\n')[5])
226+
if (stack) {
227+
suite.location = {
228+
line: stack.line,
229+
column: stack.column,
230+
}
231+
}
232+
}
233+
202234
setHooks(suite, createSuiteHooks())
203235
}
204236

205237
function clear() {
206238
tasks.length = 0
207239
factoryQueue.length = 0
208-
initSuite()
240+
initSuite(false)
209241
}
210242

211243
async function collect(file?: File) {
@@ -397,3 +429,18 @@ function formatTemplateString(cases: any[], args: any[]): any[] {
397429
}
398430
return res
399431
}
432+
433+
function findStackTrace(error: string) {
434+
// first line is the error message
435+
// and the first 3 stacks are always from the collector
436+
const lines = error.split('\n').slice(4)
437+
for (const line of lines) {
438+
const stack = parseSingleStack(line)
439+
if (stack && stack.file === getTestFilepath()) {
440+
return {
441+
line: stack.line,
442+
column: stack.column,
443+
}
444+
}
445+
}
446+
}

‎packages/runner/src/types/runner.ts

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface VitestRunnerConfig {
3333
testTimeout: number
3434
hookTimeout: number
3535
retry: number
36+
includeTaskLocation?: boolean
3637
diffOptions?: DiffOptions
3738
}
3839

‎packages/runner/src/types/tasks.ts

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export interface TaskBase {
1818
result?: TaskResult
1919
retry?: number
2020
repeats?: number
21+
location?: {
22+
line: number
23+
column: number
24+
}
2125
}
2226

2327
export interface TaskPopulated extends TaskBase {

‎packages/vitest/src/api/setup.ts

+6
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, _server?: Vi
113113
getConfig() {
114114
return vitestOrWorkspace.config
115115
},
116+
async getBrowserFileSourceMap(id) {
117+
if (!('ctx' in vitestOrWorkspace))
118+
return undefined
119+
const mod = vitestOrWorkspace.browser?.moduleGraph.getModuleById(id)
120+
return mod?.transformResult?.map
121+
},
116122
async getTransformResult(id) {
117123
const result: TransformResultWithSource | null | undefined = await ctx.vitenode.transformRequest(id)
118124
if (result) {

‎packages/vitest/src/api/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface WebSocketHandlers {
2020
resolveSnapshotPath: (testPath: string) => string
2121
resolveSnapshotRawPath: (testPath: string, rawPath: string) => string
2222
getModuleGraph: (id: string) => Promise<ModuleGraphData>
23+
getBrowserFileSourceMap: (id: string) => Promise<TransformResult['map'] | undefined>
2324
getTransformResult: (id: string) => Promise<TransformResultWithSource | undefined>
2425
readSnapshotFile: (id: string) => Promise<string | null>
2526
readTestFile: (id: string) => Promise<string | null>

‎packages/vitest/src/node/cli/cli-config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -615,4 +615,5 @@ export const cliOptionsConfig: VitestCLIOptions = {
615615
poolMatchGlobs: null,
616616
deps: null,
617617
name: null,
618+
includeTaskLocation: null,
618619
}

‎packages/vitest/src/node/workspace.ts

+1
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ export class WorkspaceProject {
382382
inspect: this.ctx.config.inspect,
383383
inspectBrk: this.ctx.config.inspectBrk,
384384
alias: [],
385+
includeTaskLocation: this.config.includeTaskLocation ?? this.ctx.config.includeTaskLocation,
385386
}, this.ctx.configOverride || {} as any) as ResolvedConfig
386387
}
387388

‎packages/vitest/src/public/utils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
export * from '@vitest/utils'
2+
3+
export function loadSourceMapUtils() {
4+
return import('@vitest/utils/source-map')
5+
}

‎packages/vitest/src/types/config.ts

+7
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,13 @@ export interface InlineConfig {
715715
* @default false
716716
*/
717717
disableConsoleIntercept?: boolean
718+
719+
/**
720+
* Include "location" property inside the test definition
721+
*
722+
* @default false
723+
*/
724+
includeTaskLocation?: boolean
718725
}
719726

720727
export interface TypecheckConfig {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {}

‎test/public-api/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"type": "module",
44
"private": true,
55
"scripts": {
6-
"test": "vitest"
6+
"test": "vitest",
7+
"fixtures": "vitest --root ./fixtures"
78
},
89
"devDependencies": {
910
"@vitest/browser": "workspace:*",

‎test/public-api/tests/runner.spec.ts

+19-4
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ it.each([
1515
headless: true,
1616
},
1717
},
18-
] as UserConfig[])('passes down metadata when $name', async (config) => {
18+
] as UserConfig[])('passes down metadata when $name', { timeout: 60_000, retry: 3 }, async (config) => {
1919
const taskUpdate: TaskResultPack[] = []
2020
const finishedFiles: File[] = []
21+
const collectedFiles: File[] = []
2122
const { vitest, stdout, stderr } = await runVitest({
2223
root: resolve(__dirname, '..', 'fixtures'),
2324
include: ['**/*.spec.ts'],
@@ -30,8 +31,12 @@ it.each([
3031
onFinished(files) {
3132
finishedFiles.push(...files || [])
3233
},
34+
onCollected(files) {
35+
collectedFiles.push(...files || [])
36+
},
3337
},
3438
],
39+
includeTaskLocation: true,
3540
...config,
3641
})
3742

@@ -69,7 +74,17 @@ it.each([
6974

7075
expect(files[0].meta).toEqual(suiteMeta)
7176
expect(files[0].tasks[0].meta).toEqual(testMeta)
72-
}, {
73-
timeout: 60_000,
74-
retry: 3,
77+
78+
expect(finishedFiles[0].tasks[0].location).toEqual({
79+
line: 14,
80+
column: 1,
81+
})
82+
expect(collectedFiles[0].tasks[0].location).toEqual({
83+
line: 14,
84+
column: 1,
85+
})
86+
expect(files[0].tasks[0].location).toEqual({
87+
line: 14,
88+
column: 1,
89+
})
7590
})

0 commit comments

Comments
 (0)
Please sign in to comment.