···2929 * @default 0
3030 */
3131 repeats?: number
3232+ /**
3333+ * Custom tags of the test. Useful for filtering tests.
3434+ */
3535+ tags?: string[] | string
3236}
3337```
3838+3939+<!-- TODO: rewrite this into separate test files with options highlighted -->
34403541When a test function returns a promise, the runner will wait until it is resolved to collect async expectations. If the promise is rejected, the test will fail.
3642
+35
docs/config/stricttags.md
···11+---
22+title: strictTags | Config
33+outline: deep
44+---
55+66+# strictTags <Version>4.1.0</Version> {#stricttags}
77+88+- **Type:** `boolean`
99+- **Default:** `true`
1010+- **CLI:** `--strict-tags`, `--no-strict-tags`
1111+1212+Should Vitest throw an error if test has a [`tag`](/config/tags) that is not defined in the config to avoid silently doing something surprising due to mistyped names (applying the wrong configuration or skipping the test due to a `--tags-filter` flag).
1313+1414+Note that Vitest will always throw an error if `--tags-filter` flag defines a tag not present in the config.
1515+1616+For example, this test will throw an error because the tag `fortnend` has a typo (it should be `frontend`):
1717+1818+::: code-group
1919+```js [form.test.js]
2020+test('renders a form', { tags: ['fortnend'] }, () => {
2121+ // ...
2222+})
2323+```
2424+```js [vitest.config.js]
2525+import { defineConfig } from 'vitest/config'
2626+2727+export default defineConfig({
2828+ test: {
2929+ tags: [
3030+ { name: 'frontend' },
3131+ ],
3232+ },
3333+})
3434+```
3535+:::
+194
docs/config/tags.md
···11+---
22+title: tags | Config
33+outline: deep
44+---
55+66+# tags <Version>4.1.0</Version> {#tags}
77+88+- **Type:** `TestTagDefinition[]`
99+- **Default:** `[]`
1010+1111+Defines all [available tags](/guide/test-tags) in your test project. By default, if test defines a name not listed here, Vitest will throw an error, but this can be configured via a [`strictTags`](/config/stricttags) option.
1212+1313+If you are using [`projects`](/config/projects), they will inherit all global tags definitions automatically.
1414+1515+Use [`--tags-filter`](/guide/test-tags#syntax) to filter tests by their tags. Use [`--list-tags`](/guide/cli#listtags) to print every tag in your Vitest workspace.
1616+1717+## name
1818+1919+- **Type:** `string`
2020+- **Required:** `true`
2121+2222+The name of the tag. This is what you use in the `tags` option in tests.
2323+2424+```ts
2525+export default defineConfig({
2626+ test: {
2727+ tags: [
2828+ { name: 'unit' },
2929+ { name: 'e2e' },
3030+ ],
3131+ },
3232+})
3333+```
3434+3535+::: tip
3636+If you are using TypeScript, you can enforce what tags are available by augmenting the `TestTags` type with a property that contains a union of strings (make sure this file is included by your `tsconfig`):
3737+3838+```ts [vitest.shims.ts]
3939+import 'vitest'
4040+4141+declare module 'vitest' {
4242+ interface TestTags {
4343+ tags:
4444+ | 'frontend'
4545+ | 'backend'
4646+ | 'db'
4747+ | 'flaky'
4848+ }
4949+}
5050+```
5151+:::
5252+5353+## description
5454+5555+- **Type:** `string`
5656+5757+A human-readable description for the tag. This will be shown in UI and inside error messages when a tag is not found.
5858+5959+```ts
6060+export default defineConfig({
6161+ test: {
6262+ tags: [
6363+ {
6464+ name: 'slow',
6565+ description: 'Tests that take a long time to run.',
6666+ },
6767+ ],
6868+ },
6969+})
7070+```
7171+7272+## priority
7373+7474+- **Type:** `number`
7575+- **Default:** `Infinity`
7676+7777+Priority for merging options when multiple tags with the same options are applied to a test. Lower number means higher priority (e.g., priority `1` takes precedence over priority `3`).
7878+7979+```ts
8080+export default defineConfig({
8181+ test: {
8282+ tags: [
8383+ {
8484+ name: 'flaky',
8585+ timeout: 30_000,
8686+ priority: 1, // higher priority
8787+ },
8888+ {
8989+ name: 'db',
9090+ timeout: 60_000,
9191+ priority: 2, // lower priority
9292+ },
9393+ ],
9494+ },
9595+})
9696+```
9797+9898+When a test has both tags, the `timeout` will be `30_000` because `flaky` has a higher priority.
9999+100100+## Test Options
101101+102102+Tags can define test options that will be applied to every test marked with the tag. These options are merged with the test's own options, with the test's options taking precedence.
103103+104104+### timeout
105105+106106+- **Type:** `number`
107107+108108+Test timeout in milliseconds.
109109+110110+### retry
111111+112112+- **Type:** `number | { count?: number, delay?: number, condition?: RegExp }`
113113+114114+Retry configuration for the test. If a number, specifies how many times to retry. If an object, allows fine-grained retry control.
115115+116116+### repeats
117117+118118+- **Type:** `number`
119119+120120+How many times the test will run again.
121121+122122+### concurrent
123123+124124+- **Type:** `boolean`
125125+126126+Whether suites and tests run concurrently.
127127+128128+### sequential
129129+130130+- **Type:** `boolean`
131131+132132+Whether tests run sequentially.
133133+134134+### skip
135135+136136+- **Type:** `boolean`
137137+138138+Whether the test should be skipped.
139139+140140+### only
141141+142142+- **Type:** `boolean`
143143+144144+Should this test be the only one running in a suite.
145145+146146+### todo
147147+148148+- **Type:** `boolean`
149149+150150+Whether the test should be skipped and marked as a todo.
151151+152152+### fails
153153+154154+- **Type:** `boolean`
155155+156156+Whether the test is expected to fail. If it does, the test will pass, otherwise it will fail.
157157+158158+## Example
159159+160160+```ts
161161+import { defineConfig } from 'vitest/config'
162162+163163+export default defineConfig({
164164+ test: {
165165+ tags: [
166166+ {
167167+ name: 'unit',
168168+ description: 'Unit tests.',
169169+ },
170170+ {
171171+ name: 'e2e',
172172+ description: 'End-to-end tests.',
173173+ timeout: 60_000,
174174+ },
175175+ {
176176+ name: 'flaky',
177177+ description: 'Flaky tests that need retries.',
178178+ retry: process.env.CI ? 3 : 0,
179179+ priority: 1,
180180+ },
181181+ {
182182+ name: 'slow',
183183+ description: 'Slow tests.',
184184+ timeout: 120_000,
185185+ },
186186+ {
187187+ name: 'skip-ci',
188188+ description: 'Tests to skip in CI.',
189189+ skip: !!process.env.CI,
190190+ },
191191+ ],
192192+ },
193193+})
194194+```
+37-4
docs/guide/cli-generated.md
···518518519519Stop test execution when given number of tests have failed (default: `0`)
520520521521-### retry
521521+### retry.count
522522523523-- **CLI:** `--retry <times>`
524524-- **Config:** [retry](/config/retry)
523523+- **CLI:** `--retry.count <times>`
524524+- **Config:** [retry.count](/config/retry#retry-count)
525525526526-Retry the test specific number of times if it fails (default: `0`)
526526+Number of times to retry a test if it fails (default: `0`)
527527+528528+### retry.delay
529529+530530+- **CLI:** `--retry.delay <ms>`
531531+- **Config:** [retry.delay](/config/retry#retry-delay)
532532+533533+Delay in milliseconds between retry attempts (default: `0`)
534534+535535+### retry.condition
536536+537537+- **CLI:** `--retry.condition <pattern>`
538538+- **Config:** [retry.condition](/config/retry#retry-condition)
539539+540540+Regex pattern to match error messages that should trigger a retry. Only errors matching this pattern will cause a retry (default: retry on all errors)
527541528542### diff.aAnnotation
529543···792806793807Start Vitest without running tests. Tests will be running only on change. This option is ignored when CLI file filters are passed. (default: `false`)
794808809809+### listTags
810810+811811+- **CLI:** `--listTags [type]`
812812+813813+List all available tags instead of running tests. `--list-tags=json` will output tags in JSON format, unless there are no tags.
814814+795815### clearCache
796816797817- **CLI:** `--clearCache`
798818799819Delete all Vitest caches, including `experimental.fsModuleCache`, without running any tests. This will reduce the performance in the subsequent test run.
820820+821821+### tagsFilter
822822+823823+- **CLI:** `--tagsFilter <expression>`
824824+825825+Run only tests with the specified tags. You can use logical operators `&&` (and), `||` (or) and `!` (not) to create complex expressions, see [Test Tags](/guide/test-tags#syntax) for more information.
826826+827827+### strictTags
828828+829829+- **CLI:** `--strictTags`
830830+- **Config:** [strictTags](/config/stricttags)
831831+832832+Should Vitest throw an error if test has a tag that is not defined in the config. (default: `true`)
800833801834### experimental.fsModuleCache
802835
+18
docs/guide/filtering.md
···8989})
9090```
91919292+## Filtering Tags
9393+9494+If your test defines a [tag](/guide/test-tags), you can filter your tests with a `--tags-filter` option:
9595+9696+```ts
9797+test('renders a form', { tags: ['frontend'] }, () => {
9898+ // ...
9999+})
100100+101101+test('calls an external API', { tags: ['backend'] }, () => {
102102+ // ...
103103+})
104104+```
105105+106106+```shell
107107+vitest --tags-filter=frontend
108108+```
109109+92110## Selecting Suites and Tests to Run
9311194112Use `.only` to only run certain suites or tests
+302
docs/guide/test-tags.md
···11+---
22+title: Test Tags | Guide
33+outline: deep
44+---
55+66+# Test Tags <Version>4.1.0</Version>
77+88+[`Tags`](/config/tags) allow you to mark tests and change their options based on the tag's definition.
99+1010+## Defining Tags
1111+1212+Tags must be defined in your configuration file — Vitest does not provide any built-in tags. If a test uses a tag that isn't defined in the config, the test runner will throw an error. This prevents unexpected behavior from mistyped tag names. You can disable this check with the [`strictTags`](/config/stricttags) option.
1313+1414+You must define a `name` of the tag, and you may define additional options that will be applied to every test marked with the tag, e.g., a `timeout`, or `retry`. For the full list of available options, see [`tags`](/config/tags).
1515+1616+```ts [vitest.config.js]
1717+import { defineConfig } from 'vitest/config'
1818+1919+export default defineConfig({
2020+ test: {
2121+ tags: [
2222+ {
2323+ name: 'frontend',
2424+ description: 'Tests written for frontend.',
2525+ },
2626+ {
2727+ name: 'backend',
2828+ description: 'Tests written for backend.',
2929+ },
3030+ {
3131+ name: 'db',
3232+ description: 'Tests for database queries.',
3333+ timeout: 60_000,
3434+ },
3535+ {
3636+ name: 'flaky',
3737+ description: 'Flaky CI tests.',
3838+ retry: process.env.CI ? 3 : 0,
3939+ timeout: 30_000,
4040+ priority: 1,
4141+ },
4242+ ],
4343+ },
4444+})
4545+```
4646+4747+::: warning
4848+If several tags have the same options and are used on the same test, they will be resolved in the order they were specified, or sorted by priority first (the lower the number, the higher the priority). Tags without a defined priority are merged first and will be overriden by higher priority ones:
4949+5050+```ts
5151+test('flaky database test', { tags: ['flaky', 'db'] })
5252+// { timeout: 30_000, retry: 3 }
5353+```
5454+5555+Note that the `timeout` is 30 seconds (and not 60) because `flaky` tag has a priority of `1` while `db` (that defines 60 second timeout) has no priority.
5656+5757+If test defines its own options, they will have the highest priority:
5858+5959+```ts
6060+test('flaky database test', { tags: ['flaky', 'db'], timeout: 120_000 })
6161+// { timeout: 120_000, retry: 3 }
6262+```
6363+:::
6464+6565+If you are using TypeScript, you can enforce what tags are available by augmenting the `TestTags` type with a property that contains a union of strings (make sure this file is included by your `tsconfig`):
6666+6767+```ts [vitest.shims.ts]
6868+import 'vitest'
6969+7070+declare module 'vitest' {
7171+ interface TestTags {
7272+ tags:
7373+ | 'frontend'
7474+ | 'backend'
7575+ | 'db'
7676+ | 'flaky'
7777+ }
7878+}
7979+```
8080+8181+To see all your tags, you can use [`--list-tags`](/guide/cli#listtags) command:
8282+8383+```shell
8484+vitest --list-tags
8585+8686+frontend: Tests written for frontend.
8787+backend: Tests written for backend.
8888+db: Tests for database queries.
8989+flaky: Flaky CI tests.
9090+```
9191+9292+To print it in JSON, pass down `--list-tags=json`:
9393+9494+```json
9595+{
9696+ "tags": [
9797+ {
9898+ "name": "frontend",
9999+ "description": "Tests written for frontend."
100100+ },
101101+ {
102102+ "name": "backend",
103103+ "description": "Tests written for backend."
104104+ },
105105+ {
106106+ "name": "db",
107107+ "description": "Tests for database queries.",
108108+ "timeout": 60000
109109+ },
110110+ {
111111+ "name": "flaky",
112112+ "description": "Flaky CI tests.",
113113+ "retry": 0,
114114+ "timeout": 30000,
115115+ "priority": 1
116116+ }
117117+ ],
118118+ "projects": []
119119+}
120120+```
121121+122122+## Using Tags in Tests
123123+124124+You can apply tags to individual tests or entire suites using the `tags` option:
125125+126126+```ts
127127+import { describe, test } from 'vitest'
128128+129129+test('renders homepage', { tags: ['frontend'] }, () => {
130130+ // ...
131131+})
132132+133133+describe('API endpoints', { tags: ['backend'] }, () => {
134134+ test('returns user data', () => {
135135+ // This test inherits the "backend" tag from the parent suite
136136+ })
137137+138138+ test('validates input', { tags: ['validation'] }, () => {
139139+ // This test has both "backend" (inherited) and "validation" tags
140140+ })
141141+})
142142+```
143143+144144+Tags are inherited from parent suites, so all tests inside a tagged `describe` block will automatically have that tag.
145145+146146+It's also possible to define `tags` for every test in the file by using JSDoc's `@module-tag` at the top of the file:
147147+148148+```ts
149149+/**
150150+ * Auth tests
151151+ * @module-tag admin/pages/dashboard
152152+ * @module-tag acceptance
153153+ */
154154+155155+test('dashboard renders items', () => {
156156+ // ...
157157+})
158158+```
159159+160160+::: danger
161161+A `@module-tag` in a JSDoc comment applies to all tests in that file, not just the test it precedes.
162162+163163+Consider this example:
164164+165165+```js{3,10}
166166+describe('forms', () => {
167167+ /**
168168+ * @module-tag frontend
169169+ */
170170+ test('renders a form', () => {
171171+ // ...
172172+ })
173173+174174+ /**
175175+ * @module-tag db
176176+ */
177177+ test('db returns users', () => {
178178+ // ...
179179+ })
180180+})
181181+```
182182+183183+In this example, every test in the file will have both the `frontend` and `db` tags. To tag individual tests, use the options argument instead:
184184+185185+```js{2,6}
186186+describe('forms', () => {
187187+ test('renders a form', { tags: 'frontend' }, () => {
188188+ // ...
189189+ })
190190+191191+ test('db returns users', { tags: 'db' }, () => {
192192+ // ...
193193+ })
194194+})
195195+```
196196+:::
197197+198198+## Filtering Tests by Tag
199199+200200+To run only tests with specific tags, use the [`--tags-filter`](/guide/cli#tagsfilter) CLI option:
201201+202202+```shell
203203+vitest --tags-filter=frontend
204204+vitest --tags-filter="frontend and backend"
205205+```
206206+207207+If you are running Vitest UI, you can start a filter with a `tag:` prefix to filter out tests by tags using the same tags expression sytax:
208208+209209+<img alt="The tags filter in Vitest UI" img-light src="/ui/light-ui-tags.png">
210210+<img alt="The tags filter in Vitest UI" img-dark src="/ui/dark-ui-tags.png">
211211+212212+If you are using a programmatic API, you can pass down a `tagsFilter` option to [`startVitest`](/guide/advanced/#startvitest) or [`createVitest`](/guide/advanced/#createvitest):
213213+214214+```ts
215215+import { startVitest } from 'vitest/node'
216216+217217+await startVitest('test', [], {
218218+ tagsFilter: ['frontend and backend'],
219219+})
220220+```
221221+222222+Or you can create a [test specification](/api/advanced/test-specification) with your custom filters:
223223+224224+```ts
225225+const specification = vitest.getRootProject().createSpecification(
226226+ '/path-to-file.js',
227227+ {
228228+ testTagsFilter: ['frontend and backend'],
229229+ },
230230+)
231231+```
232232+233233+### Syntax
234234+235235+You can combine tags in different ways. Vitest supports these keywords:
236236+237237+- `and` or `&&` to include both expressions
238238+- `or` or `||` to include at least one expression
239239+- `not` or `!` to exclude the expression
240240+- `*` to match any number of characters (0 or more)
241241+- `()` to group expressions and override precedence
242242+243243+The parser follows standard [operator precedence](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence): `not`/`!` has the highest priority, then `and`/`&&`, then `or`/`||`. Use parentheses to override default precedence.
244244+245245+::: warning Reserved Names
246246+Tag names cannot be `and`, `or`, or `not` (case-insensitive) as these are reserved keywords. Tag names also cannot contain special characters (`(`, `)`, `&`, `|`, `!`, `*`, spaces) as these are used by the expression parser.
247247+:::
248248+249249+### Wildcards
250250+251251+You can use a wildcard (`*`) to match any number of characters:
252252+253253+```shell
254254+vitest --tags-filter="unit/*"
255255+```
256256+257257+This will match tags like `unit/components`, `unit/utils`, etc.
258258+259259+### Excluding Tags
260260+261261+To exclude tests with a specific tag, add an exclamation mark (`!`) at the start or a "not" keyword:
262262+263263+```shell
264264+vitest --tags-filter="!slow and not flaky"
265265+```
266266+267267+### Examples
268268+269269+Here are some common filtering patterns:
270270+271271+```shell
272272+# Run only unit tests
273273+vitest --tags-filter="unit"
274274+275275+# Run tests that are both frontend AND fast
276276+vitest --tags-filter="frontend and fast"
277277+278278+# Run tests that are either unit OR e2e
279279+vitest --tags-filter="unit or e2e"
280280+281281+# Run all tests except slow ones
282282+vitest --tags-filter="!slow"
283283+284284+# Run frontend tests that are not flaky
285285+vitest --tags-filter="frontend && !flaky"
286286+287287+# Run tests matching a wildcard pattern
288288+vitest --tags-filter="api/*"
289289+290290+# Complex expression with parentheses
291291+vitest --tags-filter="(unit || e2e) && !slow"
292292+293293+# Run database tests that are either postgres or mysql, but not slow
294294+vitest --tags-filter="db && (postgres || mysql) && !slow"
295295+```
296296+297297+You can also pass multiple `--tags-filter` flags. They are combined with AND logic:
298298+299299+```shell
300300+# Run tests that match (unit OR e2e) AND are NOT slow
301301+vitest --tags-filter="unit || e2e" --tags-filter="!slow"
302302+```
···105105 readonly shuffle: boolean | undefined
106106 readonly retry: number | undefined
107107 readonly repeats: number | undefined
108108+ readonly tags: string[] | undefined
109109+ readonly timeout: number | undefined
108110 readonly mode: 'run' | 'only' | 'skip' | 'todo'
109111}
110112```
111113112114The options that test was collected with.
115115+116116+## tags <Version>4.1.0</Version> {#tags}
117117+118118+[Tags](/guide/test-tags) that were implicitly or explicitly assigned to the test.
113119114120## ok
115121
+5
docs/api/advanced/test-specification.md
···1111 testLines: [20, 40],
1212 testNamePattern: /hello world/,
1313 testIds: ['1223128da3_0_0_0', '1223128da3_0_0'],
1414+ testTagsFilter: ['frontend and backend'],
1415 } // optional test filters
1516)
1617```
···8182## testIds <Version>4.1.0</Version> {#testids}
82838384The ids of tasks inside of this specification to run.
8585+8686+## testTagsFilter <Version>4.1.0</Version> {#testtagsfilter}
8787+8888+The [tags filter](/guide/test-tags#syntax) that a test must pass in order to be included in the run. Multiple filters are treated as `AND`.
84898590## toJSON
8691
···173173})
174174175175test('adding a plugin with existing name throws and error', async () => {
176176- await expect(() => vitest({
176176+ await expect(() => throws({
177177 projects: [
178178 {
179179 test: {
···196196 }),
197197 ).rejects.toThrowError('Project name "project-1" is not unique. All projects should have unique names. Make sure your configuration is correct.')
198198199199- await expect(() => vitest({
199199+ await expect(() => throws({
200200 projects: [
201201 {
202202 plugins: [
···221221 }),
222222 ).rejects.toThrowError('Project name "project-1" is not unique. All projects should have unique names. Make sure your configuration is correct.')
223223224224- await expect(() => vitest({
224224+ await expect(() => throws({
225225 projects: [
226226 {
227227 plugins: [
···248248 }),
249249 ).rejects.toThrowError('Project name "project-1" is not unique. All projects should have unique names. Make sure your configuration is correct.')
250250})
251251+252252+async function throws(cliOptions: TestUserConfig) {
253253+ await vitest(cliOptions)
254254+}
···6666 await page.goto(pageUrl)
67676868 // dashboard
6969- await expect(page.locator('[aria-labelledby=tests]')).toContainText('14 Pass 1 Fail 15 Total')
6969+ await expect(page.locator('[aria-labelledby=tests]')).toContainText('15 Pass 1 Fail 16 Total')
70707171 // unhandled errors
7272 await expect(page.getByTestId('unhandled-errors')).toContainText(
···189189190190 await expect(annotations.last().getByRole('link')).toHaveAttribute('href', /data\/\w+/)
191191 await expect(annotations.nth(3).getByRole('link')).toHaveAttribute('href', /data\/\w+/)
192192+ })
193193+194194+ test('tags filter', async ({ page }) => {
195195+ await page.goto(pageUrl)
196196+197197+ await page.getByPlaceholder('Search...').fill('tag:db')
198198+199199+ // only one test with the tag "db"
200200+ await expect(page.getByText('PASS (1)')).toBeVisible()
201201+ await expect(page.getByTestId('explorer-item').filter({ hasText: 'has tags' })).toBeVisible()
202202+203203+ await page.getByPlaceholder('Search...').fill('tag:db && !flaky')
204204+ await expect(page.getByText('No matched test')).toBeVisible()
205205+206206+ await page.getByPlaceholder('Search...').fill('tag:unknown')
207207+ await expect(page.getByText('The tag pattern "unknown" is not defined in the configuration')).toBeVisible()
192208 })
193209194210 test('visual regression in the report tab', async ({ page }) => {
+21-4
test/ui/test/ui.spec.ts
···7070 await page.goto(pageUrl)
71717272 // dashboard
7373- await expect(page.locator('[aria-labelledby=tests]')).toContainText('14 Pass 1 Fail 15 Total')
7373+ await expect(page.locator('[aria-labelledby=tests]')).toContainText('15 Pass 1 Fail 16 Total')
74747575 // unhandled errors
7676 await expect(page.getByTestId('unhandled-errors')).toContainText(
···256256 // pass files with special chars
257257 await page.getByPlaceholder('Search...').fill('char () - Square root of nine (9)')
258258 await expect(page.getByText('char () - Square root of nine (9)')).toBeVisible()
259259- await page.getByText('char () - Square root of nine (9)').hover()
260260- await page.getByLabel('Run current test').click()
261261- await expect(page.getByText('All tests passed in this file')).toBeVisible()
259259+ const testItem = page.getByTestId('explorer-item').filter({ hasText: 'char () - Square root of nine (9)' })
260260+ await testItem.hover()
261261+ await testItem.getByLabel('Run current test').click()
262262+ await expect(page.getByText('The test has passed without any errors')).toBeVisible()
263263+ })
264264+265265+ test('tags filter', async ({ page }) => {
266266+ await page.goto(pageUrl)
267267+268268+ await page.getByPlaceholder('Search...').fill('tag:db')
269269+270270+ // only one test with the tag "db"
271271+ await expect(page.getByText('PASS (1)')).toBeVisible()
272272+ await expect(page.getByTestId('explorer-item').filter({ hasText: 'has tags' })).toBeVisible()
273273+274274+ await page.getByPlaceholder('Search...').fill('tag:db && !flaky')
275275+ await expect(page.getByText('No matched test')).toBeVisible()
276276+277277+ await page.getByPlaceholder('Search...').fill('tag:unknown')
278278+ await expect(page.getByText('The tag pattern "unknown" is not defined in the configuration')).toBeVisible()
262279 })
263280264281 test('dashboard entries filter tests correctly', async ({ page }) => {
+28
packages/runner/src/types/runner.ts
···1212 TestAnnotation,
1313 TestArtifact,
1414 TestContext,
1515+ TestOptions,
1616+ TestTags,
1517} from './tasks'
16181719/**
···4042 retry: SerializableRetry
4143 includeTaskLocation?: boolean
4244 diffOptions?: DiffOptions
4545+ tags: TestTagDefinition[]
4646+ tagsFilter?: string[]
4747+ strictTags: boolean
4348}
44494550/**
···4752 */
4853export interface FileSpecification {
4954 filepath: string
5555+ // file can be marked via a jsdoc comment to have tags,
5656+ // these are _not_ tags to filter tests by
5757+ fileTags?: string[]
5058 testLocations: number[] | undefined
5159 testNamePattern: RegExp | undefined
6060+ testTagsFilter: string[] | undefined
5261 testIds: string[] | undefined
6262+}
6363+6464+export interface TestTagDefinition extends Omit<TestOptions, 'tags' | 'shuffle'> {
6565+ /**
6666+ * The name of the tag. This is what you use in the `tags` array in tests.
6767+ */
6868+ name: keyof TestTags extends never
6969+ ? string
7070+ : TestTags[keyof TestTags]
7171+ /**
7272+ * A description for the tag. This will be shown in the CLI help and UI.
7373+ */
7474+ description?: string
7575+ /**
7676+ * Priority for merging options when multiple tags with the same options are applied to a test.
7777+ *
7878+ * Lower number means higher priority. E.g., priority 1 takes precedence over priority 3.
7979+ */
8080+ priority?: number
5381}
54825583export type VitestRunnerImportSource = 'collect' | 'setup'
+21-6
packages/runner/src/types/tasks.ts
···114114 * @experimental
115115 */
116116 dynamic?: boolean
117117+ /**
118118+ * Custom tags of the task. Useful for filtering tasks.
119119+ */
120120+ tags?: string[]
117121}
118122119123export interface TaskPopulated extends TaskBase {
···545549 */
546550 sequential?: boolean
547551 /**
548548- * Whether the tasks of the suite run in a random order.
549549- */
550550- shuffle?: boolean
551551- /**
552552 * Whether the test should be skipped.
553553 */
554554 skip?: boolean
···564564 * Whether the test is expected to fail. If it does, the test will pass, otherwise it will fail.
565565 */
566566 fails?: boolean
567567+ /**
568568+ * Custom tags of the test. Useful for filtering tests.
569569+ */
570570+ tags?: keyof TestTags extends never
571571+ ? string[] | string
572572+ : TestTags[keyof TestTags] | TestTags[keyof TestTags][]
573573+}
574574+575575+export interface TestTags {}
576576+577577+export interface SuiteOptions extends TestOptions {
578578+ /**
579579+ * Whether the tasks of the suite run in a random order.
580580+ */
581581+ shuffle?: boolean
567582}
568583569584interface ExtendedAPI<ExtraContext> {
···647662 ): SuiteCollector<OverrideExtraContext>
648663 <OverrideExtraContext extends ExtraContext = ExtraContext>(
649664 name: string | Function,
650650- options: TestOptions,
665665+ options: SuiteOptions,
651666 fn?: SuiteFactory<OverrideExtraContext>
652667 ): SuiteCollector<OverrideExtraContext>
653668}
···719734export interface SuiteCollector<ExtraContext = object> {
720735 readonly name: string
721736 readonly mode: RunMode
722722- options?: TestOptions
737737+ options?: SuiteOptions
723738 type: 'collector'
724739 test: TestAPI<ExtraContext>
725740 tasks: (
···1212import { rootDir } from '../paths'
1313import { isWindows } from '../utils/env'
1414import { getWorkerMemoryLimit, stringToBytes } from '../utils/memory-limit'
1515-import { getSpecificationsEnvironments } from '../utils/test-helpers'
1515+import { getSpecificationsOptions } from '../utils/test-helpers'
1616import { createBrowserPool } from './pools/browser'
1717import { Pool } from './pools/pool'
1818···8787 let workerId = 0
88888989 const sorted = await sequencer.sort(specs)
9090- const environments = await getSpecificationsEnvironments(specs)
9090+ const { environments, tags } = await getSpecificationsOptions(specs)
9191 const groups = groupSpecs(sorted, environments)
92929393 const projectEnvs = new WeakMap<TestProject, Partial<NodeJS.ProcessEnv>>()
···149149 context: {
150150 files: specs.map(spec => ({
151151 filepath: spec.moduleId,
152152+ fileTags: tags.get(spec),
152153 testLocations: spec.testLines,
153154 testNamePattern: spec.testNamePattern,
154155 testIds: spec.testIds,
156156+ testTagsFilter: spec.testTagsFilter,
155157 })),
156158 invalidates,
157159 providedContext: project.getProvidedContext(),
···337339 return null
338340}
339341340340-function groupSpecs(specs: TestSpecification[], environments: Awaited<ReturnType<typeof getSpecificationsEnvironments>>) {
342342+function groupSpecs(specs: TestSpecification[], environments: WeakMap<TestSpecification, ContextTestEnvironment>) {
341343 // Test files are passed to test runner one at a time, except for Typechecker or when "--maxWorker=1 --no-isolate"
342344 type SpecsForRunner = TestSpecification[]
343345
+27
packages/vitest/src/node/tags.ts
···11+import type { TestTagDefinition } from '@vitest/runner'
22+import type { TestProject } from './project'
33+44+export function populateProjectsTags(rootProject: TestProject, projects: TestProject[]): void {
55+ // Include root project if not already in the list
66+ const allProjects = projects.includes(rootProject) ? projects : [rootProject, ...projects]
77+88+ // Collect all tags from all projects (first definition wins)
99+ const globalTags = new Map<string, TestTagDefinition>()
1010+ for (const project of allProjects) {
1111+ for (const tag of project.config.tags || []) {
1212+ if (!globalTags.has(tag.name)) {
1313+ globalTags.set(tag.name, tag)
1414+ }
1515+ }
1616+ }
1717+1818+ // Add missing tags to each project (without overriding local definitions)
1919+ for (const project of allProjects) {
2020+ const projectTagNames = new Set(project.config.tags.map(t => t.name))
2121+ for (const [tagName, tagDef] of globalTags) {
2222+ if (!projectTagNames.has(tagName)) {
2323+ project.config.tags.push(tagDef)
2424+ }
2525+ }
2626+ }
2727+}
+7
packages/vitest/src/node/test-specification.ts
···99 testNamePattern?: RegExp
1010 testIds?: string[]
1111 testLines?: number[]
1212+ testTagsFilter?: string[]
1213}
13141415export class TestSpecification {
···4142 * The ids of tasks inside of this specification to run.
4243 */
4344 public readonly testIds: string[] | undefined
4545+ /**
4646+ * The tags of tests to run.
4747+ */
4848+ public readonly testTagsFilter: string[] | undefined
44494550 /**
4651 * This class represents a test suite for a test module within a single project.
···7378 this.testLines = testLinesOrOptions.testLines
7479 this.testNamePattern = testLinesOrOptions.testNamePattern
7580 this.testIds = testLinesOrOptions.testIds
8181+ this.testTagsFilter = testLinesOrOptions.testTagsFilter
7682 }
7783 }
7884···99105 testLines: this.testLines,
100106 testIds: this.testIds,
101107 testNamePattern: this.testNamePattern,
108108+ testTagsFilter: this.testTagsFilter,
102109 },
103110 ]
104111 }
+1
packages/vitest/src/public/config.ts
···1919 defaultInclude,
2020} from '../defaults'
2121export type { WatcherTriggerPattern } from '../node/watcher'
2222+export type { TestTagDefinition } from '@vitest/runner'
2223export { mergeConfig } from 'vite'
2324export type { Plugin } from 'vite'
2425
+2-1
packages/vitest/src/public/index.ts
···140140 TestContext,
141141 TestFunction,
142142 TestOptions,
143143-144143 VitestRunnerConfig as TestRunnerConfig,
144144+145145+ TestTags,
145146 VitestRunner as VitestTestRunner,
146147} from '@vitest/runner'
147148
+4-1
packages/vitest/src/runtime/config.ts
···11import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers'
22import type { PrettyFormatOptions } from '@vitest/pretty-format'
33-import type { SequenceHooks, SequenceSetupFiles, SerializableRetry } from '@vitest/runner'
33+import type { SequenceHooks, SequenceSetupFiles, SerializableRetry, TestTagDefinition } from '@vitest/runner'
44import type { SnapshotEnvironment, SnapshotUpdateState } from '@vitest/snapshot'
55import type { SerializedDiffOptions } from '@vitest/utils/diff'
66···128128 browserSdkPath?: string
129129 } | undefined
130130 }
131131+ tags: TestTagDefinition[]
132132+ tagsFilter: string[] | undefined
133133+ strictTags: boolean
131134}
132135133136export interface SerializedCoverageConfig {
···104104 </div>
105105 <template v-else>
106106 <div bg="green-500/10" text="green-500 sm" p="x4 y2" m-2 rounded>
107107- All tests passed in this file
107107+ The test has passed without any errors
108108 </div>
109109 </template>
110110 <template v-if="test.annotations.length">
+9
packages/ui/client/composables/client/state.ts
···11+import type { TestTagDefinition } from '@vitest/runner'
12import type { TestError } from '@vitest/utils'
23import type { Ref } from 'vue'
34import type { RunState } from '../../../types'
45import { computed, ref } from 'vue'
66+import { config } from '.'
5768export const testRunState: Ref<RunState> = ref('idle')
79export const finished = computed(() => testRunState.value === 'idle')
810export const unhandledErrors: Ref<TestError[]> = ref([])
1111+export const tagsDefinitions = computed(() => {
1212+ const tags = config.value.tags || []
1313+ return tags.reduce((acc, tag) => {
1414+ acc[tag.name] = tag
1515+ return acc
1616+ }, {} as Record<string, TestTagDefinition>)
1717+})
···9191 })
92929393 try {
9494- if (ctx.config.clearCache) {
9494+ if (ctx.config.listTags) {
9595+ await ctx.listTags()
9696+ }
9797+ else if (ctx.config.clearCache) {
9598 await ctx.experimental_clearCache()
9699 }
97100 else if (ctx.config.mergeReports) {
+13
packages/vitest/src/node/cli/cli-config.ts
···785785 return value
786786 },
787787 },
788788+ listTags: {
789789+ description: 'List all available tags instead of running tests. `--list-tags=json` will output tags in JSON format, unless there are no tags.',
790790+ argument: '[type]',
791791+ },
788792 clearCache: {
789793 description: 'Delete all Vitest caches, including `experimental.fsModuleCache`, without running any tests. This will reduce the performance in the subsequent test run.',
794794+ },
795795+ tagsFilter: {
796796+ description: 'Run only tests with the specified tags. You can use logical operators `&&` (and), `||` (or) and `!` (not) to create complex expressions, see [Test Tags](https://vitest.dev/guide/test-tags#syntax) for more information.',
797797+ argument: '<expression>',
798798+ array: true,
799799+ },
800800+ strictTags: {
801801+ description: 'Should Vitest throw an error if test has a tag that is not defined in the config. (default: `true`)',
790802 },
791803792804 experimental: {
···844856 filesOnly: null,
845857 projects: null,
846858 watchTriggerPatterns: null,
859859+ tags: null,
847860}
848861849862export const benchCliOptionsConfig: Pick<
+28
packages/vitest/src/node/config/resolveConfig.ts
···168168 resolved.project = toArray(resolved.project)
169169 resolved.provide ??= {}
170170171171+ // shallow copy tags array to avoid mutating user config
172172+ resolved.tags = [...resolved.tags || []]
173173+ const definedTags = new Set<string>()
174174+ resolved.tags.forEach((tag) => {
175175+ if (!tag.name || typeof tag.name !== 'string') {
176176+ throw new Error(`Each tag defined in "test.tags" must have a "name" property, received: ${JSON.stringify(tag)}`)
177177+ }
178178+ if (definedTags.has(tag.name)) {
179179+ throw new Error(`Tag name "${tag.name}" is already defined in "test.tags". Tag names must be unique.`)
180180+ }
181181+ if (tag.name.match(/\s/)) {
182182+ throw new Error(`Tag name "${tag.name}" is invalid. Tag names cannot contain spaces.`)
183183+ }
184184+ if (tag.name.match(/([!()*|&])/)) {
185185+ throw new Error(`Tag name "${tag.name}" is invalid. Tag names cannot contain "!", "*", "&", "|", "(", or ")".`)
186186+ }
187187+ if (tag.name.match(/^\s*(and|or|not)\s*$/i)) {
188188+ throw new Error(`Tag name "${tag.name}" is invalid. Tag names cannot be a logical operator like "and", "or", "not".`)
189189+ }
190190+ if (typeof tag.retry === 'object' && typeof tag.retry.condition === 'function') {
191191+ throw new TypeError(`Tag "${tag.name}": retry.condition function cannot be used inside a config file. Use a RegExp pattern instead, or define the function in your test file.`)
192192+ }
193193+ if (tag.priority != null && (typeof tag.priority !== 'number' || tag.priority < 0)) {
194194+ throw new TypeError(`Tag "${tag.name}": priority must be a non-negative number.`)
195195+ }
196196+ definedTags.add(tag.name)
197197+ })
198198+171199 resolved.name = typeof options.name === 'string'
172200 ? options.name
173201 : (options.name?.label || '')
···103103 */
104104 public readonly parent: TestSuite | TestModule
105105106106+ /**
107107+ * Tags associated with the test.
108108+ */
109109+ public readonly tags: string[]
110110+106111 /** @internal */
107112 protected constructor(task: RunnerTestCase, project: TestProject) {
108113 super(task, project)
···117122 this.parent = this.module
118123 }
119124 this.options = buildOptions(task)
125125+ this.tags = this.options.tags || []
120126 }
121127122128 /**
···565571 readonly shuffle: boolean | undefined
566572 readonly retry: SerializableRetry | undefined
567573 readonly repeats: number | undefined
574574+ readonly tags: string[] | undefined
575575+ /**
576576+ * Only tests have a `timeout` option.
577577+ */
578578+ readonly timeout: number | undefined
568579 readonly mode: 'run' | 'only' | 'skip' | 'todo'
569580}
570581···578589 shuffle: task.shuffle,
579590 retry: task.retry as SerializableRetry | undefined,
580591 repeats: task.repeats,
592592+ tags: task.tags,
593593+ timeout: task.type === 'test' ? task.timeout : undefined,
581594 // runner types are too broad, but the public API should be more strict
582595 // the queued state exists only on Files and this method is called
583596 // only for tests and suites
+28-1
packages/vitest/src/node/types/config.ts
···11import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers'
22import type { PrettyFormatOptions } from '@vitest/pretty-format'
33-import type { SequenceHooks, SequenceSetupFiles, SerializableRetry } from '@vitest/runner'
33+import type { SequenceHooks, SequenceSetupFiles, SerializableRetry, TestTagDefinition } from '@vitest/runner'
44import type { SnapshotStateOptions } from '@vitest/snapshot'
55import type { Arrayable } from '@vitest/utils'
66import type { SerializedDiffOptions } from '@vitest/utils/diff'
···879879 */
880880 nodeLoader?: boolean
881881 }
882882+883883+ /**
884884+ * Define tags available in your test files.
885885+ *
886886+ * If test defines a tag that is not listed here, an error will be thrown.
887887+ */
888888+ tags?: TestTagDefinition[]
889889+890890+ /**
891891+ * Should Vitest throw an error if test has a tag that is not defined in the config.
892892+ * @default true
893893+ */
894894+ strictTags?: boolean
882895}
883896884897export interface TypecheckConfig {
···10141027 * @experimental
10151028 */
10161029 clearCache?: boolean
10301030+10311031+ /**
10321032+ * Tags expression to filter tests to run. Multiple filters will be applied using AND logic.
10331033+ * @see {@link https://vitest.dev/guide/test-tags#syntax}
10341034+ */
10351035+ tagsFilter?: string[]
10361036+10371037+ /**
10381038+ * Log all available tags instead of running tests.
10391039+ */
10401040+ listTags?: boolean | 'json'
10171041}
1018104210191043export type OnUnhandledErrorCallback = (error: (TestError | Error) & { type: string }) => boolean | void
···10461070 | 'name'
10471071 | 'vmMemoryLimit'
10481072 | 'fileParallelism'
10731073+ | 'tagsFilter'
10491074 > {
10501075 mode: VitestRunMode
10511076···1116114111171142 vmMemoryLimit?: UserConfig['vmMemoryLimit']
11181143 dumpDir?: string
11441144+ tagsFilter?: string[]
11191145}
1120114611211147type NonProjectOptions
···11451171 | 'inspectBrk'
11461172 | 'coverage'
11471173 | 'watchTriggerPatterns'
11741174+ | 'tagsFilter' // CLI option only
1148117511491176export interface ServerDepsOptions {
11501177 /**