[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat: support `meta` in test options (#9535)

authored by

Vladimir and committed by
GitHub
(Jan 29, 2026, 12:24 PM +0100) 7d622e3d 687b633c

+938 -16
+39
docs/api/test.md
··· 170 170 }) 171 171 ``` 172 172 173 + ### meta <Version>4.1.0</Version> {#meta} 174 + 175 + - **Type:** `TaskMeta` 176 + 177 + Attaches custom [metadata](/api/advanced/metadata) available in reporters. 178 + 179 + ::: warning 180 + Vitest merges top-level properties inherited from suites or tags. However, it does not perform a deep merge of nested objects. 181 + 182 + ```ts 183 + import { describe, test } from 'vitest' 184 + 185 + describe( 186 + 'nested meta', 187 + { 188 + meta: { 189 + nested: { object: true, array: false }, 190 + }, 191 + }, 192 + () => { 193 + test( 194 + 'overrides part of meta', 195 + { 196 + meta: { 197 + nested: { object: false } 198 + }, 199 + }, 200 + ({ task }) => { 201 + // task.meta === { nested: { object: false } } 202 + // notice array got lost because "nested" object was overriden 203 + } 204 + ) 205 + } 206 + ) 207 + ``` 208 + 209 + Prefer using non-nested meta, if possible. 210 + ::: 211 + 173 212 ### concurrent 174 213 175 214 - **Type:** `boolean`
+7 -1
docs/api/advanced/test-case.md
··· 143 143 }) 144 144 ``` 145 145 146 - If the test did not finish running yet, the meta will be an empty object. 146 + If the test did not finish running yet, the meta will be an empty object, unless it has static meta: 147 + 148 + ```ts 149 + test('the validation works correctly', { meta: { decorated: true } }) 150 + ``` 151 + 152 + Since Vitest 4.1, Vitest inherits [`meta`](/api/advanced/test-suite#meta) property defined on the [suite](/api/advanced/test-suite). 147 153 148 154 ## result 149 155
+8 -7
docs/api/advanced/test-suite.md
··· 198 198 function meta(): TaskMeta 199 199 ``` 200 200 201 - 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: 201 + 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: 202 202 203 - ```ts {7,12} 203 + ```ts {7,10} 204 204 import { describe, test, TestRunner } from 'vitest' 205 205 206 - describe('the validation works correctly', () => { 207 - // assign "decorated" during collection 208 - const { suite } = TestRunner.getCurrentSuite() 209 - suite!.meta.decorated = true 210 - 206 + describe('the validation works correctly', { meta: { decorated: true } }, () => { 211 207 test('some test', ({ task }) => { 212 208 // assign "decorated" during test run, it will be available 213 209 // only in onTestCaseReady hook 214 210 task.suite.meta.decorated = false 211 + 212 + // tests inherit suite's metadata 213 + task.meta.decorated === true 215 214 }) 216 215 }) 217 216 ``` 217 + 218 + Note that suite metadata will be inherited by tests since Vitest 4.1. 218 219 219 220 :::tip 220 221 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.
+25 -4
packages/runner/src/suite.ts
··· 331 331 // higher priority should be last, run 1, 2, 3, ... etc 332 332 .sort((tag1, tag2) => (tag2.priority ?? POSITIVE_INFINITY) - (tag1.priority ?? POSITIVE_INFINITY)) 333 333 .reduce((acc, tag) => { 334 - const { name, description, priority, ...options } = tag 334 + const { name, description, priority, meta, ...options } = tag 335 335 Object.assign(acc, options) 336 + if (meta) { 337 + acc.meta = Object.assign(acc.meta ?? Object.create(null), meta) 338 + } 336 339 return acc 337 340 }, {} as TestOptions) 338 341 342 + const testOwnMeta = options.meta 339 343 options = { 340 344 ...tagsOptions, 341 345 ...options, 342 346 } 343 347 const timeout = options.timeout ?? runner.config.testTimeout 348 + const parentMeta = currentSuite?.meta 349 + const tagMeta = tagsOptions.meta 350 + const testMeta = Object.create(null) 351 + if (tagMeta) { 352 + Object.assign(testMeta, tagMeta) 353 + } 354 + if (parentMeta) { 355 + Object.assign(testMeta, parentMeta) 356 + } 357 + if (testOwnMeta) { 358 + Object.assign(testMeta, testOwnMeta) 359 + } 344 360 const task: Test = { 345 361 id: '', 346 362 name, ··· 365 381 : options.todo 366 382 ? 'todo' 367 383 : 'run', 368 - meta: options.meta ?? Object.create(null), 384 + meta: testMeta, 369 385 annotations: [], 370 386 artifacts: [], 371 387 tags: testTags, ··· 513 529 file: (currentSuite?.file ?? collectorContext.currentSuite?.file)!, 514 530 shuffle: suiteOptions?.shuffle, 515 531 tasks: [], 516 - meta: Object.create(null), 532 + meta: suiteOptions?.meta ?? Object.create(null), 517 533 concurrent: suiteOptions?.concurrent, 518 534 tags: unique([...parentTask?.tags || [], ...suiteTags]), 519 535 } ··· 604 620 const isConcurrentSpecified = options.concurrent || this.concurrent || options.sequential === false 605 621 const isSequentialSpecified = options.sequential || this.sequential || options.concurrent === false 606 622 623 + const { meta: parentMeta, ...parentOptions } = currentSuite?.options || {} 607 624 // inherit options from current suite 608 625 options = { 609 - ...currentSuite?.options, 626 + ...parentOptions, 610 627 ...options, 611 628 } 612 629 ··· 636 653 } 637 654 if (isSequential != null) { 638 655 options.sequential = isSequential && !isConcurrent 656 + } 657 + 658 + if (parentMeta) { 659 + options.meta = Object.assign(Object.create(null), parentMeta, options.meta) 639 660 } 640 661 641 662 return createSuiteCollector(
+699
test/cli/test/test-meta.test.ts
··· 1 + import type { TestCase, TestSuite } from 'vitest/node' 2 + import { runInlineTests } from '#test-utils' 3 + import { expect, test } from 'vitest' 4 + 5 + test('meta can be defined on test options', async () => { 6 + const { stderr, ctx } = await runInlineTests({ 7 + 'basic.test.js': ` 8 + test('test 1', { meta: { custom: 'value', count: 42 } }, () => {}) 9 + `, 10 + 'vitest.config.js': { 11 + test: { 12 + globals: true, 13 + }, 14 + }, 15 + }) 16 + 17 + expect(stderr).toBe('') 18 + const testModule = ctx!.state.getTestModules()[0] 19 + const testCase = testModule.children.at(0) as TestCase 20 + expect(testCase.meta()).toMatchInlineSnapshot(` 21 + { 22 + "count": 42, 23 + "custom": "value", 24 + } 25 + `) 26 + }) 27 + 28 + test('meta can be defined on suite options', async () => { 29 + const { stderr, ctx } = await runInlineTests({ 30 + 'basic.test.js': ` 31 + describe('suite', { meta: { suiteKey: 'suiteValue' } }, () => { 32 + test('test 1', () => {}) 33 + }) 34 + `, 35 + 'vitest.config.js': { 36 + test: { 37 + globals: true, 38 + }, 39 + }, 40 + }) 41 + 42 + expect(stderr).toBe('') 43 + const testModule = ctx!.state.getTestModules()[0] 44 + const testSuite = testModule.children.at(0) as TestSuite 45 + expect(testSuite.meta()).toMatchInlineSnapshot(` 46 + { 47 + "suiteKey": "suiteValue", 48 + } 49 + `) 50 + }) 51 + 52 + test('test inherits meta from parent suite', async () => { 53 + const { stderr, ctx } = await runInlineTests({ 54 + 'basic.test.js': ` 55 + describe('suite', { meta: { inherited: true, level: 'suite' } }, () => { 56 + test('test 1', () => {}) 57 + }) 58 + `, 59 + 'vitest.config.js': { 60 + test: { 61 + globals: true, 62 + }, 63 + }, 64 + }) 65 + 66 + expect(stderr).toBe('') 67 + const testModule = ctx!.state.getTestModules()[0] 68 + const testSuite = testModule.children.at(0) as TestSuite 69 + const testCase = testSuite.children.at(0) as TestCase 70 + expect(testCase.meta()).toMatchInlineSnapshot(` 71 + { 72 + "inherited": true, 73 + "level": "suite", 74 + } 75 + `) 76 + }) 77 + 78 + test('test meta overrides inherited suite meta', async () => { 79 + const { stderr, ctx } = await runInlineTests({ 80 + 'basic.test.js': ` 81 + describe('suite', { meta: { shared: 'fromSuite', suiteOnly: true } }, () => { 82 + test('test 1', { meta: { shared: 'fromTest', testOnly: 123 } }, () => {}) 83 + }) 84 + `, 85 + 'vitest.config.js': { 86 + test: { 87 + globals: true, 88 + }, 89 + }, 90 + }) 91 + 92 + expect(stderr).toBe('') 93 + const testModule = ctx!.state.getTestModules()[0] 94 + const testSuite = testModule.children.at(0) as TestSuite 95 + const testCase = testSuite.children.at(0) as TestCase 96 + expect(testCase.meta()).toMatchInlineSnapshot(` 97 + { 98 + "shared": "fromTest", 99 + "suiteOnly": true, 100 + "testOnly": 123, 101 + } 102 + `) 103 + }) 104 + 105 + test('nested suites inherit meta from parent suites', async () => { 106 + const { stderr, ctx } = await runInlineTests({ 107 + 'basic.test.js': ` 108 + describe('outer', { meta: { outer: true } }, () => { 109 + describe('inner', { meta: { inner: true } }, () => { 110 + test('test 1', () => {}) 111 + }) 112 + }) 113 + `, 114 + 'vitest.config.js': { 115 + test: { 116 + globals: true, 117 + }, 118 + }, 119 + }) 120 + 121 + expect(stderr).toBe('') 122 + const testModule = ctx!.state.getTestModules()[0] 123 + const outerSuite = testModule.children.at(0) as TestSuite 124 + const innerSuite = outerSuite.children.at(0) as TestSuite 125 + const testCase = innerSuite.children.at(0) as TestCase 126 + 127 + expect(outerSuite.meta()).toMatchInlineSnapshot(` 128 + { 129 + "outer": true, 130 + } 131 + `) 132 + expect(innerSuite.meta()).toMatchInlineSnapshot(` 133 + { 134 + "inner": true, 135 + "outer": true, 136 + } 137 + `) 138 + expect(testCase.meta()).toMatchInlineSnapshot(` 139 + { 140 + "inner": true, 141 + "outer": true, 142 + } 143 + `) 144 + }) 145 + 146 + test('deeply nested meta inheritance with overrides', async () => { 147 + const { stderr, ctx } = await runInlineTests({ 148 + 'basic.test.js': ` 149 + describe('level1', { meta: { level: 1, a: 'first' } }, () => { 150 + describe('level2', { meta: { level: 2, b: 'second' } }, () => { 151 + describe('level3', { meta: { level: 3, a: 'override' } }, () => { 152 + test('test 1', { meta: { level: 4 } }, () => {}) 153 + }) 154 + }) 155 + }) 156 + `, 157 + 'vitest.config.js': { 158 + test: { 159 + globals: true, 160 + }, 161 + }, 162 + }) 163 + 164 + expect(stderr).toBe('') 165 + const testModule = ctx!.state.getTestModules()[0] 166 + const level1 = testModule.children.at(0) as TestSuite 167 + const level2 = level1.children.at(0) as TestSuite 168 + const level3 = level2.children.at(0) as TestSuite 169 + const testCase = level3.children.at(0) as TestCase 170 + 171 + expect(level1.meta()).toMatchInlineSnapshot(` 172 + { 173 + "a": "first", 174 + "level": 1, 175 + } 176 + `) 177 + expect(level2.meta()).toMatchInlineSnapshot(` 178 + { 179 + "a": "first", 180 + "b": "second", 181 + "level": 2, 182 + } 183 + `) 184 + expect(level3.meta()).toMatchInlineSnapshot(` 185 + { 186 + "a": "override", 187 + "b": "second", 188 + "level": 3, 189 + } 190 + `) 191 + expect(testCase.meta()).toMatchInlineSnapshot(` 192 + { 193 + "a": "override", 194 + "b": "second", 195 + "level": 4, 196 + } 197 + `) 198 + }) 199 + 200 + test('meta is accessible from task.meta inside tests', async () => { 201 + const { stderr, stdout } = await runInlineTests({ 202 + 'basic.test.js': ` 203 + describe('suite', { meta: { suiteKey: 'inherited' } }, () => { 204 + test('test 1', { meta: { testKey: 'own' } }, ({ task }) => { 205 + console.log('META:', JSON.stringify(task.meta)) 206 + }) 207 + }) 208 + `, 209 + 'vitest.config.js': { 210 + test: { 211 + globals: true, 212 + }, 213 + }, 214 + }) 215 + 216 + expect(stderr).toBe('') 217 + const metaLine = stdout.split('\n').find(line => line.startsWith('META:')) 218 + expect(metaLine).toBeDefined() 219 + expect(JSON.parse(metaLine!.slice('META:'.length))).toMatchInlineSnapshot(` 220 + { 221 + "suiteKey": "inherited", 222 + "testKey": "own", 223 + } 224 + `) 225 + }) 226 + 227 + test('sibling tests have independent meta', async () => { 228 + const { stderr, ctx } = await runInlineTests({ 229 + 'basic.test.js': ` 230 + describe('suite', { meta: { shared: 'parent' } }, () => { 231 + test('test 1', { meta: { id: 1 } }, () => {}) 232 + test('test 2', { meta: { id: 2 } }, () => {}) 233 + test('test 3', () => {}) 234 + }) 235 + `, 236 + 'vitest.config.js': { 237 + test: { 238 + globals: true, 239 + }, 240 + }, 241 + }) 242 + 243 + expect(stderr).toBe('') 244 + const testModule = ctx!.state.getTestModules()[0] 245 + const testSuite = testModule.children.at(0) as TestSuite 246 + const [test1, test2, test3] = testSuite.children.array() as TestCase[] 247 + 248 + expect(test1.meta()).toMatchInlineSnapshot(` 249 + { 250 + "id": 1, 251 + "shared": "parent", 252 + } 253 + `) 254 + expect(test2.meta()).toMatchInlineSnapshot(` 255 + { 256 + "id": 2, 257 + "shared": "parent", 258 + } 259 + `) 260 + expect(test3.meta()).toMatchInlineSnapshot(` 261 + { 262 + "shared": "parent", 263 + } 264 + `) 265 + }) 266 + 267 + test('sibling suites have independent meta', async () => { 268 + const { stderr, ctx } = await runInlineTests({ 269 + 'basic.test.js': ` 270 + describe('suite1', { meta: { suite: 1 } }, () => { 271 + test('test 1', () => {}) 272 + }) 273 + describe('suite2', { meta: { suite: 2 } }, () => { 274 + test('test 2', () => {}) 275 + }) 276 + `, 277 + 'vitest.config.js': { 278 + test: { 279 + globals: true, 280 + }, 281 + }, 282 + }) 283 + 284 + expect(stderr).toBe('') 285 + const testModule = ctx!.state.getTestModules()[0] 286 + const [suite1, suite2] = testModule.children.array() as TestSuite[] 287 + const test1 = suite1.children.at(0) as TestCase 288 + const test2 = suite2.children.at(0) as TestCase 289 + 290 + expect(suite1.meta()).toMatchInlineSnapshot(` 291 + { 292 + "suite": 1, 293 + } 294 + `) 295 + expect(suite2.meta()).toMatchInlineSnapshot(` 296 + { 297 + "suite": 2, 298 + } 299 + `) 300 + expect(test1.meta()).toMatchInlineSnapshot(` 301 + { 302 + "suite": 1, 303 + } 304 + `) 305 + expect(test2.meta()).toMatchInlineSnapshot(` 306 + { 307 + "suite": 2, 308 + } 309 + `) 310 + }) 311 + 312 + test('test without parent suite has empty meta', async () => { 313 + const { stderr, ctx } = await runInlineTests({ 314 + 'basic.test.js': ` 315 + test('test 1', () => {}) 316 + `, 317 + 'vitest.config.js': { 318 + test: { 319 + globals: true, 320 + }, 321 + }, 322 + }) 323 + 324 + expect(stderr).toBe('') 325 + const testModule = ctx!.state.getTestModules()[0] 326 + const testCase = testModule.children.at(0) as TestCase 327 + expect(testCase.meta()).toMatchInlineSnapshot(`{}`) 328 + }) 329 + 330 + test('test.each works with meta', async () => { 331 + const { stderr, ctx } = await runInlineTests({ 332 + 'basic.test.js': ` 333 + describe('suite', { meta: { feature: 'each' } }, () => { 334 + test.each([1, 2, 3])('test %i', { meta: { eachTest: true } }, () => {}) 335 + }) 336 + `, 337 + 'vitest.config.js': { 338 + test: { 339 + globals: true, 340 + }, 341 + }, 342 + }) 343 + 344 + expect(stderr).toBe('') 345 + const testModule = ctx!.state.getTestModules()[0] 346 + const testSuite = testModule.children.at(0) as TestSuite 347 + const tests = testSuite.children.array() as TestCase[] 348 + 349 + expect(tests).toHaveLength(3) 350 + for (const test of tests) { 351 + expect(test.meta()).toMatchInlineSnapshot(` 352 + { 353 + "eachTest": true, 354 + "feature": "each", 355 + } 356 + `) 357 + } 358 + }) 359 + 360 + test('describe.each works with meta', async () => { 361 + const { stderr, ctx } = await runInlineTests({ 362 + 'basic.test.js': ` 363 + describe.each([1, 2])('suite %i', { meta: { dynamic: true } }, () => { 364 + test('test', () => {}) 365 + }) 366 + `, 367 + 'vitest.config.js': { 368 + test: { 369 + globals: true, 370 + }, 371 + }, 372 + }) 373 + 374 + expect(stderr).toBe('') 375 + const testModule = ctx!.state.getTestModules()[0] 376 + const [suite1, suite2] = testModule.children.array() as TestSuite[] 377 + 378 + expect(suite1.meta()).toMatchInlineSnapshot(` 379 + { 380 + "dynamic": true, 381 + } 382 + `) 383 + expect(suite2.meta()).toMatchInlineSnapshot(` 384 + { 385 + "dynamic": true, 386 + } 387 + `) 388 + expect((suite1.children.at(0) as TestCase).meta()).toMatchInlineSnapshot(` 389 + { 390 + "dynamic": true, 391 + } 392 + `) 393 + }) 394 + 395 + test('concurrent tests have independent meta', async () => { 396 + const { stderr, ctx } = await runInlineTests({ 397 + 'basic.test.js': ` 398 + describe('suite', { meta: { shared: true } }, () => { 399 + test.concurrent('test 1', { meta: { id: 1 } }, () => {}) 400 + test.concurrent('test 2', { meta: { id: 2 } }, () => {}) 401 + }) 402 + `, 403 + 'vitest.config.js': { 404 + test: { 405 + globals: true, 406 + }, 407 + }, 408 + }) 409 + 410 + expect(stderr).toBe('') 411 + const testModule = ctx!.state.getTestModules()[0] 412 + const testSuite = testModule.children.at(0) as TestSuite 413 + const [test1, test2] = testSuite.children.array() as TestCase[] 414 + 415 + expect(test1.meta()).toMatchInlineSnapshot(` 416 + { 417 + "id": 1, 418 + "shared": true, 419 + } 420 + `) 421 + expect(test2.meta()).toMatchInlineSnapshot(` 422 + { 423 + "id": 2, 424 + "shared": true, 425 + } 426 + `) 427 + }) 428 + 429 + test('meta with complex values', async () => { 430 + const { stderr, ctx } = await runInlineTests({ 431 + 'basic.test.js': ` 432 + test('test 1', { 433 + meta: { 434 + nested: { a: { b: { c: 1 } } }, 435 + array: [1, 2, 3], 436 + nullValue: null, 437 + boolTrue: true, 438 + boolFalse: false, 439 + num: 42.5, 440 + } 441 + }, () => {}) 442 + `, 443 + 'vitest.config.js': { 444 + test: { 445 + globals: true, 446 + }, 447 + }, 448 + }) 449 + 450 + expect(stderr).toBe('') 451 + const testModule = ctx!.state.getTestModules()[0] 452 + const testCase = testModule.children.at(0) as TestCase 453 + expect(testCase.meta()).toMatchInlineSnapshot(` 454 + { 455 + "array": [ 456 + 1, 457 + 2, 458 + 3, 459 + ], 460 + "boolFalse": false, 461 + "boolTrue": true, 462 + "nested": { 463 + "a": { 464 + "b": { 465 + "c": 1, 466 + }, 467 + }, 468 + }, 469 + "nullValue": null, 470 + "num": 42.5, 471 + } 472 + `) 473 + }) 474 + 475 + test('meta works with test modifiers (skip, only, todo)', async () => { 476 + const { stderr, ctx } = await runInlineTests({ 477 + 'basic.test.js': ` 478 + test.skip('skipped test', { meta: { status: 'skipped' } }, () => {}) 479 + test.todo('todo test', { meta: { status: 'todo' } }) 480 + `, 481 + 'vitest.config.js': { 482 + test: { 483 + globals: true, 484 + }, 485 + }, 486 + }) 487 + 488 + expect(stderr).toBe('') 489 + const testModule = ctx!.state.getTestModules()[0] 490 + const [skipped, todo] = testModule.children.array() as TestCase[] 491 + 492 + expect(skipped.meta()).toMatchInlineSnapshot(` 493 + { 494 + "status": "skipped", 495 + } 496 + `) 497 + expect(todo.meta()).toMatchInlineSnapshot(` 498 + { 499 + "status": "todo", 500 + } 501 + `) 502 + }) 503 + 504 + test('meta works with test.fails', async () => { 505 + const { stderr, ctx } = await runInlineTests({ 506 + 'basic.test.js': ` 507 + test.fails('failing test', { meta: { expectFailure: true } }, () => { 508 + throw new Error('Expected error') 509 + }) 510 + `, 511 + 'vitest.config.js': { 512 + test: { 513 + globals: true, 514 + }, 515 + }, 516 + }) 517 + 518 + expect(stderr).toBe('') 519 + const testModule = ctx!.state.getTestModules()[0] 520 + const testCase = testModule.children.at(0) as TestCase 521 + expect(testCase.meta()).toMatchInlineSnapshot(` 522 + { 523 + "expectFailure": true, 524 + } 525 + `) 526 + expect(testCase.result().state).toBe('passed') 527 + }) 528 + 529 + test('suite without meta does not inherit to tests', async () => { 530 + const { stderr, ctx } = await runInlineTests({ 531 + 'basic.test.js': ` 532 + describe('suite without meta', () => { 533 + test('test with meta', { meta: { ownMeta: true } }, () => {}) 534 + test('test without meta', () => {}) 535 + }) 536 + `, 537 + 'vitest.config.js': { 538 + test: { 539 + globals: true, 540 + }, 541 + }, 542 + }) 543 + 544 + expect(stderr).toBe('') 545 + const testModule = ctx!.state.getTestModules()[0] 546 + const testSuite = testModule.children.at(0) as TestSuite 547 + const [withMeta, withoutMeta] = testSuite.children.array() as TestCase[] 548 + 549 + expect(testSuite.meta()).toMatchInlineSnapshot(`{}`) 550 + expect(withMeta.meta()).toMatchInlineSnapshot(` 551 + { 552 + "ownMeta": true, 553 + } 554 + `) 555 + expect(withoutMeta.meta()).toMatchInlineSnapshot(`{}`) 556 + }) 557 + 558 + test('meta does not mutate parent when child overrides', async () => { 559 + const { stderr, ctx } = await runInlineTests({ 560 + 'basic.test.js': ` 561 + describe('parent', { meta: { key: 'parent', parentOnly: true } }, () => { 562 + describe('child', { meta: { key: 'child', childOnly: true } }, () => { 563 + test('test', () => {}) 564 + }) 565 + test('sibling test', () => {}) 566 + }) 567 + `, 568 + 'vitest.config.js': { 569 + test: { 570 + globals: true, 571 + }, 572 + }, 573 + }) 574 + 575 + expect(stderr).toBe('') 576 + const testModule = ctx!.state.getTestModules()[0] 577 + const parent = testModule.children.at(0) as TestSuite 578 + const child = parent.children.at(0) as TestSuite 579 + const siblingTest = parent.children.at(1) as TestCase 580 + 581 + expect(parent.meta()).toMatchInlineSnapshot(` 582 + { 583 + "key": "parent", 584 + "parentOnly": true, 585 + } 586 + `) 587 + expect(child.meta()).toMatchInlineSnapshot(` 588 + { 589 + "childOnly": true, 590 + "key": "child", 591 + "parentOnly": true, 592 + } 593 + `) 594 + expect(siblingTest.meta()).toMatchInlineSnapshot(` 595 + { 596 + "key": "parent", 597 + "parentOnly": true, 598 + } 599 + `) 600 + }) 601 + 602 + test('meta with test.for', async () => { 603 + const { stderr, ctx } = await runInlineTests({ 604 + 'basic.test.js': ` 605 + describe('suite', { meta: { fromSuite: true } }, () => { 606 + test.for([ 607 + { input: 1, expected: 2 }, 608 + { input: 2, expected: 4 }, 609 + ])('test $input', { meta: { forTest: true } }, ({ input, expected }) => { 610 + expect(input * 2).toBe(expected) 611 + }) 612 + }) 613 + `, 614 + 'vitest.config.js': { 615 + test: { 616 + globals: true, 617 + }, 618 + }, 619 + }) 620 + 621 + expect(stderr).toBe('') 622 + const testModule = ctx!.state.getTestModules()[0] 623 + const testSuite = testModule.children.at(0) as TestSuite 624 + const tests = testSuite.children.array() as TestCase[] 625 + 626 + expect(tests).toHaveLength(2) 627 + for (const test of tests) { 628 + expect(test.meta()).toMatchInlineSnapshot(` 629 + { 630 + "forTest": true, 631 + "fromSuite": true, 632 + } 633 + `) 634 + } 635 + }) 636 + 637 + test('empty meta object is allowed', async () => { 638 + const { stderr, ctx } = await runInlineTests({ 639 + 'basic.test.js': ` 640 + describe('suite', { meta: {} }, () => { 641 + test('test', { meta: {} }, () => {}) 642 + }) 643 + `, 644 + 'vitest.config.js': { 645 + test: { 646 + globals: true, 647 + }, 648 + }, 649 + }) 650 + 651 + expect(stderr).toBe('') 652 + const testModule = ctx!.state.getTestModules()[0] 653 + const testSuite = testModule.children.at(0) as TestSuite 654 + const testCase = testSuite.children.at(0) as TestCase 655 + 656 + expect(testSuite.meta()).toMatchInlineSnapshot(`{}`) 657 + expect(testCase.meta()).toMatchInlineSnapshot(`{}`) 658 + }) 659 + 660 + test('meta inheritance across multiple files', async () => { 661 + const { stderr, ctx } = await runInlineTests({ 662 + 'file1.test.js': ` 663 + describe('suite in file1', { meta: { file: 1 } }, () => { 664 + test('test 1', () => {}) 665 + }) 666 + `, 667 + 'file2.test.js': ` 668 + describe('suite in file2', { meta: { file: 2 } }, () => { 669 + test('test 2', () => {}) 670 + }) 671 + `, 672 + 'vitest.config.js': { 673 + test: { 674 + globals: true, 675 + }, 676 + }, 677 + }) 678 + 679 + expect(stderr).toBe('') 680 + const testModules = ctx!.state.getTestModules() 681 + const file1Module = testModules.find(m => m.moduleId.includes('file1'))! 682 + const file2Module = testModules.find(m => m.moduleId.includes('file2'))! 683 + 684 + const suite1 = file1Module.children.at(0) as TestSuite 685 + const suite2 = file2Module.children.at(0) as TestSuite 686 + const test1 = suite1.children.at(0) as TestCase 687 + const test2 = suite2.children.at(0) as TestCase 688 + 689 + expect(test1.meta()).toMatchInlineSnapshot(` 690 + { 691 + "file": 1, 692 + } 693 + `) 694 + expect(test2.meta()).toMatchInlineSnapshot(` 695 + { 696 + "file": 2, 697 + } 698 + `) 699 + })
+150
test/cli/test/test-tags.test.ts
··· 1255 1255 `) 1256 1256 }) 1257 1257 1258 + test('tags can define meta in config', async () => { 1259 + const { stderr, ctx } = await runInlineTests({ 1260 + 'basic.test.js': ` 1261 + test('test 1', { tags: ['unit'] }, () => {}) 1262 + test('test 2', { tags: ['e2e'] }, () => {}) 1263 + test('test 3', { tags: ['unit', 'slow'] }, () => {}) 1264 + `, 1265 + 'vitest.config.js': { 1266 + test: { 1267 + globals: true, 1268 + tags: [ 1269 + { name: 'unit', meta: { type: 'unit', priority: 1 } }, 1270 + { name: 'e2e', meta: { type: 'e2e', browser: true } }, 1271 + { name: 'slow', meta: { priority: 2, slow: true } }, 1272 + ], 1273 + }, 1274 + }, 1275 + }) 1276 + expect(stderr).toBe('') 1277 + const testModule = ctx!.state.getTestModules()[0] 1278 + const [test1, test2, test3] = testModule.children.array() as TestCase[] 1279 + expect(test1.meta()).toMatchInlineSnapshot(` 1280 + { 1281 + "priority": 1, 1282 + "type": "unit", 1283 + } 1284 + `) 1285 + expect(test2.meta()).toMatchInlineSnapshot(` 1286 + { 1287 + "browser": true, 1288 + "type": "e2e", 1289 + } 1290 + `) 1291 + expect(test3.meta()).toMatchInlineSnapshot(` 1292 + { 1293 + "priority": 2, 1294 + "slow": true, 1295 + "type": "unit", 1296 + } 1297 + `) 1298 + }) 1299 + 1300 + test('tag meta is inherited by suite and test meta', async () => { 1301 + const { stderr, ctx } = await runInlineTests({ 1302 + 'basic.test.js': ` 1303 + describe('suite', { tags: ['suite-tag'], meta: { suiteOwn: true } }, () => { 1304 + test('test', { tags: ['test-tag'], meta: { testOwn: true } }, () => {}) 1305 + }) 1306 + `, 1307 + 'vitest.config.js': { 1308 + test: { 1309 + globals: true, 1310 + tags: [ 1311 + { name: 'suite-tag', meta: { fromSuiteTag: 'value' } }, 1312 + { name: 'test-tag', meta: { fromTestTag: 'value' } }, 1313 + ], 1314 + }, 1315 + }, 1316 + }) 1317 + expect(stderr).toBe('') 1318 + const testModule = ctx!.state.getTestModules()[0] 1319 + const suite = testModule.children.at(0) as TestSuite 1320 + const testCase = suite.children.at(0) as TestCase 1321 + // suite has a tag with metadata, but tags are only applied to tests, 1322 + // so suites don't get tag metadata 1323 + expect(suite.meta()).toMatchInlineSnapshot(` 1324 + { 1325 + "suiteOwn": true, 1326 + } 1327 + `) 1328 + expect(testCase.meta()).toMatchInlineSnapshot(` 1329 + { 1330 + "fromSuiteTag": "value", 1331 + "fromTestTag": "value", 1332 + "suiteOwn": true, 1333 + "testOwn": true, 1334 + } 1335 + `) 1336 + }) 1337 + 1338 + test('test meta overrides tag meta', async () => { 1339 + const { stderr, ctx } = await runInlineTests({ 1340 + 'basic.test.js': ` 1341 + test('test', { tags: ['tagged'], meta: { key: 'fromTest', testOnly: true } }, () => {}) 1342 + `, 1343 + 'vitest.config.js': { 1344 + test: { 1345 + globals: true, 1346 + tags: [ 1347 + { name: 'tagged', meta: { key: 'fromTag', tagOnly: true } }, 1348 + ], 1349 + }, 1350 + }, 1351 + }) 1352 + expect(stderr).toBe('') 1353 + const testModule = ctx!.state.getTestModules()[0] 1354 + const testCase = testModule.children.at(0) as TestCase 1355 + expect(testCase.meta()).toMatchInlineSnapshot(` 1356 + { 1357 + "key": "fromTest", 1358 + "tagOnly": true, 1359 + "testOnly": true, 1360 + } 1361 + `) 1362 + }) 1363 + 1364 + test('multiple tags with meta are merged with priority order', async () => { 1365 + const { stderr, ctx } = await runInlineTests({ 1366 + 'basic.test.js': ` 1367 + test('test', { tags: ['low', 'high'] }, () => {}) 1368 + `, 1369 + 'vitest.config.js': { 1370 + test: { 1371 + globals: true, 1372 + tags: [ 1373 + { name: 'low', priority: 2, meta: { shared: 'low', lowOnly: true } }, 1374 + { name: 'high', priority: 1, meta: { shared: 'high', highOnly: true } }, 1375 + ], 1376 + }, 1377 + }, 1378 + }) 1379 + expect(stderr).toBe('') 1380 + const testModule = ctx!.state.getTestModules()[0] 1381 + const testCase = testModule.children.at(0) as TestCase 1382 + expect(testCase.meta()).toMatchInlineSnapshot(` 1383 + { 1384 + "highOnly": true, 1385 + "lowOnly": true, 1386 + "shared": "high", 1387 + } 1388 + `) 1389 + }) 1390 + 1258 1391 function getTestTree(builder: (fn: (test: TestCase) => any) => any) { 1259 1392 return builder(test => test.options.tags) 1260 1393 } ··· 1271 1404 } 1272 1405 } 1273 1406 return result 1407 + } 1408 + 1409 + declare module 'vitest' { 1410 + interface TaskMeta { 1411 + type?: string 1412 + priority?: number 1413 + browser?: boolean 1414 + slow?: boolean 1415 + fromSuiteTag?: string 1416 + fromTestTag?: string 1417 + suiteOwn?: boolean 1418 + testOwn?: boolean 1419 + tagOnly?: boolean 1420 + shared?: string 1421 + lowOnly?: boolean 1422 + highOnly?: boolean 1423 + } 1274 1424 }
+6
test/core/test/custom.test.ts
··· 10 10 } from 'vitest' 11 11 import { Gardener } from '../src/custom/gardener.js' 12 12 13 + declare module 'vitest' { 14 + interface TaskMeta { 15 + customPropertyToDifferentiateTask?: boolean 16 + } 17 + } 18 + 13 19 // this function will be called, when Vitest collects tasks 14 20 const myCustomTask = TestRunner.createChainable(['todo'], function (name: string, fn: () => void) { 15 21 TestRunner.getCurrentSuite().task(name, {
+4 -4
packages/runner/src/types/tasks.ts
··· 570 570 tags?: keyof TestTags extends never 571 571 ? string[] | string 572 572 : TestTags[keyof TestTags] | TestTags[keyof TestTags][] 573 + /** 574 + * Custom test metadata available to reporters. 575 + */ 576 + meta?: Partial<TaskMeta> 573 577 } 574 578 575 579 export interface TestTags {} ··· 735 739 * Whether the task was produced with `.each()` method. 736 740 */ 737 741 each?: boolean 738 - /** 739 - * Custom metadata for the task that will be assigned to `task.meta`. 740 - */ 741 - meta?: Record<string, unknown> 742 742 /** 743 743 * Task fixtures. 744 744 */