Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Advanced: typeof import('./components/Advanced.vue')['default']
ArrowDown: typeof import('./components/ArrowDown.vue')['default']
BlogIndex: typeof import('./components/BlogIndex.vue')['default']
Box: typeof import('./components/Box.vue')['default']
Expand Down
5 changes: 5 additions & 0 deletions docs/.vitepress/components/Advanced.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<Badge type="danger" title="This is an advanced API intended for library authors and framework integrations. Most users should not need this." class="cursor-help">
advanced
</Badge>
</template>
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,10 @@ export default ({ mode }: { mode: string }) => {
text: 'TaskMeta',
link: '/api/advanced/metadata',
},
{
text: 'TestArtifact',
link: '/api/advanced/artifacts',
},
],
},
// {
Expand Down
176 changes: 176 additions & 0 deletions docs/api/advanced/artifacts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
outline: deep
title: Test Artifacts
---

# Test Artifacts <Advanced /> <Version type="experimental">4.0.11</Version> <Experimental />

::: warning
This is an advanced API. As a user, you most likely want to use [test annotations](/guide/test-annotations) to add notes or context to your tests instead. This is primarily used internally and by library authors.
:::

Test artifacts allow attaching or recording structured data, files, or metadata during test execution. This is a low-level feature primarily designed for:

- Internal use ([`annotate`](/guide/test-annotations) is built on top of the artifact system)
- Framework authors creating custom testing tools on top of Vitest

Each artifact includes:

- A type discriminator which is a unique identifier for the artifact type
- Custom data, can be any relevant information
- Optional attachments, either files or inline content associated with the artifact
- A source code location indicating where the artifact was created

Vitest automatically manages attachment serialization (files are copied to [`attachmentsDir`](/config/#attachmentsdir)) and injects source location metadata, so you can focus on the data you want to record. All artifacts **must** extend from [`TestArtifactBase`](#testartifactbase) and all attachments from [`TestAttachment`](#testattachment) to be correctly handled internally.

## API

### `recordArtifact` <Experimental /> {#recordartifact}

::: warning
`recordArtifact` is an experimental API. Breaking changes might not follow SemVer, please pin Vitest's version when using it.

The API surface may change based on feedback. We encourage you to try it out and share your experience with the team.
:::

```ts
function recordArtifact<Artifact extends TestArtifact>(task: Test, artifact: Artifact): Promise<Artifact>
```

The `recordArtifact` function records an artifact during test execution and returns it. It expects a [task](/api/advanced/runner#tasks) as the first parameter and an object assignable to [`TestArtifact`](#testartifact) as the second.

This function has to be used within a test, and the test has to still be running. Recording after test completion will throw an error.

When an artifact is recorded on a test, it emits an `onTestArtifactRecord` runner event and a [`onTestCaseArtifactRecord` reporter event](/api/advanced/reporters#ontestcaseartifactrecord).

Note: annotations, [even though they're built on top of this feature](#relationship-with-annotations), won't appear in the `task.artifacts` array for backwards compatibility reasons until the next major version.

### `TestArtifact`

The `TestArtifact` type is a union containing all artifacts Vitest can produce, including custom ones. All artifacts extend from [`TestArtifactBase`](#testartifactbase)

### `TestArtifactBase` <Experimental /> {#testartifactbase}

```ts
export interface TestArtifactBase {
/** File or data attachments associated with this artifact */
attachments?: TestAttachment[]
/** Source location where this artifact was created */
location?: TestArtifactLocation
}
```

The `TestArtifactBase` interface is the base for all test artifacts.

Extend this interface when creating custom test artifacts. Vitest automatically manages the `attachments` array and injects the `location` property to indicate where the artifact was created in your test code.

### `TestAttachment`

```ts
export interface TestAttachment {
/** MIME type of the attachment (e.g., 'image/png', 'text/plain') */
contentType?: string
/** File system path to the attachment */
path?: string
/** Inline attachment content as a string or raw binary data */
body?: string | Uint8Array
}
```

The `TestAttachment` interface represents a file or data attachment associated with a test artifact.

Attachments can be either file-based (via `path`) or inline content (via `body`). The `contentType` helps consumers understand how to interpret the attachment data.

### `TestArtifactLocation`

```ts
export interface TestArtifactLocation {
/** Line number in the source file (1-indexed) */
line: number
/** Column number in the line (1-indexed) */
column: number
/** Path to the source file */
file: string
}
```

The `TestArtifactLocation` interface represents the source code location information for a test artifact. It indicates where in the source code the artifact originated from.

### `TestArtifactRegistry`

The `TestArtifactRegistry` interface is a registry for custom test artifact types.

Augmenting this interface using [TypeScript's module augmentation feature](https://typescriptlang.org/docs/handbook/declaration-merging#module-augmentation) allows registering custom artifact types that tests can produce.

Each custom artifact should extend [`TestArtifactBase`](#testartifactbase) and include a unique `type` discriminator property.

Here are a few guidelines or best practices to follow:

- Try using a `Symbol` as the **registry key** to guarantee uniqueness
- The `type` property should follow the pattern `'package-name:artifact-name'`, **`'internal:'` is a reserved prefix**
- Use `attachments` to include files or data; extend [`TestAttachment`](#testattachment) for custom metadata
- `location` property is automatically injected

## Custom Artifacts

To use and manage artifacts in a type-safe manner, you need to create its type and register it:

```ts
import type { TestArtifactBase, TestAttachment } from 'vitest'

interface A11yReportAttachment extends TestAttachment {
contentType: 'text/html'
path: string
}

interface AccessibilityArtifact extends TestArtifactBase {
type: 'a11y:report'
passed: boolean
wcagLevel: 'A' | 'AA' | 'AAA'
attachments: [A11yReportAttachment]
}

const a11yReportKey = Symbol('report')

declare module 'vitest' {
interface TestArtifactRegistry {
[a11yReportKey]: AccessibilityArtifact
}
}
```

As long as the types are assignable to their bases and don't have errors, everything should work fine and you should be able to record artifacts using [`recordArtifact`](#recordartifact):

```ts
async function toBeAccessible(
this: MatcherState,
actual: Element,
wcagLevel: 'A' | 'AA' | 'AAA' = 'AA'
): AsyncExpectationResult {
const report = await runAccessibilityAudit(actual, wcagLevel)

await recordArtifact(this.task, {
type: 'a11y:report',
passed: report.violations.length === 0,
wcagLevel,
attachments: [{
contentType: 'text/html',
path: report.path,
}],
})

return {
pass: violations.length === 0,
message: () => `Found ${report.violations.length} accessibility violation(s)`
}
}
```

## Relationship with Annotations

Test annotations are built on top of the artifact system. When using annotations in tests, they create `internal:annotation` artifacts under the hood. However, annotations are:

- Simpler to use
- Designed for end-users, not developers

Use annotations if you just want to add notes to your tests. Use artifacts if you need custom data.
16 changes: 16 additions & 0 deletions docs/api/advanced/reporters.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Vitest has its own test run lifecycle. These are represented by reporter's metho
- [`onHookEnd(beforeAll)`](#onhookend)
- [`onTestCaseReady`](#ontestcaseready)
- [`onTestAnnotate`](#ontestannotate) <Version>3.2.0</Version>
- [`onTestCaseArtifactRecord`](#ontestcaseartifactrecord) <Version type="experimental">4.0.11</Version>
- [`onHookStart(beforeEach)`](#onhookstart)
- [`onHookEnd(beforeEach)`](#onhookend)
- [`onHookStart(afterEach)`](#onhookstart)
Expand Down Expand Up @@ -332,3 +333,18 @@ function onTestAnnotate(
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.

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.

## onTestCaseArtifactRecord <Version type="experimental">4.0.11</Version> {#ontestcaseartifactrecord}

```ts
function onTestCaseArtifactRecord(
testCase: TestCase,
artifact: TestArtifact,
): Awaitable<void>
```

The `onTestCaseArtifactRecord` hook is associated with the [`recordArtifact`](/api/advanced/artifacts#recordartifact) utility. When `recordArtifact` is invoked, Vitest serialises it and sends the same attachment to the main thread where reporter can interact with it.

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.

Note: annotations, [even though they're built on top of this feature](/api/advanced/artifacts#relationship-with-annotations), won't hit this hook and won't appear in the `task.artifacts` array for backwards compatibility reasons until the next major version.
31 changes: 18 additions & 13 deletions packages/browser-playwright/src/commands/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,25 @@ export const annotateTraces: BrowserCommand<[{ traces: string[]; testId: string
const vitest = project.vitest
await Promise.all(traces.map((trace) => {
const entity = vitest.state.getReportedEntityById(testId)
return vitest._testRun.annotate(testId, {
message: relative(project.config.root, trace),
type: 'traces',
attachment: {
path: trace,
contentType: 'application/octet-stream',
const location = entity?.location
? {
file: entity.module.moduleId,
line: entity.location.line,
column: entity.location.column,
}
: undefined
return vitest._testRun.recordArtifact(testId, {
type: 'internal:annotation',
annotation: {
message: relative(project.config.root, trace),
type: 'traces',
attachment: {
path: trace,
contentType: 'application/octet-stream',
},
location,
},
location: entity?.location
? {
file: entity.module.moduleId,
line: entity.location.line,
column: entity.location.column,
}
: undefined,
location,
})
}))
}
Expand Down
30 changes: 22 additions & 8 deletions packages/browser/src/client/tester/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
TaskResultPack,
Test,
TestAnnotation,
TestArtifact,
VitestRunner,
} from '@vitest/runner'
import type { SerializedConfig, TestExecutionMethod, WorkerGlobalState } from 'vitest'
Expand Down Expand Up @@ -227,28 +228,41 @@ export function createBrowserRunner(
}

onTestAnnotate = (test: Test, annotation: TestAnnotation): Promise<TestAnnotation> => {
if (annotation.location) {
const artifact: TestArtifact = { type: 'internal:annotation', annotation, location: annotation.location }

return this.onTestArtifactRecord(test, artifact).then(({ annotation }) => annotation)
}

onTestArtifactRecord = <Artifact extends TestArtifact>(test: Test, artifact: Artifact): Promise<Artifact> => {
if (artifact.location) {
// the file should be the test file
// tests from other files are not supported
const map = this.sourceMapCache.get(annotation.location.file)
const map = this.sourceMapCache.get(artifact.location.file)

if (!map) {
return rpc().onTaskAnnotate(test.id, annotation)
return rpc().onTaskArtifactRecord(test.id, artifact)
}

const traceMap = new DecodedMap(map as any, annotation.location.file)
const position = getOriginalPosition(traceMap, annotation.location)
const traceMap = new DecodedMap(map as any, artifact.location.file)
const position = getOriginalPosition(traceMap, artifact.location)

if (position) {
const { source, column, line } = position
const file = source || annotation.location.file
annotation.location = {
const file = source || artifact.location.file
artifact.location = {
line,
column: column + 1,
// if the file path is on windows, we need to remove the starting slash
file: file.match(/\/\w:\//) ? file.slice(1) : file,
}

if (artifact.type === 'internal:annotation') {
artifact.annotation.location = artifact.location
}
}
}
return rpc().onTaskAnnotate(test.id, annotation)

return rpc().onTaskArtifactRecord(test.id, artifact)
}

onTaskUpdate = (task: TaskResultPack[], events: TaskEventPack[]): Promise<void> => {
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/node/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
await vitest._testRun.collected(project, files)
}
},
async onTaskAnnotate(id, annotation) {
return vitest._testRun.annotate(id, annotation)
async onTaskArtifactRecord(id, artifact) {
return vitest._testRun.recordArtifact(id, artifact)
},
async onTaskUpdate(method, packs, events) {
if (method === 'collect') {
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MockedModuleSerialized, ServerIdResolution, ServerMockResolution } from '@vitest/mocker'
import type { TaskEventPack, TaskResultPack, TestAnnotation } from '@vitest/runner'
import type { TaskEventPack, TaskResultPack, TestArtifact } from '@vitest/runner'
import type { BirpcReturn } from 'birpc'
import type {
AfterSuiteRunMeta,
Expand All @@ -18,7 +18,7 @@ export interface WebSocketBrowserHandlers {
onUnhandledError: (error: unknown, type: string) => Promise<void>
onQueued: (method: TestExecutionMethod, file: RunnerTestFile) => void
onCollected: (method: TestExecutionMethod, files: RunnerTestFile[]) => Promise<void>
onTaskAnnotate: (testId: string, annotation: TestAnnotation) => Promise<TestAnnotation>
onTaskArtifactRecord: <Artifact extends TestArtifact>(testId: string, artifact: Artifact) => Promise<Artifact>
onTaskUpdate: (method: TestExecutionMethod, packs: TaskResultPack[], events: TaskEventPack[]) => void
onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void
cancelCurrentRun: (reason: CancelReason) => void
Expand Down
Loading
Loading