[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(experimental): expose `matchesTagsFilter` to test if the current filter matches tags (#9913)

authored by

Vladimir and committed by
GitHub
(Mar 23, 2026, 2:17 PM +0100) eec53d9f d4f924eb

+415 -8
+17
docs/guide/test-tags.md
··· 300 300 # Run tests that match (unit OR e2e) AND are NOT slow 301 301 vitest --tags-filter="unit || e2e" --tags-filter="!slow" 302 302 ``` 303 + 304 + ### Checking Tags Filter at Runtime 305 + 306 + You can use `TestRunner.matchesTagsFilter` (since Vitest 4.1.1) to check whether the current tags filter matches a set of tags. This is useful for conditionally running expensive setup logic only when relevant tests are included: 307 + 308 + ```ts 309 + import { beforeAll, TestRunner } from 'vitest' 310 + 311 + beforeAll(async () => { 312 + // Seed database when "vitest --tags-filter db" is used 313 + if (TestRunner.matchesTagsFilter(['db'])) { 314 + await seedDatabase() 315 + } 316 + }) 317 + ``` 318 + 319 + The method accepts an array of tags and returns `true` if the current `--tags-filter` would include a test with those tags. If no tags filter is active, it always returns `true`.
+2
packages/runner/src/collect.ts
··· 36 36 'collect_spec', 37 37 { 'code.file.path': filepath }, 38 38 async () => { 39 + runner._currentSpecification = typeof spec === 'string' ? { filepath: spec } : spec 40 + 39 41 const testLocations = typeof spec === 'string' ? undefined : spec.testLocations 40 42 const testNamePattern = typeof spec === 'string' ? undefined : spec.testNamePattern 41 43 const testIds = typeof spec === 'string' ? undefined : spec.testIds
+8 -6
packages/runner/src/types/runner.ts
··· 55 55 // file can be marked via a jsdoc comment to have tags, 56 56 // these are _not_ tags to filter tests by 57 57 fileTags?: string[] 58 - testLocations: number[] | undefined 59 - testNamePattern: RegExp | undefined 60 - testTagsFilter: string[] | undefined 61 - testIds: string[] | undefined 58 + testLocations?: number[] | undefined 59 + testNamePattern?: RegExp | undefined 60 + testTagsFilter?: string[] | undefined 61 + testIds?: string[] | undefined 62 62 } 63 63 64 64 export interface TestTagDefinition extends Omit<TestOptions, 'tags' | 'shuffle'> { ··· 231 231 // eslint-disable-next-line ts/method-signature-style 232 232 trace?<T>(name: string, attributes: Record<string, any>, cb: () => T): T 233 233 234 - /** @private */ 234 + /** @internal */ 235 + _currentSpecification?: FileSpecification | undefined 236 + /** @internal */ 235 237 _currentTaskStartTime?: number 236 - /** @private */ 238 + /** @internal */ 237 239 _currentTaskTimeout?: number 238 240 }
+1 -1
packages/runner/src/utils/index.ts
··· 10 10 } from './collect' 11 11 export { limitConcurrency } from './limit-concurrency' 12 12 export { partitionSuiteChildren } from './suite' 13 - export { createTagsFilter, validateTags } from './tags' 13 + export { createTagsFilter, matchesTagsFilter, validateTags } from './tags' 14 14 export { 15 15 createTaskName, 16 16 getFullName,
+20
packages/runner/src/utils/tags.ts
··· 1 1 import type { TestTagDefinition, VitestRunnerConfig } from '../types/runner' 2 + import { getRunner } from '../suite' 3 + 4 + const filterMap = new WeakMap<string[], (testTags: string[]) => boolean>() 5 + 6 + /** 7 + * @experimental 8 + */ 9 + export function matchesTagsFilter(testTags: string[]): boolean { 10 + const runner = getRunner() 11 + const tagsFilter = runner._currentSpecification?.testTagsFilter ?? runner.config.tagsFilter 12 + if (!tagsFilter) { 13 + return true 14 + } 15 + let tagsFilterPredicate = filterMap.get(tagsFilter) 16 + if (!tagsFilterPredicate) { 17 + tagsFilterPredicate = createTagsFilter(tagsFilter, runner.config.tags) 18 + filterMap.set(tagsFilter, tagsFilterPredicate) 19 + } 20 + return tagsFilterPredicate(testTags) 21 + } 2 22 3 23 export function validateTags(config: VitestRunnerConfig, tags: string[]): void { 4 24 if (!config.strictTags) {
+2 -1
packages/vitest/src/runtime/runners/test.ts
··· 22 22 getFn, 23 23 getHooks, 24 24 } from '@vitest/runner' 25 - import { createChainable, getNames, getTestName, getTests } from '@vitest/runner/utils' 25 + import { createChainable, getNames, getTestName, getTests, matchesTagsFilter } from '@vitest/runner/utils' 26 26 import { processError } from '@vitest/utils/error' 27 27 import { normalize } from 'pathe' 28 28 import { createExpect } from '../../integrations/chai/index' ··· 278 278 static getTestFn: typeof getFn = getFn 279 279 static setSuiteHooks: typeof getHooks = getHooks 280 280 static setTestFn: typeof getFn = getFn 281 + static matchesTagsFilter: typeof matchesTagsFilter = matchesTagsFilter 281 282 282 283 /** 283 284 * @deprecated
+365
test/cli/test/test-tags.test.ts
··· 1361 1361 `) 1362 1362 }) 1363 1363 1364 + test('matchesTagsFilter returns true when no filter is configured', async () => { 1365 + const { stderr, testTree } = await runInlineTests({ 1366 + 'basic.test.js': ` 1367 + import { TestRunner, beforeAll, describe, expect, test } from 'vitest' 1368 + 1369 + let matchResult 1370 + 1371 + beforeAll(() => { 1372 + matchResult = TestRunner.matchesTagsFilter(['unit']) 1373 + }) 1374 + 1375 + test('test 1', { tags: ['unit'] }, () => { 1376 + expect(matchResult).toBe(true) 1377 + }) 1378 + `, 1379 + 'vitest.config.js': { 1380 + test: { 1381 + tags: [{ name: 'unit' }], 1382 + }, 1383 + }, 1384 + }) 1385 + expect(stderr).toBe('') 1386 + expect(testTree()).toMatchInlineSnapshot(` 1387 + { 1388 + "basic.test.js": { 1389 + "test 1": "passed", 1390 + }, 1391 + } 1392 + `) 1393 + }) 1394 + 1395 + test('matchesTagsFilter returns true when tags match the filter', async () => { 1396 + const { stderr, testTree } = await runInlineTests({ 1397 + 'basic.test.js': ` 1398 + import { TestRunner, beforeAll, expect, test } from 'vitest' 1399 + 1400 + let matchUnit, matchE2e 1401 + 1402 + beforeAll(() => { 1403 + matchUnit = TestRunner.matchesTagsFilter(['unit']) 1404 + matchE2e = TestRunner.matchesTagsFilter(['e2e']) 1405 + }) 1406 + 1407 + test('unit matches', { tags: ['unit'] }, () => { 1408 + expect(matchUnit).toBe(true) 1409 + expect(matchE2e).toBe(false) 1410 + }) 1411 + `, 1412 + 'vitest.config.js': { 1413 + test: { 1414 + tags: [{ name: 'unit' }, { name: 'e2e' }], 1415 + }, 1416 + }, 1417 + }, { 1418 + tagsFilter: ['unit'], 1419 + }) 1420 + expect(stderr).toBe('') 1421 + expect(testTree()).toMatchInlineSnapshot(` 1422 + { 1423 + "basic.test.js": { 1424 + "unit matches": "passed", 1425 + }, 1426 + } 1427 + `) 1428 + }) 1429 + 1430 + test('matchesTagsFilter supports NOT expressions', async () => { 1431 + const { stderr, testTree } = await runInlineTests({ 1432 + 'basic.test.js': ` 1433 + import { TestRunner, beforeAll, expect, test } from 'vitest' 1434 + 1435 + let matchUnit, matchSlow 1436 + 1437 + beforeAll(() => { 1438 + matchUnit = TestRunner.matchesTagsFilter(['unit']) 1439 + matchSlow = TestRunner.matchesTagsFilter(['slow']) 1440 + }) 1441 + 1442 + test('unit passes NOT slow', { tags: ['unit'] }, () => { 1443 + expect(matchUnit).toBe(true) 1444 + expect(matchSlow).toBe(false) 1445 + }) 1446 + `, 1447 + 'vitest.config.js': { 1448 + test: { 1449 + tags: [{ name: 'unit' }, { name: 'slow' }], 1450 + }, 1451 + }, 1452 + }, { 1453 + tagsFilter: ['!slow'], 1454 + }) 1455 + expect(stderr).toBe('') 1456 + expect(testTree()).toMatchInlineSnapshot(` 1457 + { 1458 + "basic.test.js": { 1459 + "unit passes NOT slow": "passed", 1460 + }, 1461 + } 1462 + `) 1463 + }) 1464 + 1465 + test('matchesTagsFilter supports AND/OR expressions', async () => { 1466 + const { stderr, testTree } = await runInlineTests({ 1467 + 'basic.test.js': ` 1468 + import { TestRunner, beforeAll, expect, test } from 'vitest' 1469 + 1470 + let matchUnitFast, matchUnitSlow, matchE2eFast, matchEmpty 1471 + 1472 + beforeAll(() => { 1473 + matchUnitFast = TestRunner.matchesTagsFilter(['unit', 'fast']) 1474 + matchUnitSlow = TestRunner.matchesTagsFilter(['unit', 'slow']) 1475 + matchE2eFast = TestRunner.matchesTagsFilter(['e2e', 'fast']) 1476 + matchEmpty = TestRunner.matchesTagsFilter([]) 1477 + }) 1478 + 1479 + test('matches complex expression', { tags: ['unit', 'fast'] }, () => { 1480 + expect(matchUnitFast).toBe(true) 1481 + expect(matchUnitSlow).toBe(false) 1482 + expect(matchE2eFast).toBe(true) 1483 + expect(matchEmpty).toBe(false) 1484 + }) 1485 + `, 1486 + 'vitest.config.js': { 1487 + test: { 1488 + tags: [ 1489 + { name: 'unit' }, 1490 + { name: 'e2e' }, 1491 + { name: 'fast' }, 1492 + { name: 'slow' }, 1493 + ], 1494 + }, 1495 + }, 1496 + }, { 1497 + tagsFilter: ['(unit || e2e) && fast'], 1498 + }) 1499 + expect(stderr).toBe('') 1500 + expect(testTree()).toMatchInlineSnapshot(` 1501 + { 1502 + "basic.test.js": { 1503 + "matches complex expression": "passed", 1504 + }, 1505 + } 1506 + `) 1507 + }) 1508 + 1509 + test('matchesTagsFilter supports wildcard patterns', async () => { 1510 + const { stderr, testTree } = await runInlineTests({ 1511 + 'basic.test.js': ` 1512 + import { TestRunner, beforeAll, expect, test } from 'vitest' 1513 + 1514 + let matchBrowserChrome, matchNode 1515 + 1516 + beforeAll(() => { 1517 + matchBrowserChrome = TestRunner.matchesTagsFilter(['browser-chrome']) 1518 + matchNode = TestRunner.matchesTagsFilter(['node']) 1519 + }) 1520 + 1521 + test('wildcard matches', { tags: ['browser-chrome'] }, () => { 1522 + expect(matchBrowserChrome).toBe(true) 1523 + expect(matchNode).toBe(false) 1524 + }) 1525 + `, 1526 + 'vitest.config.js': { 1527 + test: { 1528 + tags: [ 1529 + { name: 'browser-chrome' }, 1530 + { name: 'browser-firefox' }, 1531 + { name: 'node' }, 1532 + ], 1533 + }, 1534 + }, 1535 + }, { 1536 + tagsFilter: ['browser-*'], 1537 + }) 1538 + expect(stderr).toBe('') 1539 + expect(testTree()).toMatchInlineSnapshot(` 1540 + { 1541 + "basic.test.js": { 1542 + "wildcard matches": "passed", 1543 + }, 1544 + } 1545 + `) 1546 + }) 1547 + 1548 + test('matchesTagsFilter with empty tags array and no filter returns true', async () => { 1549 + const { stderr, testTree } = await runInlineTests({ 1550 + 'basic.test.js': ` 1551 + import { TestRunner, beforeAll, expect, test } from 'vitest' 1552 + 1553 + let matchEmpty 1554 + 1555 + beforeAll(() => { 1556 + matchEmpty = TestRunner.matchesTagsFilter([]) 1557 + }) 1558 + 1559 + test('empty tags no filter', () => { 1560 + expect(matchEmpty).toBe(true) 1561 + }) 1562 + `, 1563 + 'vitest.config.js': { 1564 + test: { 1565 + tags: [{ name: 'unit' }], 1566 + }, 1567 + }, 1568 + }) 1569 + expect(stderr).toBe('') 1570 + expect(testTree()).toMatchInlineSnapshot(` 1571 + { 1572 + "basic.test.js": { 1573 + "empty tags no filter": "passed", 1574 + }, 1575 + } 1576 + `) 1577 + }) 1578 + 1579 + test('per-specification testTagsFilter overrides global tagsFilter', async () => { 1580 + const { fs, ctx, errorTree } = await runInlineTests({ 1581 + 'basic.test.js': ` 1582 + test('unit-test', { tags: ['unit'] }, () => {}) 1583 + test('e2e-test', { tags: ['e2e'] }, () => {}) 1584 + test('integration-test', { tags: ['integration'] }, () => {}) 1585 + `, 1586 + 'vitest.config.js': { 1587 + test: { 1588 + globals: true, 1589 + tags: [ 1590 + { name: 'unit' }, 1591 + { name: 'e2e' }, 1592 + { name: 'integration' }, 1593 + ], 1594 + }, 1595 + }, 1596 + }, { standalone: true, watch: true, tagsFilter: ['unit'] }) 1597 + const vitest = ctx! 1598 + 1599 + const specification = vitest.getRootProject().createSpecification( 1600 + fs.resolveFile('./basic.test.js'), 1601 + { testTagsFilter: ['e2e'] }, 1602 + ) 1603 + 1604 + await vitest.runTestSpecifications([specification]) 1605 + 1606 + expect(errorTree()).toEqual({ 1607 + 'basic.test.js': { 1608 + 'unit-test': 'skipped', 1609 + 'e2e-test': 'passed', 1610 + 'integration-test': 'skipped', 1611 + }, 1612 + }) 1613 + }) 1614 + 1615 + test('per-specification testTagsFilter with complex expression', async () => { 1616 + const { fs, ctx, errorTree } = await runInlineTests({ 1617 + 'basic.test.js': ` 1618 + test('test 1', { tags: ['unit', 'fast'] }, () => {}) 1619 + test('test 2', { tags: ['unit', 'slow'] }, () => {}) 1620 + test('test 3', { tags: ['e2e', 'fast'] }, () => {}) 1621 + test('test 4', { tags: ['e2e', 'slow'] }, () => {}) 1622 + `, 1623 + 'vitest.config.js': { 1624 + test: { 1625 + globals: true, 1626 + tags: [ 1627 + { name: 'unit' }, 1628 + { name: 'e2e' }, 1629 + { name: 'fast' }, 1630 + { name: 'slow' }, 1631 + ], 1632 + }, 1633 + }, 1634 + }, { standalone: true, watch: true }) 1635 + const vitest = ctx! 1636 + 1637 + const specification = vitest.getRootProject().createSpecification( 1638 + fs.resolveFile('./basic.test.js'), 1639 + { testTagsFilter: ['unit && fast'] }, 1640 + ) 1641 + 1642 + await vitest.runTestSpecifications([specification]) 1643 + 1644 + expect(errorTree()).toEqual({ 1645 + 'basic.test.js': { 1646 + 'test 1': 'passed', 1647 + 'test 2': 'skipped', 1648 + 'test 3': 'skipped', 1649 + 'test 4': 'skipped', 1650 + }, 1651 + }) 1652 + }) 1653 + 1654 + test('matchesTagsFilter uses per-specification filter instead of global filter', async () => { 1655 + const { fs, ctx, errorTree } = await runInlineTests({ 1656 + 'basic.test.js': ` 1657 + import { TestRunner, beforeAll, expect, test } from 'vitest' 1658 + 1659 + let matchUnit, matchE2e 1660 + 1661 + beforeAll(() => { 1662 + matchUnit = TestRunner.matchesTagsFilter(['unit']) 1663 + matchE2e = TestRunner.matchesTagsFilter(['e2e']) 1664 + }) 1665 + 1666 + test('check filter', { tags: ['e2e'] }, () => { 1667 + expect(matchUnit).toBe(false) 1668 + expect(matchE2e).toBe(true) 1669 + }) 1670 + `, 1671 + 'vitest.config.js': { 1672 + test: { 1673 + tags: [ 1674 + { name: 'unit' }, 1675 + { name: 'e2e' }, 1676 + ], 1677 + }, 1678 + }, 1679 + }, { standalone: true, watch: true, tagsFilter: ['unit'] }) 1680 + const vitest = ctx! 1681 + 1682 + const specification = vitest.getRootProject().createSpecification( 1683 + fs.resolveFile('./basic.test.js'), 1684 + { testTagsFilter: ['e2e'] }, 1685 + ) 1686 + 1687 + await vitest.runTestSpecifications([specification]) 1688 + 1689 + expect(errorTree()).toEqual({ 1690 + 'basic.test.js': { 1691 + 'check filter': 'passed', 1692 + }, 1693 + }) 1694 + }) 1695 + 1696 + test('per-specification testTagsFilter with no global filter', async () => { 1697 + const { fs, ctx, errorTree } = await runInlineTests({ 1698 + 'basic.test.js': ` 1699 + test('unit-test', { tags: ['unit'] }, () => {}) 1700 + test('e2e-test', { tags: ['e2e'] }, () => {}) 1701 + `, 1702 + 'vitest.config.js': { 1703 + test: { 1704 + globals: true, 1705 + tags: [ 1706 + { name: 'unit' }, 1707 + { name: 'e2e' }, 1708 + ], 1709 + }, 1710 + }, 1711 + }, { standalone: true, watch: true }) 1712 + const vitest = ctx! 1713 + 1714 + const specification = vitest.getRootProject().createSpecification( 1715 + fs.resolveFile('./basic.test.js'), 1716 + { testTagsFilter: ['unit'] }, 1717 + ) 1718 + 1719 + await vitest.runTestSpecifications([specification]) 1720 + 1721 + expect(errorTree()).toEqual({ 1722 + 'basic.test.js': { 1723 + 'unit-test': 'passed', 1724 + 'e2e-test': 'skipped', 1725 + }, 1726 + }) 1727 + }) 1728 + 1364 1729 test('multiple tags with meta are merged with priority order', async () => { 1365 1730 const { stderr, ctx } = await runInlineTests({ 1366 1731 'basic.test.js': `