Skip to content

Commit e39adea

Browse files
feat: pass down meta information to Node.js process (#3449)
Co-authored-by: Anjorin Damilare <[email protected]>
1 parent 39432e8 commit e39adea

File tree

24 files changed

+417
-159
lines changed

24 files changed

+417
-159
lines changed

docs/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ export default withPwa(defineConfig({
128128
text: 'Runner API',
129129
link: '/advanced/runner',
130130
},
131+
{
132+
text: 'Task Metadata',
133+
link: '/advanced/metadata',
134+
},
131135
],
132136
},
133137
],

docs/advanced/metadata.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Task Metadata
2+
3+
::: warning
4+
Vitest exposes experimental private API. Breaking changes might not follow semver, please pin Vitest's version when using it.
5+
:::
6+
7+
If you are developing a custom reporter or using Vitest Node.js API, you might find it useful to pass data from tests that are being executed in various contexts to your reporter or custom Vitest handler.
8+
9+
To accomplish this, relying on the [test context](/guide/test-context) is not feasible since it cannot be serialized. However, with Vitest, you can utilize the `meta` property available on every task (suite or test) to share data between your tests and the Node.js process. It's important to note that this communication is one-way only, as the `meta` property can only be modified from within the test context. Any changes made within the Node.js context will not be visible in your tests.
10+
11+
You can populate `meta` property on test context or inside `beforeAll`/`afterAll` hooks for suite tasks.
12+
13+
```ts
14+
afterAll((suite) => {
15+
suite.meta.done = true
16+
})
17+
18+
test('custom', ({ task }) => {
19+
task.meta.custom = 'some-custom-handler'
20+
})
21+
```
22+
23+
Once a test is completed, Vitest will send a task including the result and `meta` to the Node.js process using RPC. To intercept and process this task, you can utilize the `onTaskUpdate` method available in your reporter implementation:
24+
25+
```ts
26+
// custom-reporter.js
27+
export default {
28+
// you can intercept packs if needed
29+
onTaskUpdate(packs) {
30+
const [id, result, meta] = packs[0]
31+
},
32+
// meta is located on every task inside "onFinished"
33+
onFinished(files) {
34+
files[0].meta.done === true
35+
files[0].tasks[0].meta.custom === 'some-custom-handler'
36+
}
37+
}
38+
```
39+
40+
::: warning
41+
Vitest can send several tasks at the same time if several tests are completed in a short period of time.
42+
:::
43+
44+
::: danger BEWARE
45+
Vitest uses different methods to communicate with the Node.js process.
46+
47+
- If Vitest runs tests inside worker threads, it will send data via [message port](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort)
48+
- If Vitest uses child process, the data will be send as a serialized Buffer via [`process.send`](https://nodejs.org/api/process.html#processsendmessage-sendhandle-options-callback) API
49+
- If Vitest run tests in the browser, the data will be stringified using [flatted](https://www.npmjs.com/package/flatted) package
50+
51+
The general rule of thumb is that you can send almost anything, except for functions, Promises, regexp (`v8.stringify` cannot serialize it, but you can send a string version and parse it in the Node.js process yourself), and other non-serializable data, but you can have cyclic references inside.
52+
53+
Also, make sure you serialize [Error properties](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#error_types) before you set them.
54+
:::
55+
56+
You can also get this information from Vitest state when tests finished running:
57+
58+
```ts
59+
const vitest = await createVitest('test')
60+
await vitest.start()
61+
vitest.state.getFiles()[0].meta.done === true
62+
vitest.state.getFiles()[0].tasks[0].meta.custom === 'some-custom-handler'
63+
```
64+
65+
It's also possible to extend type definitions when using TypeScript:
66+
67+
```ts
68+
declare module 'vitest' {
69+
interface TaskMeta {
70+
done?: boolean
71+
custom?: string
72+
}
73+
}
74+
```

docs/guide/test-context.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import { it } from 'vitest'
1515

1616
it('should work', (ctx) => {
1717
// prints name of the test
18-
console.log(ctx.meta.name)
18+
console.log(ctx.task.name)
1919
})
2020
```
2121

2222
## Built-in Test Context
2323

24-
#### `context.meta`
24+
#### `context.task`
2525

2626
A readonly object containing metadata about the test.
2727

packages/browser/src/client/runner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { File, TaskResult, Test } from '@vitest/runner'
1+
import type { File, TaskResultPack, Test } from '@vitest/runner'
22
import { rpc } from './rpc'
33
import type { ResolvedConfig } from '#types'
44

@@ -49,7 +49,7 @@ export function createBrowserRunner(original: any, coverageModule: CoverageHandl
4949
return rpc().onCollected(files)
5050
}
5151

52-
onTaskUpdate(task: [string, TaskResult | undefined][]): Promise<void> {
52+
onTaskUpdate(task: TaskResultPack[]): Promise<void> {
5353
return rpc().onTaskUpdate(task)
5454
}
5555

packages/runner/src/collect.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export async function collectTests(paths: string[], runner: VitestRunner): Promi
2424
mode: 'run',
2525
filepath,
2626
tasks: [],
27+
meta: Object.create(null),
2728
projectName: config.name,
2829
}
2930

packages/runner/src/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export function createTestContext(test: Test, runner: VitestRunner): TestContext
4747
} as unknown as TestContext
4848

4949
context.meta = test
50+
context.task = test
5051

5152
context.onTestFailed = (fn) => {
5253
test.onFailed ||= []

packages/runner/src/run.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import limit from 'p-limit'
22
import { getSafeTimers, shuffle } from '@vitest/utils'
33
import type { VitestRunner } from './types/runner'
4-
import type { File, HookCleanupCallback, HookListener, SequenceHooks, Suite, SuiteHooks, Task, TaskResult, TaskState, Test } from './types'
4+
import type { File, HookCleanupCallback, HookListener, SequenceHooks, Suite, SuiteHooks, Task, TaskMeta, TaskResult, TaskResultPack, TaskState, Test } from './types'
55
import { partitionSuiteChildren } from './utils/suite'
66
import { getFn, getHooks } from './map'
77
import { collectTests } from './collect'
@@ -70,12 +70,12 @@ export async function callSuiteHook<T extends keyof SuiteHooks>(
7070
return callbacks
7171
}
7272

73-
const packs = new Map<string, TaskResult | undefined>()
73+
const packs = new Map<string, [TaskResult | undefined, TaskMeta]>()
7474
let updateTimer: any
7575
let previousUpdate: Promise<void> | undefined
7676

7777
export function updateTask(task: Task, runner: VitestRunner) {
78-
packs.set(task.id, task.result)
78+
packs.set(task.id, [task.result, task.meta])
7979

8080
const { clearTimeout, setTimeout } = getSafeTimers()
8181

@@ -91,7 +91,14 @@ async function sendTasksUpdate(runner: VitestRunner) {
9191
await previousUpdate
9292

9393
if (packs.size) {
94-
const p = runner.onTaskUpdate?.(Array.from(packs))
94+
const taskPacks = Array.from(packs).map<TaskResultPack>(([id, task]) => {
95+
return [
96+
id,
97+
task[0],
98+
task[1],
99+
]
100+
})
101+
const p = runner.onTaskUpdate?.(taskPacks)
95102
packs.clear()
96103
return p
97104
}

packages/runner/src/suite.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
8787
fails: this.fails,
8888
retry: options?.retry,
8989
repeats: options?.repeats,
90+
meta: Object.create(null),
9091
} as Omit<Test, 'context'> as Test
9192

9293
if (this.concurrent || concurrent)
@@ -116,6 +117,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
116117
name,
117118
type: 'custom',
118119
mode: self.only ? 'only' : self.skip ? 'skip' : self.todo ? 'todo' : 'run',
120+
meta: Object.create(null),
119121
}
120122
tasks.push(task)
121123
return task
@@ -150,6 +152,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
150152
each,
151153
shuffle,
152154
tasks: [],
155+
meta: Object.create(null),
153156
}
154157

155158
setHooks(suite, createSuiteHooks())

packages/runner/src/types/runner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { File, SequenceHooks, SequenceSetupFiles, Suite, TaskResult, Test, TestContext } from './tasks'
1+
import type { File, SequenceHooks, SequenceSetupFiles, Suite, TaskResultPack, Test, TestContext } from './tasks'
22

33
export interface VitestRunnerConfig {
44
root: string
@@ -86,7 +86,7 @@ export interface VitestRunner {
8686
/**
8787
* Called, when a task is updated. The same as "onTaskUpdate" in a reporter, but this is running in the same thread as tests.
8888
*/
89-
onTaskUpdate?(task: [string, TaskResult | undefined][]): Promise<void>
89+
onTaskUpdate?(task: TaskResultPack[]): Promise<void>
9090

9191
/**
9292
* Called before running all tests in collected paths.

packages/runner/src/types/tasks.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@ export interface TaskBase {
99
id: string
1010
name: string
1111
mode: RunMode
12+
meta: TaskMeta
1213
each?: boolean
1314
concurrent?: boolean
1415
shuffle?: boolean
1516
suite?: Suite
1617
file?: File
1718
result?: TaskResult
1819
retry?: number
19-
meta?: any
2020
repeats?: number
2121
}
2222

23+
export interface TaskMeta {}
24+
2325
export interface TaskCustom extends TaskBase {
2426
type: 'custom'
2527
}
@@ -40,7 +42,7 @@ export interface TaskResult {
4042
repeatCount?: number
4143
}
4244

43-
export type TaskResultPack = [id: string, result: TaskResult | undefined]
45+
export type TaskResultPack = [id: string, result: TaskResult | undefined, meta: TaskMeta]
4446

4547
export interface Suite extends TaskBase {
4648
type: 'suite'
@@ -205,10 +207,10 @@ export type HookListener<T extends any[], Return = void> = (...args: T) => Await
205207
export type HookCleanupCallback = (() => Awaitable<unknown>) | void
206208

207209
export interface SuiteHooks<ExtraContext = {}> {
208-
beforeAll: HookListener<[Suite | File], HookCleanupCallback>[]
209-
afterAll: HookListener<[Suite | File]>[]
210-
beforeEach: HookListener<[TestContext & ExtraContext, Suite], HookCleanupCallback>[]
211-
afterEach: HookListener<[TestContext & ExtraContext, Suite]>[]
210+
beforeAll: HookListener<[Readonly<Suite | File>], HookCleanupCallback>[]
211+
afterAll: HookListener<[Readonly<Suite | File>]>[]
212+
beforeEach: HookListener<[TestContext & ExtraContext, Readonly<Suite>], HookCleanupCallback>[]
213+
afterEach: HookListener<[TestContext & ExtraContext, Readonly<Suite>]>[]
212214
}
213215

214216
export interface SuiteCollector<ExtraContext = {}> {
@@ -234,9 +236,16 @@ export interface RuntimeContext {
234236
export interface TestContext {
235237
/**
236238
* Metadata of the current test
239+
*
240+
* @deprecated Use `task` instead
237241
*/
238242
meta: Readonly<Test>
239243

244+
/**
245+
* Metadata of the current test
246+
*/
247+
task: Readonly<Test>
248+
240249
/**
241250
* Extract hooks on test failed
242251
*/

0 commit comments

Comments
 (0)