diff --git a/docs/api/advanced/test-case.md b/docs/api/advanced/test-case.md
index c9601650bc40..b91d14b46152 100644
--- a/docs/api/advanced/test-case.md
+++ b/docs/api/advanced/test-case.md
@@ -143,7 +143,13 @@ test('the validation works correctly', ({ task }) => {
})
```
-If the test did not finish running yet, the meta will be an empty object.
+If the test did not finish running yet, the meta will be an empty object, unless it has static meta:
+
+```ts
+test('the validation works correctly', { meta: { decorated: true } })
+```
+
+Since Vitest 4.1, Vitest inherits [`meta`](/api/advanced/test-suite#meta) property defined on the [suite](/api/advanced/test-suite).
## result
diff --git a/docs/api/advanced/test-suite.md b/docs/api/advanced/test-suite.md
index 56abb67ec125..f8aa837fc62a 100644
--- a/docs/api/advanced/test-suite.md
+++ b/docs/api/advanced/test-suite.md
@@ -198,24 +198,25 @@ Note that errors are serialized into simple objects: `instanceof Error` will alw
function meta(): TaskMeta
```
-Custom [metadata](/api/advanced/metadata) that was attached to the suite during its execution or collection. The meta can be attached by assigning a property to the `suite.meta` object during a test run:
+Custom [metadata](/api/advanced/metadata) that was attached to the suite during its execution or collection. Since Vitest 4.1, the meta can be attached by providing a `meta` object during test collection:
-```ts {7,12}
+```ts {7,10}
import { describe, test, TestRunner } from 'vitest'
-describe('the validation works correctly', () => {
- // assign "decorated" during collection
- const { suite } = TestRunner.getCurrentSuite()
- suite!.meta.decorated = true
-
+describe('the validation works correctly', { meta: { decorated: true } }, () => {
test('some test', ({ task }) => {
// assign "decorated" during test run, it will be available
// only in onTestCaseReady hook
task.suite.meta.decorated = false
+
+ // tests inherit suite's metadata
+ task.meta.decorated === true
})
})
```
+Note that suite metadata will be inherited by tests since Vitest 4.1.
+
:::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.
:::
diff --git a/docs/api/test.md b/docs/api/test.md
index 84c108bbd2a5..93d6532bb1d3 100644
--- a/docs/api/test.md
+++ b/docs/api/test.md
@@ -170,6 +170,45 @@ it('user returns data from db', { tags: ['db', 'flaky'] }, () => {
})
```
+### meta 4.1.0 {#meta}
+
+- **Type:** `TaskMeta`
+
+Attaches custom [metadata](/api/advanced/metadata) available in reporters.
+
+::: warning
+Vitest merges top-level properties inherited from suites or tags. However, it does not perform a deep merge of nested objects.
+
+```ts
+import { describe, test } from 'vitest'
+
+describe(
+ 'nested meta',
+ {
+ meta: {
+ nested: { object: true, array: false },
+ },
+ },
+ () => {
+ test(
+ 'overrides part of meta',
+ {
+ meta: {
+ nested: { object: false }
+ },
+ },
+ ({ task }) => {
+ // task.meta === { nested: { object: false } }
+ // notice array got lost because "nested" object was overriden
+ }
+ )
+ }
+)
+```
+
+Prefer using non-nested meta, if possible.
+:::
+
### concurrent
- **Type:** `boolean`
diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts
index 911479b88f19..f56ee8426f61 100644
--- a/packages/runner/src/suite.ts
+++ b/packages/runner/src/suite.ts
@@ -331,16 +331,32 @@ function createSuiteCollector(
// higher priority should be last, run 1, 2, 3, ... etc
.sort((tag1, tag2) => (tag2.priority ?? POSITIVE_INFINITY) - (tag1.priority ?? POSITIVE_INFINITY))
.reduce((acc, tag) => {
- const { name, description, priority, ...options } = tag
+ const { name, description, priority, meta, ...options } = tag
Object.assign(acc, options)
+ if (meta) {
+ acc.meta = Object.assign(acc.meta ?? Object.create(null), meta)
+ }
return acc
}, {} as TestOptions)
+ const testOwnMeta = options.meta
options = {
...tagsOptions,
...options,
}
const timeout = options.timeout ?? runner.config.testTimeout
+ const parentMeta = currentSuite?.meta
+ const tagMeta = tagsOptions.meta
+ const testMeta = Object.create(null)
+ if (tagMeta) {
+ Object.assign(testMeta, tagMeta)
+ }
+ if (parentMeta) {
+ Object.assign(testMeta, parentMeta)
+ }
+ if (testOwnMeta) {
+ Object.assign(testMeta, testOwnMeta)
+ }
const task: Test = {
id: '',
name,
@@ -365,7 +381,7 @@ function createSuiteCollector(
: options.todo
? 'todo'
: 'run',
- meta: options.meta ?? Object.create(null),
+ meta: testMeta,
annotations: [],
artifacts: [],
tags: testTags,
@@ -513,7 +529,7 @@ function createSuiteCollector(
file: (currentSuite?.file ?? collectorContext.currentSuite?.file)!,
shuffle: suiteOptions?.shuffle,
tasks: [],
- meta: Object.create(null),
+ meta: suiteOptions?.meta ?? Object.create(null),
concurrent: suiteOptions?.concurrent,
tags: unique([...parentTask?.tags || [], ...suiteTags]),
}
@@ -604,9 +620,10 @@ function createSuite() {
const isConcurrentSpecified = options.concurrent || this.concurrent || options.sequential === false
const isSequentialSpecified = options.sequential || this.sequential || options.concurrent === false
+ const { meta: parentMeta, ...parentOptions } = currentSuite?.options || {}
// inherit options from current suite
options = {
- ...currentSuite?.options,
+ ...parentOptions,
...options,
}
@@ -638,6 +655,10 @@ function createSuite() {
options.sequential = isSequential && !isConcurrent
}
+ if (parentMeta) {
+ options.meta = Object.assign(Object.create(null), parentMeta, options.meta)
+ }
+
return createSuiteCollector(
formatName(name),
factory,
diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts
index 948e358c68ba..2fbffb924a12 100644
--- a/packages/runner/src/types/tasks.ts
+++ b/packages/runner/src/types/tasks.ts
@@ -570,6 +570,10 @@ export interface TestOptions {
tags?: keyof TestTags extends never
? string[] | string
: TestTags[keyof TestTags] | TestTags[keyof TestTags][]
+ /**
+ * Custom test metadata available to reporters.
+ */
+ meta?: Partial
}
export interface TestTags {}
@@ -735,10 +739,6 @@ export interface TaskCustomOptions extends TestOptions {
* Whether the task was produced with `.each()` method.
*/
each?: boolean
- /**
- * Custom metadata for the task that will be assigned to `task.meta`.
- */
- meta?: Record
/**
* Task fixtures.
*/
diff --git a/test/cli/test/test-meta.test.ts b/test/cli/test/test-meta.test.ts
new file mode 100644
index 000000000000..c22b49d8cc96
--- /dev/null
+++ b/test/cli/test/test-meta.test.ts
@@ -0,0 +1,699 @@
+import type { TestCase, TestSuite } from 'vitest/node'
+import { runInlineTests } from '#test-utils'
+import { expect, test } from 'vitest'
+
+test('meta can be defined on test options', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test 1', { meta: { custom: 'value', count: 42 } }, () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "count": 42,
+ "custom": "value",
+ }
+ `)
+})
+
+test('meta can be defined on suite options', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { suiteKey: 'suiteValue' } }, () => {
+ test('test 1', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ expect(testSuite.meta()).toMatchInlineSnapshot(`
+ {
+ "suiteKey": "suiteValue",
+ }
+ `)
+})
+
+test('test inherits meta from parent suite', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { inherited: true, level: 'suite' } }, () => {
+ test('test 1', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const testCase = testSuite.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "inherited": true,
+ "level": "suite",
+ }
+ `)
+})
+
+test('test meta overrides inherited suite meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { shared: 'fromSuite', suiteOnly: true } }, () => {
+ test('test 1', { meta: { shared: 'fromTest', testOnly: 123 } }, () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const testCase = testSuite.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "shared": "fromTest",
+ "suiteOnly": true,
+ "testOnly": 123,
+ }
+ `)
+})
+
+test('nested suites inherit meta from parent suites', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('outer', { meta: { outer: true } }, () => {
+ describe('inner', { meta: { inner: true } }, () => {
+ test('test 1', () => {})
+ })
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const outerSuite = testModule.children.at(0) as TestSuite
+ const innerSuite = outerSuite.children.at(0) as TestSuite
+ const testCase = innerSuite.children.at(0) as TestCase
+
+ expect(outerSuite.meta()).toMatchInlineSnapshot(`
+ {
+ "outer": true,
+ }
+ `)
+ expect(innerSuite.meta()).toMatchInlineSnapshot(`
+ {
+ "inner": true,
+ "outer": true,
+ }
+ `)
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "inner": true,
+ "outer": true,
+ }
+ `)
+})
+
+test('deeply nested meta inheritance with overrides', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('level1', { meta: { level: 1, a: 'first' } }, () => {
+ describe('level2', { meta: { level: 2, b: 'second' } }, () => {
+ describe('level3', { meta: { level: 3, a: 'override' } }, () => {
+ test('test 1', { meta: { level: 4 } }, () => {})
+ })
+ })
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const level1 = testModule.children.at(0) as TestSuite
+ const level2 = level1.children.at(0) as TestSuite
+ const level3 = level2.children.at(0) as TestSuite
+ const testCase = level3.children.at(0) as TestCase
+
+ expect(level1.meta()).toMatchInlineSnapshot(`
+ {
+ "a": "first",
+ "level": 1,
+ }
+ `)
+ expect(level2.meta()).toMatchInlineSnapshot(`
+ {
+ "a": "first",
+ "b": "second",
+ "level": 2,
+ }
+ `)
+ expect(level3.meta()).toMatchInlineSnapshot(`
+ {
+ "a": "override",
+ "b": "second",
+ "level": 3,
+ }
+ `)
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "a": "override",
+ "b": "second",
+ "level": 4,
+ }
+ `)
+})
+
+test('meta is accessible from task.meta inside tests', async () => {
+ const { stderr, stdout } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { suiteKey: 'inherited' } }, () => {
+ test('test 1', { meta: { testKey: 'own' } }, ({ task }) => {
+ console.log('META:', JSON.stringify(task.meta))
+ })
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const metaLine = stdout.split('\n').find(line => line.startsWith('META:'))
+ expect(metaLine).toBeDefined()
+ expect(JSON.parse(metaLine!.slice('META:'.length))).toMatchInlineSnapshot(`
+ {
+ "suiteKey": "inherited",
+ "testKey": "own",
+ }
+ `)
+})
+
+test('sibling tests have independent meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { shared: 'parent' } }, () => {
+ test('test 1', { meta: { id: 1 } }, () => {})
+ test('test 2', { meta: { id: 2 } }, () => {})
+ test('test 3', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const [test1, test2, test3] = testSuite.children.array() as TestCase[]
+
+ expect(test1.meta()).toMatchInlineSnapshot(`
+ {
+ "id": 1,
+ "shared": "parent",
+ }
+ `)
+ expect(test2.meta()).toMatchInlineSnapshot(`
+ {
+ "id": 2,
+ "shared": "parent",
+ }
+ `)
+ expect(test3.meta()).toMatchInlineSnapshot(`
+ {
+ "shared": "parent",
+ }
+ `)
+})
+
+test('sibling suites have independent meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite1', { meta: { suite: 1 } }, () => {
+ test('test 1', () => {})
+ })
+ describe('suite2', { meta: { suite: 2 } }, () => {
+ test('test 2', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const [suite1, suite2] = testModule.children.array() as TestSuite[]
+ const test1 = suite1.children.at(0) as TestCase
+ const test2 = suite2.children.at(0) as TestCase
+
+ expect(suite1.meta()).toMatchInlineSnapshot(`
+ {
+ "suite": 1,
+ }
+ `)
+ expect(suite2.meta()).toMatchInlineSnapshot(`
+ {
+ "suite": 2,
+ }
+ `)
+ expect(test1.meta()).toMatchInlineSnapshot(`
+ {
+ "suite": 1,
+ }
+ `)
+ expect(test2.meta()).toMatchInlineSnapshot(`
+ {
+ "suite": 2,
+ }
+ `)
+})
+
+test('test without parent suite has empty meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test 1', () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`{}`)
+})
+
+test('test.each works with meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { feature: 'each' } }, () => {
+ test.each([1, 2, 3])('test %i', { meta: { eachTest: true } }, () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const tests = testSuite.children.array() as TestCase[]
+
+ expect(tests).toHaveLength(3)
+ for (const test of tests) {
+ expect(test.meta()).toMatchInlineSnapshot(`
+ {
+ "eachTest": true,
+ "feature": "each",
+ }
+ `)
+ }
+})
+
+test('describe.each works with meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe.each([1, 2])('suite %i', { meta: { dynamic: true } }, () => {
+ test('test', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const [suite1, suite2] = testModule.children.array() as TestSuite[]
+
+ expect(suite1.meta()).toMatchInlineSnapshot(`
+ {
+ "dynamic": true,
+ }
+ `)
+ expect(suite2.meta()).toMatchInlineSnapshot(`
+ {
+ "dynamic": true,
+ }
+ `)
+ expect((suite1.children.at(0) as TestCase).meta()).toMatchInlineSnapshot(`
+ {
+ "dynamic": true,
+ }
+ `)
+})
+
+test('concurrent tests have independent meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { shared: true } }, () => {
+ test.concurrent('test 1', { meta: { id: 1 } }, () => {})
+ test.concurrent('test 2', { meta: { id: 2 } }, () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const [test1, test2] = testSuite.children.array() as TestCase[]
+
+ expect(test1.meta()).toMatchInlineSnapshot(`
+ {
+ "id": 1,
+ "shared": true,
+ }
+ `)
+ expect(test2.meta()).toMatchInlineSnapshot(`
+ {
+ "id": 2,
+ "shared": true,
+ }
+ `)
+})
+
+test('meta with complex values', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test 1', {
+ meta: {
+ nested: { a: { b: { c: 1 } } },
+ array: [1, 2, 3],
+ nullValue: null,
+ boolTrue: true,
+ boolFalse: false,
+ num: 42.5,
+ }
+ }, () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "array": [
+ 1,
+ 2,
+ 3,
+ ],
+ "boolFalse": false,
+ "boolTrue": true,
+ "nested": {
+ "a": {
+ "b": {
+ "c": 1,
+ },
+ },
+ },
+ "nullValue": null,
+ "num": 42.5,
+ }
+ `)
+})
+
+test('meta works with test modifiers (skip, only, todo)', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test.skip('skipped test', { meta: { status: 'skipped' } }, () => {})
+ test.todo('todo test', { meta: { status: 'todo' } })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const [skipped, todo] = testModule.children.array() as TestCase[]
+
+ expect(skipped.meta()).toMatchInlineSnapshot(`
+ {
+ "status": "skipped",
+ }
+ `)
+ expect(todo.meta()).toMatchInlineSnapshot(`
+ {
+ "status": "todo",
+ }
+ `)
+})
+
+test('meta works with test.fails', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test.fails('failing test', { meta: { expectFailure: true } }, () => {
+ throw new Error('Expected error')
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "expectFailure": true,
+ }
+ `)
+ expect(testCase.result().state).toBe('passed')
+})
+
+test('suite without meta does not inherit to tests', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite without meta', () => {
+ test('test with meta', { meta: { ownMeta: true } }, () => {})
+ test('test without meta', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const [withMeta, withoutMeta] = testSuite.children.array() as TestCase[]
+
+ expect(testSuite.meta()).toMatchInlineSnapshot(`{}`)
+ expect(withMeta.meta()).toMatchInlineSnapshot(`
+ {
+ "ownMeta": true,
+ }
+ `)
+ expect(withoutMeta.meta()).toMatchInlineSnapshot(`{}`)
+})
+
+test('meta does not mutate parent when child overrides', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('parent', { meta: { key: 'parent', parentOnly: true } }, () => {
+ describe('child', { meta: { key: 'child', childOnly: true } }, () => {
+ test('test', () => {})
+ })
+ test('sibling test', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const parent = testModule.children.at(0) as TestSuite
+ const child = parent.children.at(0) as TestSuite
+ const siblingTest = parent.children.at(1) as TestCase
+
+ expect(parent.meta()).toMatchInlineSnapshot(`
+ {
+ "key": "parent",
+ "parentOnly": true,
+ }
+ `)
+ expect(child.meta()).toMatchInlineSnapshot(`
+ {
+ "childOnly": true,
+ "key": "child",
+ "parentOnly": true,
+ }
+ `)
+ expect(siblingTest.meta()).toMatchInlineSnapshot(`
+ {
+ "key": "parent",
+ "parentOnly": true,
+ }
+ `)
+})
+
+test('meta with test.for', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { fromSuite: true } }, () => {
+ test.for([
+ { input: 1, expected: 2 },
+ { input: 2, expected: 4 },
+ ])('test $input', { meta: { forTest: true } }, ({ input, expected }) => {
+ expect(input * 2).toBe(expected)
+ })
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const tests = testSuite.children.array() as TestCase[]
+
+ expect(tests).toHaveLength(2)
+ for (const test of tests) {
+ expect(test.meta()).toMatchInlineSnapshot(`
+ {
+ "forTest": true,
+ "fromSuite": true,
+ }
+ `)
+ }
+})
+
+test('empty meta object is allowed', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: {} }, () => {
+ test('test', { meta: {} }, () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const testCase = testSuite.children.at(0) as TestCase
+
+ expect(testSuite.meta()).toMatchInlineSnapshot(`{}`)
+ expect(testCase.meta()).toMatchInlineSnapshot(`{}`)
+})
+
+test('meta inheritance across multiple files', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'file1.test.js': `
+ describe('suite in file1', { meta: { file: 1 } }, () => {
+ test('test 1', () => {})
+ })
+ `,
+ 'file2.test.js': `
+ describe('suite in file2', { meta: { file: 2 } }, () => {
+ test('test 2', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModules = ctx!.state.getTestModules()
+ const file1Module = testModules.find(m => m.moduleId.includes('file1'))!
+ const file2Module = testModules.find(m => m.moduleId.includes('file2'))!
+
+ const suite1 = file1Module.children.at(0) as TestSuite
+ const suite2 = file2Module.children.at(0) as TestSuite
+ const test1 = suite1.children.at(0) as TestCase
+ const test2 = suite2.children.at(0) as TestCase
+
+ expect(test1.meta()).toMatchInlineSnapshot(`
+ {
+ "file": 1,
+ }
+ `)
+ expect(test2.meta()).toMatchInlineSnapshot(`
+ {
+ "file": 2,
+ }
+ `)
+})
diff --git a/test/cli/test/test-tags.test.ts b/test/cli/test/test-tags.test.ts
index 426f81306d58..8da7b33e497b 100644
--- a/test/cli/test/test-tags.test.ts
+++ b/test/cli/test/test-tags.test.ts
@@ -1255,6 +1255,139 @@ test('multiple filter expressions act as AND', async () => {
`)
})
+test('tags can define meta in config', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test 1', { tags: ['unit'] }, () => {})
+ test('test 2', { tags: ['e2e'] }, () => {})
+ test('test 3', { tags: ['unit', 'slow'] }, () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ tags: [
+ { name: 'unit', meta: { type: 'unit', priority: 1 } },
+ { name: 'e2e', meta: { type: 'e2e', browser: true } },
+ { name: 'slow', meta: { priority: 2, slow: true } },
+ ],
+ },
+ },
+ })
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const [test1, test2, test3] = testModule.children.array() as TestCase[]
+ expect(test1.meta()).toMatchInlineSnapshot(`
+ {
+ "priority": 1,
+ "type": "unit",
+ }
+ `)
+ expect(test2.meta()).toMatchInlineSnapshot(`
+ {
+ "browser": true,
+ "type": "e2e",
+ }
+ `)
+ expect(test3.meta()).toMatchInlineSnapshot(`
+ {
+ "priority": 2,
+ "slow": true,
+ "type": "unit",
+ }
+ `)
+})
+
+test('tag meta is inherited by suite and test meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { tags: ['suite-tag'], meta: { suiteOwn: true } }, () => {
+ test('test', { tags: ['test-tag'], meta: { testOwn: true } }, () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ tags: [
+ { name: 'suite-tag', meta: { fromSuiteTag: 'value' } },
+ { name: 'test-tag', meta: { fromTestTag: 'value' } },
+ ],
+ },
+ },
+ })
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const suite = testModule.children.at(0) as TestSuite
+ const testCase = suite.children.at(0) as TestCase
+ // suite has a tag with metadata, but tags are only applied to tests,
+ // so suites don't get tag metadata
+ expect(suite.meta()).toMatchInlineSnapshot(`
+ {
+ "suiteOwn": true,
+ }
+ `)
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "fromSuiteTag": "value",
+ "fromTestTag": "value",
+ "suiteOwn": true,
+ "testOwn": true,
+ }
+ `)
+})
+
+test('test meta overrides tag meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test', { tags: ['tagged'], meta: { key: 'fromTest', testOnly: true } }, () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ tags: [
+ { name: 'tagged', meta: { key: 'fromTag', tagOnly: true } },
+ ],
+ },
+ },
+ })
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "key": "fromTest",
+ "tagOnly": true,
+ "testOnly": true,
+ }
+ `)
+})
+
+test('multiple tags with meta are merged with priority order', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test', { tags: ['low', 'high'] }, () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ tags: [
+ { name: 'low', priority: 2, meta: { shared: 'low', lowOnly: true } },
+ { name: 'high', priority: 1, meta: { shared: 'high', highOnly: true } },
+ ],
+ },
+ },
+ })
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "highOnly": true,
+ "lowOnly": true,
+ "shared": "high",
+ }
+ `)
+})
+
function getTestTree(builder: (fn: (test: TestCase) => any) => any) {
return builder(test => test.options.tags)
}
@@ -1272,3 +1405,20 @@ function removeUndefined>(obj: T): Partial {
}
return result
}
+
+declare module 'vitest' {
+ interface TaskMeta {
+ type?: string
+ priority?: number
+ browser?: boolean
+ slow?: boolean
+ fromSuiteTag?: string
+ fromTestTag?: string
+ suiteOwn?: boolean
+ testOwn?: boolean
+ tagOnly?: boolean
+ shared?: string
+ lowOnly?: boolean
+ highOnly?: boolean
+ }
+}
diff --git a/test/core/test/custom.test.ts b/test/core/test/custom.test.ts
index 28f08d2c5e5b..b7afeb1f33ad 100644
--- a/test/core/test/custom.test.ts
+++ b/test/core/test/custom.test.ts
@@ -10,6 +10,12 @@ import {
} from 'vitest'
import { Gardener } from '../src/custom/gardener.js'
+declare module 'vitest' {
+ interface TaskMeta {
+ customPropertyToDifferentiateTask?: boolean
+ }
+}
+
// this function will be called, when Vitest collects tasks
const myCustomTask = TestRunner.createChainable(['todo'], function (name: string, fn: () => void) {
TestRunner.getCurrentSuite().task(name, {