Skip to content

Commit d627e20

Browse files
authored
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

Lines changed: 13 additions & 0 deletions
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

Lines changed: 32 additions & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 51 additions & 4 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy