Skip to content

Commit b03f209

Browse files
authored
feat: annotation API (#7953)
1 parent 8fad735 commit b03f209

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1690
-123
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ test/cli/fixtures/browser-multiple/basic-*
3232
# exclude static html reporter folder
3333
test/browser/html/
3434
test/core/html/
35+
.vitest-attachments

docs/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,10 @@ function guide(): DefaultTheme.SidebarItem[] {
516516
text: 'Test Context',
517517
link: '/guide/test-context',
518518
},
519+
{
520+
text: 'Test Annotations',
521+
link: '/guide/test-annotations',
522+
},
519523
{
520524
text: 'Environment',
521525
link: '/guide/environment',

docs/advanced/api/reporters.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Vitest has its own test run lifecycle. These are represented by reporter's metho
1515
- [`onHookStart(beforeAll)`](#onhookstart)
1616
- [`onHookEnd(beforeAll)`](#onhookend)
1717
- [`onTestCaseReady`](#ontestcaseready)
18+
- [`onTestAnnotate`](#ontestannotate) <Version>3.2.0</Version>
1819
- [`onHookStart(beforeEach)`](#onhookstart)
1920
- [`onHookEnd(beforeEach)`](#onhookend)
2021
- [`onHookStart(afterEach)`](#onhookstart)
@@ -317,3 +318,16 @@ function onTestCaseResult(testCase: TestCase): Awaitable<void>
317318
This method is called when the test has finished running or was just skipped. Note that this will be called after the `afterEach` hook is finished, if there are any.
318319

319320
At this point, [`testCase.result()`](/advanced/api/test-case#result) will have non-pending state.
321+
322+
## onTestAnnotate <Version>3.2.0</Version> {#ontestannotate}
323+
324+
```ts
325+
function onTestAnnotate(
326+
testCase: TestCase,
327+
annotation: TestAnnotation,
328+
): Awaitable<void>
329+
```
330+
331+
The `onTestAnnotate` hook is associated with the [`context.annotate`](/guide/test-context#annotate) method. When `annotate` is invoked, Vitest serialises it and sends the same attachment to the main thread where reporter can interact with it.
332+
333+
If the path is specified, Vitest stores it in a separate directory (configured by [`attachmentsDir`](/config/#attachmentsdir)) and modifies the `path` property to reference it.

docs/config/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2645,3 +2645,10 @@ Polling timeout in milliseconds
26452645
- **Default:** `false`
26462646

26472647
Always print console traces when calling any `console` method. This is useful for debugging.
2648+
2649+
### attachmentsDir <Version>3.2.0</Version>
2650+
2651+
- **Type:** `string`
2652+
- **Default:** `'.vitest-attachments'`
2653+
2654+
Directory path for storing attachments created by [`context.annotate`](/guide/test-context#annotate) relative to the project root.

docs/guide/test-annotations.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
title: Test Annotations | Guide
3+
outline: deep
4+
---
5+
6+
# Test Annotations
7+
8+
Vitest supports annotating your tests with custom messages and files via the [`context.annotate`](/guide/test-context#annotate) API. These annotations will be attached to the test case and passed down to reporters in the [`onTestAnnotate`](/advanced/api/reporters#ontestannotate) hook.
9+
10+
```ts
11+
test('hello world', async ({ annotate }) => {
12+
await annotate('this is my test')
13+
14+
if (condition) {
15+
await annotate('this should\'ve errored', 'error')
16+
}
17+
18+
const file = createTestSpecificFile()
19+
await annotate('creates a file', { body: file })
20+
})
21+
```
22+
23+
Depending on your reporter, you will see these annotations differently.
24+
25+
## Built-in Reporters
26+
### default
27+
28+
The `default` reporter prints annotations only if the test has failed:
29+
30+
```
31+
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
32+
33+
FAIL example.test.js > an example of a test with annotation
34+
Error: thrown error
35+
❯ example.test.js:11:21
36+
9 | await annotate('annotation 1')
37+
10| await annotate('annotation 2', 'warning')
38+
11| throw new Error('thrown error')
39+
| ^
40+
12| })
41+
42+
❯ example.test.js:9:15 notice
43+
↳ annotation 1
44+
❯ example.test.js:10:15 warning
45+
↳ annotation 2
46+
47+
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
48+
```
49+
50+
### verbose
51+
52+
In a TTY terminal, the `verbose` reporter works similarly to the `default` reporter. However, in a non-TTY environment, the `verbose` reporter will also print annotations after every test.
53+
54+
```
55+
✓ example.test.js > an example of a test with annotation
56+
57+
❯ example.test.js:9:15 notice
58+
↳ annotation 1
59+
❯ example.test.js:10:15 warning
60+
↳ annotation 2
61+
62+
```
63+
64+
### html
65+
66+
The HTML reporter shows annotations the same way the UI does. You can see the annotation on the line where it was called. At the moment, if the annotation wasn't called in a test file, you cannot see it in the UI. We are planning to support a separate test summary view where it will be visible.
67+
68+
<img alt="Vitest UI" img-light src="/annotations-html-light.png">
69+
<img alt="Vitest UI" img-dark src="/annotations-html-dark.png">
70+
71+
### junit
72+
73+
The `junit` reporter lists annotations inside the testcase's `properties` tag. The JUnit reporter will ignore all attachments and will print only the type and the message.
74+
75+
```xml
76+
<testcase classname="basic/example.test.js" name="an example of a test with annotation" time="0.14315">
77+
<properties>
78+
<property name="notice" value="the message of the annotation">
79+
</property>
80+
</properties>
81+
</testcase>
82+
```
83+
84+
### github-actions
85+
86+
The `github-actions` reporter will print the annotation as a [notice message](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-notice-message) by default. You can configure the `type` by passing down the second argument as `notice`, `warning` or `error`. If type is none of these, Vitest will show the message as a notice.
87+
88+
<img alt="GitHub Actions" img-light src="/annotations-gha-light.png">
89+
<img alt="GitHub Actions" img-dark src="/annotations-gha-dark.png">
90+
91+
### tap
92+
93+
The `tap` and `tap-flat` reporters print annotations as diagnostic messages on a new line starting with a `#` symbol. They will ignore all attachments and will print only the type and message:
94+
95+
```
96+
ok 1 - an example of a test with annotation # time=143.15ms
97+
# notice: the message of the annotation
98+
```

docs/guide/test-context.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,30 @@ it('math is hard', ({ skip, mind }) => {
7979
})
8080
```
8181

82-
#### `context.signal` <Version>3.2.0</Version> {#context-signal}
82+
#### `annotate` <Version>3.2.0</Version> {#annotate}
83+
84+
```ts
85+
function annotate(
86+
message: string,
87+
attachment?: TestAttachment,
88+
): Promise<TestAnnotation>
89+
90+
function annotate(
91+
message: string,
92+
type?: string,
93+
attachment?: TestAttachment,
94+
): Promise<TestAnnotation>
95+
```
96+
97+
Add a [test annotation](/guide/test-annotations) that will be displayed by your [reporter](/config/#reporter).
98+
99+
```ts
100+
test('annotations API', async ({ annotate }) => {
101+
await annotate('https://github.com/vitest-dev/vitest/pull/7953', 'issues')
102+
})
103+
```
104+
105+
#### `signal` <Version>3.2.0</Version> {#signal}
83106

84107
An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that can be aborted by Vitest. The signal is aborted in these situations:
85108

docs/public/annotations-gha-dark.png

34.4 KB
Loading

docs/public/annotations-gha-light.png

34.1 KB
Loading

docs/public/annotations-html-dark.png

389 KB
Loading
391 KB
Loading

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

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { CancelReason, File, Suite, Task, TaskEventPack, TaskResultPack, VitestRunner } from '@vitest/runner'
2-
import type { SerializedConfig, TestExecutionMethod, WorkerGlobalState } from 'vitest'
1+
import type { CancelReason, File, Suite, Task, TaskEventPack, TaskResultPack, TestAnnotation, VitestRunner } from '@vitest/runner'
2+
import type { RunnerTestCase, SerializedConfig, TestExecutionMethod, WorkerGlobalState } from 'vitest'
33
import type { VitestExecutor } from 'vitest/execute'
44
import type { VitestBrowserClientMocker } from './mocker'
55
import { globalChannel, onCancel } from '@vitest/browser/client'
@@ -146,6 +146,40 @@ export function createBrowserRunner(
146146
return rpc().onCollected(this.method, files)
147147
}
148148

149+
onTestAnnotate = (test: RunnerTestCase, annotation: TestAnnotation): Promise<TestAnnotation> => {
150+
if (annotation.location) {
151+
// the file should be the test file
152+
// tests from other files are not supported
153+
const map = this.sourceMapCache.get(annotation.location.file)
154+
if (!map) {
155+
return rpc().onTaskAnnotate(test.id, annotation)
156+
}
157+
158+
const traceMap = new TraceMap(map as any)
159+
const { line, column, source } = originalPositionFor(traceMap, annotation.location)
160+
if (line != null && column != null && source != null) {
161+
let file: string = annotation.location.file
162+
if (source) {
163+
const fileUrl = annotation.location.file.startsWith('file://')
164+
? annotation.location.file
165+
: `file://${annotation.location.file}`
166+
const sourceRootUrl = map.sourceRoot
167+
? new URL(map.sourceRoot, fileUrl)
168+
: fileUrl
169+
file = new URL(source, sourceRootUrl).pathname
170+
}
171+
172+
annotation.location = {
173+
line,
174+
column: column + 1,
175+
// if the file path is on windows, we need to remove the starting slash
176+
file: file.match(/\/\w:\//) ? file.slice(1) : file,
177+
}
178+
}
179+
}
180+
return rpc().onTaskAnnotate(test.id, annotation)
181+
}
182+
149183
onTaskUpdate = (task: TaskResultPack[], events: TaskEventPack[]): Promise<void> => {
150184
return rpc().onTaskUpdate(this.method, task, events)
151185
}
@@ -220,14 +254,24 @@ export async function initiateRunner(
220254
return runner
221255
}
222256

257+
async function getTraceMap(file: string, sourceMaps: Map<string, any>) {
258+
const result = sourceMaps.get(file) || await rpc().getBrowserFileSourceMap(file).then((map) => {
259+
sourceMaps.set(file, map)
260+
return map
261+
})
262+
if (!result) {
263+
return null
264+
}
265+
return new TraceMap(result as any)
266+
}
267+
223268
async function updateTestFilesLocations(files: File[], sourceMaps: Map<string, any>) {
224269
const promises = files.map(async (file) => {
225-
const result = sourceMaps.get(file.filepath) || await rpc().getBrowserFileSourceMap(file.filepath)
226-
if (!result) {
270+
const traceMap = await getTraceMap(file.filepath, sourceMaps)
271+
if (!traceMap) {
227272
return null
228273
}
229-
const traceMap = new TraceMap(result as any)
230-
function updateLocation(task: Task) {
274+
const updateLocation = (task: Task) => {
231275
if (task.location) {
232276
const { line, column } = originalPositionFor(traceMap, task.location)
233277
if (line != null && column != null) {

packages/browser/src/node/rpc.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
149149
await vitest._testRun.collected(project, files)
150150
}
151151
},
152+
async onTaskAnnotate(id, annotation) {
153+
return vitest._testRun.annotate(id, annotation)
154+
},
152155
async onTaskUpdate(method, packs, events) {
153156
if (method === 'collect') {
154157
vitest.state.updateTasks(packs)

packages/browser/src/node/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { MockedModuleSerialized } from '@vitest/mocker'
22
import type { ServerIdResolution, ServerMockResolution } from '@vitest/mocker/node'
3-
import type { TaskEventPack, TaskResultPack } from '@vitest/runner'
3+
import type { TaskEventPack, TaskResultPack, TestAnnotation } from '@vitest/runner'
44
import type { BirpcReturn } from 'birpc'
55
import type {
66
AfterSuiteRunMeta,
@@ -19,6 +19,7 @@ export interface WebSocketBrowserHandlers {
1919
onUnhandledError: (error: unknown, type: string) => Promise<void>
2020
onQueued: (method: TestExecutionMethod, file: RunnerTestFile) => void
2121
onCollected: (method: TestExecutionMethod, files: RunnerTestFile[]) => Promise<void>
22+
onTaskAnnotate: (testId: string, annotation: TestAnnotation) => Promise<TestAnnotation>
2223
onTaskUpdate: (method: TestExecutionMethod, packs: TaskResultPack[], events: TaskEventPack[]) => void
2324
onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void
2425
cancelCurrentRun: (reason: CancelReason) => void

0 commit comments

Comments
 (0)