Skip to content
Open
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
15 changes: 13 additions & 2 deletions docs/advanced/api/test-case.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,17 @@ Checks if the test did not fail the suite. If the test is not finished yet or wa
function meta(): TaskMeta
```

Custom [metadata](/advanced/metadata) that was attached to the test during its execution. The meta can be attached by assigning a property to the `ctx.task.meta` object during a test run:
Custom [metadata](/advanced/metadata) that was attached to the test during its execution or defined in test options. The meta can be defined in multiple ways:

**Using test options** (available since Vitest 4.0):
```ts {1}
test('the validation works correctly', { meta: { component: 'auth', priority: 'high' } }, ({ task }) => {
console.log(task.meta.component) // 'auth'
console.log(task.meta.priority) // 'high'
})
```

**Runtime assignment during test execution**:
```ts {3,6}
import { test } from 'vitest'

Expand All @@ -137,7 +146,9 @@ test('the validation works correctly', ({ task }) => {
})
```

If the test did not finish running yet, the meta will be an empty object.
Metadata from test options is merged with suite options and runtime assignments. See the [metadata documentation](/advanced/metadata) for details on merging behavior.

If the test did not finish running yet, the meta will contain only the metadata from options (if any).

## result

Expand Down
18 changes: 17 additions & 1 deletion docs/advanced/api/test-suite.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,22 @@ Note that errors are serialized into simple objects: `instanceof Error` will alw
function meta(): TaskMeta
```

Custom [metadata](/advanced/metadata) that was attached to the suite during its execution or collection. The meta can be attached by assigning a property to the `task.meta` object during a test run:
Custom [metadata](/advanced/metadata) that was attached to the suite during its execution, collection, or defined in describe options. The meta can be defined in multiple ways:

**Using describe options** (available since Vitest 4.0):
```ts {1}
describe('the validation works correctly', { meta: { component: 'auth', area: 'validation' } }, () => {
test('some test', ({ task }) => {
console.log(task.suite.meta.component) // 'auth'
console.log(task.suite.meta.area) // 'validation'
// Tests inherit suite metadata automatically
console.log(task.meta.component) // 'auth'
console.log(task.meta.area) // 'validation'
})
})
```

**Runtime assignment during collection or test execution**:
```ts {5,10}
import { test } from 'vitest'

Expand All @@ -214,6 +228,8 @@ describe('the validation works correctly', (task) => {
})
```

Suite metadata from options is automatically inherited by child tests and can be merged with test-specific metadata. See the [metadata documentation](/advanced/metadata) for details on merging behavior.

:::tip
If metadata was attached during collection (outside of the `test` function), then it will be available in [`onTestModuleCollected`](./reporters#ontestmodulecollected) hook in the custom reporter.
:::
72 changes: 72 additions & 0 deletions docs/advanced/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,78 @@ test('custom', ({ task }) => {
})
```

## Defining Metadata in Test Options

Since Vitest 4.0, you can also define metadata directly in the test options, which will be merged with metadata from all ancestor suites in the hierarchy:

```ts
describe('suite', { meta: { suiteLevel: 'parent', priority: 'medium' } }, () => {
test('with meta options', { meta: { testLevel: 'child', priority: 'high' } }, ({ task }) => {
// task.meta contains merged metadata:
// { suiteLevel: 'parent', testLevel: 'child', priority: 'high' }
// Note: test meta overrides suite meta when there are conflicts
console.log(task.meta.suiteLevel) // 'parent'
console.log(task.meta.testLevel) // 'child'
console.log(task.meta.priority) // 'high' (test overrides suite)
})

test('inherits suite meta', ({ task }) => {
// task.meta only contains suite metadata:
// { suiteLevel: 'parent', priority: 'medium' }
console.log(task.meta.suiteLevel) // 'parent'
console.log(task.meta.priority) // 'medium'
})
})
```

For nested describe blocks, metadata cascades through all levels of the hierarchy:

```ts
describe('Grandparent Suite', { meta: { level: 'root', priority: 'low' } }, () => {
describe('Parent Suite', { meta: { level: 'middle', priority: 'medium' } }, () => {
test('deeply nested test', ({ task }) => {
// task.meta contains metadata from all ancestor suites:
// { level: 'middle', priority: 'medium' }
// Note: closer ancestors override distant ancestors
console.log(task.meta.level) // 'middle' (parent overrides grandparent)
console.log(task.meta.priority) // 'medium' (parent overrides grandparent)
})
})
})
```

The metadata merging follows this priority order (lowest to highest):
1. Distant ancestor suite `meta` options (e.g., grandparent suites)
2. Closer ancestor suite `meta` options (e.g., parent suites)
3. Test-level `meta` options
4. Runtime modifications via `task.meta`

## Accessing Suite vs Test Metadata

While tests get merged metadata in `task.meta`, the original suite metadata is preserved separately:

```ts
describe('suite', { meta: { component: 'auth', area: 'validation' } }, () => {
test('example', { meta: { testType: 'integration' } }, ({ task }) => {
// Merged metadata (suite + test)
console.log(task.meta)
// { component: 'auth', area: 'validation', testType: 'integration' }

// Original suite metadata only
console.log(task.suite.meta)
// { component: 'auth', area: 'validation' }

// They are different objects
console.log(task.meta !== task.suite.meta) // true
})
})
```

This separation allows you to:
- Access the test's complete merged metadata via `task.meta`
- Access the suite's original metadata via `task.suite.meta`
- Distinguish between suite-level and test-level metadata in reporters and custom logic

Once a test is completed, Vitest will send a task including the result and `meta` to the Node.js process using RPC, and then report it in `onTestCaseResult` and other hooks that have access to tasks. To process this test case, you can utilize the `onTestCaseResult` method available in your reporter implementation:

```ts [custom-reporter.js]
Expand Down
19 changes: 19 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ interface TestOptions {
* @default 0
*/
repeats?: number
/**
* Custom metadata for the task. This will be merged with any meta property defined in the test.
* Values must be JSON serializable.
*
* @default undefined
*/
meta?: Partial<TaskMeta>
}
```

Expand Down Expand Up @@ -733,6 +740,18 @@ bench.todo('unimplemented test')

When you use `test` or `bench` in the top level of file, they are collected as part of the implicit suite for it. Using `describe` you can define a new suite in the current context, as a set of related tests or benchmarks and other nested suites. A suite lets you organize your tests and benchmarks so reports are more clear.

Like `test`, you can also provide options as a second argument to configure the suite behavior:

```ts
import { describe, test } from 'vitest'

describe('suite with options', { meta: { component: 'auth' } }, () => {
test('inherits suite metadata', ({ task }) => {
console.log(task.meta.component) // 'auth'
})
})
```

```ts
// basic.spec.ts
// organizing tests
Expand Down
31 changes: 29 additions & 2 deletions packages/runner/src/suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,28 @@ function assert(condition: any, message: string) {
}
}

function collectAncestorMeta(suite: Suite | undefined): Record<string, unknown> {
const ancestorMeta = Object.create(null)
let current = suite

// Walk up the suite hierarchy and collect metadata
// We'll collect from root to child so that closer ancestors override distant ones
const suites: Suite[] = []
while (current) {
if (current.meta) {
suites.unshift(current) // Add to beginning so we process from root to child
}
current = current.suite
}

// Merge metadata with closer ancestors having higher priority
for (const s of suites) {
Object.assign(ancestorMeta, s.meta)
}

return ancestorMeta
}

export function getDefaultSuite(): SuiteCollector<object> {
assert(defaultSuite, 'the default suite')
return defaultSuite
Expand Down Expand Up @@ -325,7 +347,10 @@ function createSuiteCollector(
: options.todo
? 'todo'
: 'run',
meta: options.meta ?? Object.create(null),
meta: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the meta with a null prototype. Just assign options.meta to meta collected with collectAncestorMeta:

const testMeta = collectAncestorMeta(collectorContext.currentSuite?.suite)
if(options.meta) {
  Object.assign(testMeta, options.meta)
}

{
  // ...
  meta: testMeta,
}

...collectAncestorMeta(collectorContext.currentSuite?.suite),
...(options.meta || {}),
},
annotations: [],
}
const handler = options.handler
Expand Down Expand Up @@ -457,7 +482,9 @@ function createSuiteCollector(
file: undefined!,
shuffle: suiteOptions?.shuffle,
tasks: [],
meta: Object.create(null),
meta: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

...(suiteOptions?.meta || {}),
},
concurrent: suiteOptions?.concurrent,
}

Expand Down
6 changes: 5 additions & 1 deletion packages/runner/src/types/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,10 @@ export interface TestOptions {
* Whether the test is expected to fail. If it does, the test will pass, otherwise it will fail.
*/
fails?: boolean
/**
* Custom metadata for the task. This will be merged with any meta property defined in the test.
*/
meta?: Partial<TaskMeta>
}

interface ExtendedAPI<ExtraContext> {
Expand Down Expand Up @@ -637,7 +641,7 @@ export interface TaskCustomOptions extends TestOptions {
/**
* Custom metadata for the task that will be assigned to `task.meta`.
*/
meta?: Record<string, unknown>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sheremet-va This is what I meant here

meta?: Partial<TaskMeta>
/**
* Task fixtures.
*/
Expand Down
125 changes: 125 additions & 0 deletions test/core/test/test-meta-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { beforeEach, describe, expect, test } from 'vitest'

describe('TestOptions meta property functionality', { meta: { suiteLevel: 'test-suite', priority: 'medium' } }, () => {

Check failure on line 3 in test/core/test/test-meta-options.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

No overload matches this call.
let beforeEachMeta: Record<string, unknown>

beforeEach(({ task }) => {
beforeEachMeta = { ...task.meta }
})

test('should merge suite and test meta properties', { meta: { testLevel: 'individual-test', priority: 'high' } }, ({ task }) => {

Check failure on line 10 in test/core/test/test-meta-options.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

Binding element 'task' implicitly has an 'any' type.

Check failure on line 10 in test/core/test/test-meta-options.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

No overload matches this call.
// Test should have both suite and test meta
expect(task.meta).toMatchObject({
suiteLevel: 'test-suite',
testLevel: 'individual-test',
priority: 'high', // test meta should override suite meta
})

// beforeEach should have access to merged meta
expect(beforeEachMeta).toMatchObject({
suiteLevel: 'test-suite',
testLevel: 'individual-test',
priority: 'high',
})
})

test('should inherit suite meta when no test meta provided', ({ task }) => {
// Test should only have suite meta
expect(task.meta).toMatchObject({
suiteLevel: 'test-suite',
priority: 'medium',
})

// beforeEach should have access to suite meta
expect(beforeEachMeta).toMatchObject({
suiteLevel: 'test-suite',
priority: 'medium',
})
})

test('should allow adding meta at runtime', { meta: { testLevel: 'runtime-test' } }, ({ task }) => {

Check failure on line 40 in test/core/test/test-meta-options.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

Binding element 'task' implicitly has an 'any' type.

Check failure on line 40 in test/core/test/test-meta-options.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

No overload matches this call.
// Add meta at runtime
task.meta.runtimeAdded = 'added-during-test'

expect(task.meta).toMatchObject({
suiteLevel: 'test-suite',
testLevel: 'runtime-test',
priority: 'medium',
runtimeAdded: 'added-during-test',
})
})

test('should differentiate between task.meta and task.suite.meta', { meta: { testLevel: 'child-test', priority: 'high' } }, ({ task }) => {

Check failure on line 52 in test/core/test/test-meta-options.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

Binding element 'task' implicitly has an 'any' type.

Check failure on line 52 in test/core/test/test-meta-options.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

No overload matches this call.
// task.meta should contain merged metadata (suite + test)
expect(task.meta).toMatchObject({
suiteLevel: 'test-suite',
testLevel: 'child-test',
priority: 'high', // test overrides suite
})

// task.suite.meta should contain only suite's own metadata
expect(task.suite?.meta).toMatchObject({
suiteLevel: 'test-suite',
priority: 'medium', // original suite priority
})

// They should be different objects
expect(task.meta).not.toBe(task.suite?.meta)

// task.suite.meta should NOT have test-specific metadata
expect(task.suite?.meta).not.toHaveProperty('testLevel')
})
})

describe('Suite without meta', () => {
let beforeEachMeta: Record<string, unknown>

beforeEach(({ task }) => {
beforeEachMeta = { ...task.meta }
})

test('should only have test meta when suite has no meta', { meta: { testOnly: 'test-meta' } }, ({ task }) => {

Check failure on line 81 in test/core/test/test-meta-options.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

Binding element 'task' implicitly has an 'any' type.

Check failure on line 81 in test/core/test/test-meta-options.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

No overload matches this call.
expect(task.meta).toMatchObject({
testOnly: 'test-meta',
})

expect(beforeEachMeta).toMatchObject({
testOnly: 'test-meta',
})
})
})

describe('Nested describes metadata cascading', { meta: { grandparent: 'top-level', priority: 'low' } }, () => {
describe('Middle suite', { meta: { parent: 'middle-level', priority: 'medium' } }, () => {
test('should cascade metadata from all ancestor suites', ({ task }) => {
// Should now get metadata from all ancestors: grandparent + parent
expect(task.meta).toMatchObject({
grandparent: 'top-level', // from grandparent suite
parent: 'middle-level', // from parent suite
priority: 'medium', // parent overrides grandparent
})

// Original suite metadata should be preserved
expect(task.suite?.meta).toMatchObject({
parent: 'middle-level',
priority: 'medium',
})

// Grandparent suite metadata should also be preserved
expect(task.suite?.suite?.meta).toMatchObject({
grandparent: 'top-level',
priority: 'low',
})
})

test('test metadata should override cascaded suite metadata', { meta: { testLevel: 'child', priority: 'highest' } }, ({ task }) => {
// Should get metadata from all ancestors plus test metadata
expect(task.meta).toMatchObject({
grandparent: 'top-level', // from grandparent suite
parent: 'middle-level', // from parent suite
testLevel: 'child', // from test
priority: 'highest', // test overrides all ancestors
})
})
})
})
Loading