From 7d622e3d16f3f7df3e5aa8e580db5df41a3b8091 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 29 Jan 2026 12:24:56 +0100 Subject: [PATCH] feat: support `meta` in test options (#9535) --- docs/api/advanced/test-case.md | 8 +- docs/api/advanced/test-suite.md | 15 +- docs/api/test.md | 39 ++ packages/runner/src/suite.ts | 29 +- packages/runner/src/types/tasks.ts | 8 +- test/cli/test/test-meta.test.ts | 699 +++++++++++++++++++++++++++++ test/cli/test/test-tags.test.ts | 150 +++++++ test/core/test/custom.test.ts | 6 + 8 files changed, 938 insertions(+), 16 deletions(-) create mode 100644 test/cli/test/test-meta.test.ts diff --git a/docs/api/advanced/test-case.md b/docs/api/advanced/test-case.md index c9601650b..b91d14b46 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 56abb67ec..f8aa837fc 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 84c108bbd..93d6532bb 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 911479b88..f56ee8426 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 948e358c6..2fbffb924 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 000000000..c22b49d8c --- /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 426f81306..8da7b33e4 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 28f08d2c5..b7afeb1f3 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, {