diff --git a/.vitepress/config.ts b/.vitepress/config.ts index a580ea7e..95f130ff 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -469,6 +469,14 @@ export default ({ mode }: { mode: string }) => { text: 'sequence', link: '/config/sequence', }, + { + text: 'tags', + link: '/config/tags', + }, + { + text: 'strictTags', + link: '/config/stricttags', + }, { text: 'typecheck', link: '/config/typecheck', @@ -773,7 +781,15 @@ export default ({ mode }: { mode: string }) => { link: '/guide/filtering', }, { +<<<<<<< HEAD text: '测试上下文', +======= + text: 'Test Tags', + link: '/guide/test-tags', + }, + { + text: 'Test Context', +>>>>>>> 4a24a6d9f32c7ad524eb2a1d042d5ee52ed4312b link: '/guide/test-context', }, { diff --git a/.vitepress/scripts/cli-generator.ts b/.vitepress/scripts/cli-generator.ts index 1e82a8bd..ee39aa62 100644 --- a/.vitepress/scripts/cli-generator.ts +++ b/.vitepress/scripts/cli-generator.ts @@ -42,6 +42,8 @@ const skipConfig = new Set([ 'browser.name', 'browser.fileParallelism', 'clearCache', + 'tagsFilter', + 'listTags', ]) function resolveOptions(options: CLIOptions, parentName?: string) { diff --git a/api/advanced/test-case.md b/api/advanced/test-case.md index 8879e824..de0b23ca 100644 --- a/api/advanced/test-case.md +++ b/api/advanced/test-case.md @@ -106,12 +106,18 @@ interface TaskOptions { readonly shuffle: boolean | undefined readonly retry: number | undefined readonly repeats: number | undefined + readonly tags: string[] | undefined + readonly timeout: number | undefined readonly mode: 'run' | 'only' | 'skip' | 'todo' } ``` 收集测试时使用的选项。 +## tags 4.1.0 {#tags} + +[Tags](/guide/test-tags) that were implicitly or explicitly assigned to the test. + ## ok ```ts diff --git a/api/advanced/test-specification.md b/api/advanced/test-specification.md index 129d7bf1..49b623d9 100644 --- a/api/advanced/test-specification.md +++ b/api/advanced/test-specification.md @@ -11,6 +11,7 @@ const specification = project.createSpecification( testLines: [20, 40], testNamePattern: /hello world/, testIds: ['1223128da3_0_0_0', '1223128da3_0_0'], + testTagsFilter: ['frontend and backend'], } // optional test filters ) ``` @@ -84,6 +85,10 @@ A regexp that matches the name of the test in this module. This value will overr The ids of tasks inside of this specification to run. +## testTagsFilter 4.1.0 {#testtagsfilter} + +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`. + ## toJSON ```ts diff --git a/api/advanced/test-suite.md b/api/advanced/test-suite.md index f4ef8ead..fe2f30cd 100644 --- a/api/advanced/test-suite.md +++ b/api/advanced/test-suite.md @@ -107,6 +107,7 @@ interface TaskOptions { readonly shuffle: boolean | undefined readonly retry: number | undefined readonly repeats: number | undefined + readonly tags: string[] | undefined readonly mode: 'run' | 'only' | 'skip' | 'todo' } ``` diff --git a/api/index.md b/api/index.md index 83b0b7a4..f6ef629e 100644 --- a/api/index.md +++ b/api/index.md @@ -29,10 +29,20 @@ interface TestOptions { * @default 0 */ repeats?: number + /** + * Custom tags of the test. Useful for filtering tests. + */ + tags?: string[] | string } ``` +<<<<<<< HEAD 当测试函数返回一个 promise 时,运行器会等待它解析结束收集异步的结果。如果 promise 被拒绝,测试就会失败。 +======= + + +When 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. +>>>>>>> 4a24a6d9f32c7ad524eb2a1d042d5ee52ed4312b ::: tip 在 Jest 中,`TestFunction` 也可以是 `(done: DoneCallback) => void` 类型。如果使用这种形式,测试将在调用 `done` 之前不会结束。也可以使用 `async` 函数来实现相同的效果,请参阅 [迁移指南中的回调完成部分](/guide/migration.html#done-callback)。 diff --git a/api/vi.md b/api/vi.md index 54e1e77c..4617234e 100644 --- a/api/vi.md +++ b/api/vi.md @@ -257,7 +257,11 @@ function mocked( TypeScript 的类型助手。只返回传入的对象。 +<<<<<<< HEAD 当 `partial` 为 `true` 时,它将期望一个 `Partial` 作为返回值。默认情况下,这只会让 TypeScript 认为第一层的值是模拟的。我们可以将 `{ deep: true }` 作为第二个参数传递给 TypeScript,告诉它整个对象都是模拟的(如果实际上是的话)。 +======= +When `partial` is `true` it will expect a `Partial` as a return value. By default, this will only make TypeScript believe that the first level values are mocked. You can pass down `{ deep: true }` as a second argument to tell TypeScript that the whole object is mocked, if it actually is. You can pass down `{ partial: true, deep: true }` to make nested objects also partial recursively. +>>>>>>> 4a24a6d9f32c7ad524eb2a1d042d5ee52ed4312b ```ts [example.ts] export function add(x: number, y: number): number { @@ -267,6 +271,10 @@ export function add(x: number, y: number): number { export function fetchSomething(): Promise { return fetch('https://vitest.dev/') } + +export function getUser(): { name: string; address: { city: string; zip: string } } { + return { name: 'John', address: { city: 'New York', zip: '10001' } } +} ``` ```ts [example.test.ts] @@ -286,6 +294,13 @@ test('mock return value with only partially correct typing', async () => { }) // vi.mocked(example.someFn).mockResolvedValue({ ok: false }) // 这是一个错误类型 }) + +test('mock return value with deep partial typing', async () => { + vi.mocked(example.getUser, { partial: true, deep: true }).mockReturnValue({ + address: { city: 'Los Angeles' }, + }) + expect(example.getUser().address.city).toBe('Los Angeles') +}) ``` ### vi.importActual diff --git a/config/stricttags.md b/config/stricttags.md new file mode 100644 index 00000000..f26b8134 --- /dev/null +++ b/config/stricttags.md @@ -0,0 +1,35 @@ +--- +title: strictTags | Config +outline: deep +--- + +# strictTags 4.1.0 {#stricttags} + +- **Type:** `boolean` +- **Default:** `true` +- **CLI:** `--strict-tags`, `--no-strict-tags` + +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). + +Note that Vitest will always throw an error if `--tags-filter` flag defines a tag not present in the config. + +For example, this test will throw an error because the tag `fortnend` has a typo (it should be `frontend`): + +::: code-group +```js [form.test.js] +test('renders a form', { tags: ['fortnend'] }, () => { + // ... +}) +``` +```js [vitest.config.js] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + tags: [ + { name: 'frontend' }, + ], + }, +}) +``` +::: diff --git a/config/tags.md b/config/tags.md new file mode 100644 index 00000000..6ef0a77c --- /dev/null +++ b/config/tags.md @@ -0,0 +1,194 @@ +--- +title: tags | Config +outline: deep +--- + +# tags 4.1.0 {#tags} + +- **Type:** `TestTagDefinition[]` +- **Default:** `[]` + +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. + +If you are using [`projects`](/config/projects), they will inherit all global tags definitions automatically. + +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. + +## name + +- **Type:** `string` +- **Required:** `true` + +The name of the tag. This is what you use in the `tags` option in tests. + +```ts +export default defineConfig({ + test: { + tags: [ + { name: 'unit' }, + { name: 'e2e' }, + ], + }, +}) +``` + +::: tip +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`): + +```ts [vitest.shims.ts] +import 'vitest' + +declare module 'vitest' { + interface TestTags { + tags: + | 'frontend' + | 'backend' + | 'db' + | 'flaky' + } +} +``` +::: + +## description + +- **Type:** `string` + +A human-readable description for the tag. This will be shown in UI and inside error messages when a tag is not found. + +```ts +export default defineConfig({ + test: { + tags: [ + { + name: 'slow', + description: 'Tests that take a long time to run.', + }, + ], + }, +}) +``` + +## priority + +- **Type:** `number` +- **Default:** `Infinity` + +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`). + +```ts +export default defineConfig({ + test: { + tags: [ + { + name: 'flaky', + timeout: 30_000, + priority: 1, // higher priority + }, + { + name: 'db', + timeout: 60_000, + priority: 2, // lower priority + }, + ], + }, +}) +``` + +When a test has both tags, the `timeout` will be `30_000` because `flaky` has a higher priority. + +## Test Options + +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. + +### timeout + +- **Type:** `number` + +Test timeout in milliseconds. + +### retry + +- **Type:** `number | { count?: number, delay?: number, condition?: RegExp }` + +Retry configuration for the test. If a number, specifies how many times to retry. If an object, allows fine-grained retry control. + +### repeats + +- **Type:** `number` + +How many times the test will run again. + +### concurrent + +- **Type:** `boolean` + +Whether suites and tests run concurrently. + +### sequential + +- **Type:** `boolean` + +Whether tests run sequentially. + +### skip + +- **Type:** `boolean` + +Whether the test should be skipped. + +### only + +- **Type:** `boolean` + +Should this test be the only one running in a suite. + +### todo + +- **Type:** `boolean` + +Whether the test should be skipped and marked as a todo. + +### fails + +- **Type:** `boolean` + +Whether the test is expected to fail. If it does, the test will pass, otherwise it will fail. + +## Example + +```ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + tags: [ + { + name: 'unit', + description: 'Unit tests.', + }, + { + name: 'e2e', + description: 'End-to-end tests.', + timeout: 60_000, + }, + { + name: 'flaky', + description: 'Flaky tests that need retries.', + retry: process.env.CI ? 3 : 0, + priority: 1, + }, + { + name: 'slow', + description: 'Slow tests.', + timeout: 120_000, + }, + { + name: 'skip-ci', + description: 'Tests to skip in CI.', + skip: !!process.env.CI, + }, + ], + }, +}) +``` diff --git a/guide/cli-generated.md b/guide/cli-generated.md index d605cca3..f843e762 100644 --- a/guide/cli-generated.md +++ b/guide/cli-generated.md @@ -518,12 +518,33 @@ Changes the order in which hooks are executed. Accepted values are: "stack", "li 当指定数量的测试失败时停止测试执行(默认值:`0`) -### retry +### retry.count +<<<<<<< HEAD - **命令行终端:** `--retry ` - **配置:** [retry](/config/retry) 如果测试失败,重试特定次数(默认值: `0`) +======= +- **CLI:** `--retry.count ` +- **Config:** [retry.count](/config/retry#retry-count) + +Number of times to retry a test if it fails (default: `0`) + +### retry.delay + +- **CLI:** `--retry.delay ` +- **Config:** [retry.delay](/config/retry#retry-delay) + +Delay in milliseconds between retry attempts (default: `0`) + +### retry.condition + +- **CLI:** `--retry.condition ` +- **Config:** [retry.condition](/config/retry#retry-condition) + +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) +>>>>>>> 4a24a6d9f32c7ad524eb2a1d042d5ee52ed4312b ### diff.aAnnotation @@ -792,12 +813,31 @@ watch 模式下重新运行测试时清除终端屏幕(默认值:`true`) Start Vitest without running tests. Tests will be running only on change. This option is ignored when CLI file filters are passed. (default: `false`) +### listTags + +- **CLI:** `--listTags [type]` + +List all available tags instead of running tests. `--list-tags=json` will output tags in JSON format, unless there are no tags. + ### clearCache - **命令行终端:** `--clearCache` Delete all Vitest caches, including `experimental.fsModuleCache`, without running any tests. This will reduce the performance in the subsequent test run. +### tagsFilter + +- **CLI:** `--tagsFilter ` + +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. + +### strictTags + +- **CLI:** `--strictTags` +- **Config:** [strictTags](/config/stricttags) + +Should Vitest throw an error if test has a tag that is not defined in the config. (default: `true`) + ### experimental.fsModuleCache - **命令行终端:** `--experimental.fsModuleCache` diff --git a/guide/filtering.md b/guide/filtering.md index 0d953f5f..533b1c66 100644 --- a/guide/filtering.md +++ b/guide/filtering.md @@ -93,7 +93,29 @@ describe('suite', () => { }) ``` +<<<<<<< HEAD ## 选择要运行的测试套件和测试 {#selecting-suites-and-tests-to-run} +======= +## Filtering Tags + +If your test defines a [tag](/guide/test-tags), you can filter your tests with a `--tags-filter` option: + +```ts +test('renders a form', { tags: ['frontend'] }, () => { + // ... +}) + +test('calls an external API', { tags: ['backend'] }, () => { + // ... +}) +``` + +```shell +vitest --tags-filter=frontend +``` + +## Selecting Suites and Tests to Run +>>>>>>> 4a24a6d9f32c7ad524eb2a1d042d5ee52ed4312b 使用 `.only` 仅运行某些测试套件或测试 diff --git a/guide/test-tags.md b/guide/test-tags.md new file mode 100644 index 00000000..46367325 --- /dev/null +++ b/guide/test-tags.md @@ -0,0 +1,302 @@ +--- +title: Test Tags | Guide +outline: deep +--- + +# Test Tags 4.1.0 + +[`Tags`](/config/tags) allow you to mark tests and change their options based on the tag's definition. + +## Defining Tags + +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. + +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). + +```ts [vitest.config.js] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + tags: [ + { + name: 'frontend', + description: 'Tests written for frontend.', + }, + { + name: 'backend', + description: 'Tests written for backend.', + }, + { + name: 'db', + description: 'Tests for database queries.', + timeout: 60_000, + }, + { + name: 'flaky', + description: 'Flaky CI tests.', + retry: process.env.CI ? 3 : 0, + timeout: 30_000, + priority: 1, + }, + ], + }, +}) +``` + +::: warning +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: + +```ts +test('flaky database test', { tags: ['flaky', 'db'] }) +// { timeout: 30_000, retry: 3 } +``` + +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. + +If test defines its own options, they will have the highest priority: + +```ts +test('flaky database test', { tags: ['flaky', 'db'], timeout: 120_000 }) +// { timeout: 120_000, retry: 3 } +``` +::: + +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`): + +```ts [vitest.shims.ts] +import 'vitest' + +declare module 'vitest' { + interface TestTags { + tags: + | 'frontend' + | 'backend' + | 'db' + | 'flaky' + } +} +``` + +To see all your tags, you can use [`--list-tags`](/guide/cli#listtags) command: + +```shell +vitest --list-tags + +frontend: Tests written for frontend. +backend: Tests written for backend. +db: Tests for database queries. +flaky: Flaky CI tests. +``` + +To print it in JSON, pass down `--list-tags=json`: + +```json +{ + "tags": [ + { + "name": "frontend", + "description": "Tests written for frontend." + }, + { + "name": "backend", + "description": "Tests written for backend." + }, + { + "name": "db", + "description": "Tests for database queries.", + "timeout": 60000 + }, + { + "name": "flaky", + "description": "Flaky CI tests.", + "retry": 0, + "timeout": 30000, + "priority": 1 + } + ], + "projects": [] +} +``` + +## Using Tags in Tests + +You can apply tags to individual tests or entire suites using the `tags` option: + +```ts +import { describe, test } from 'vitest' + +test('renders homepage', { tags: ['frontend'] }, () => { + // ... +}) + +describe('API endpoints', { tags: ['backend'] }, () => { + test('returns user data', () => { + // This test inherits the "backend" tag from the parent suite + }) + + test('validates input', { tags: ['validation'] }, () => { + // This test has both "backend" (inherited) and "validation" tags + }) +}) +``` + +Tags are inherited from parent suites, so all tests inside a tagged `describe` block will automatically have that tag. + +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: + +```ts +/** + * Auth tests + * @module-tag admin/pages/dashboard + * @module-tag acceptance + */ + +test('dashboard renders items', () => { + // ... +}) +``` + +::: danger +A `@module-tag` in a JSDoc comment applies to all tests in that file, not just the test it precedes. + +Consider this example: + +```js{3,10} +describe('forms', () => { + /** + * @module-tag frontend + */ + test('renders a form', () => { + // ... + }) + + /** + * @module-tag db + */ + test('db returns users', () => { + // ... + }) +}) +``` + +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: + +```js{2,6} +describe('forms', () => { + test('renders a form', { tags: 'frontend' }, () => { + // ... + }) + + test('db returns users', { tags: 'db' }, () => { + // ... + }) +}) +``` +::: + +## Filtering Tests by Tag + +To run only tests with specific tags, use the [`--tags-filter`](/guide/cli#tagsfilter) CLI option: + +```shell +vitest --tags-filter=frontend +vitest --tags-filter="frontend and backend" +``` + +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: + +The tags filter in Vitest UI +The tags filter in Vitest UI + +If you are using a programmatic API, you can pass down a `tagsFilter` option to [`startVitest`](/guide/advanced/#startvitest) or [`createVitest`](/guide/advanced/#createvitest): + +```ts +import { startVitest } from 'vitest/node' + +await startVitest('test', [], { + tagsFilter: ['frontend and backend'], +}) +``` + +Or you can create a [test specification](/api/advanced/test-specification) with your custom filters: + +```ts +const specification = vitest.getRootProject().createSpecification( + '/path-to-file.js', + { + testTagsFilter: ['frontend and backend'], + }, +) +``` + +### Syntax + +You can combine tags in different ways. Vitest supports these keywords: + +- `and` or `&&` to include both expressions +- `or` or `||` to include at least one expression +- `not` or `!` to exclude the expression +- `*` to match any number of characters (0 or more) +- `()` to group expressions and override precedence + +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. + +::: warning Reserved Names +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. +::: + +### Wildcards + +You can use a wildcard (`*`) to match any number of characters: + +```shell +vitest --tags-filter="unit/*" +``` + +This will match tags like `unit/components`, `unit/utils`, etc. + +### Excluding Tags + +To exclude tests with a specific tag, add an exclamation mark (`!`) at the start or a "not" keyword: + +```shell +vitest --tags-filter="!slow and not flaky" +``` + +### Examples + +Here are some common filtering patterns: + +```shell +# Run only unit tests +vitest --tags-filter="unit" + +# Run tests that are both frontend AND fast +vitest --tags-filter="frontend and fast" + +# Run tests that are either unit OR e2e +vitest --tags-filter="unit or e2e" + +# Run all tests except slow ones +vitest --tags-filter="!slow" + +# Run frontend tests that are not flaky +vitest --tags-filter="frontend && !flaky" + +# Run tests matching a wildcard pattern +vitest --tags-filter="api/*" + +# Complex expression with parentheses +vitest --tags-filter="(unit || e2e) && !slow" + +# Run database tests that are either postgres or mysql, but not slow +vitest --tags-filter="db && (postgres || mysql) && !slow" +``` + +You can also pass multiple `--tags-filter` flags. They are combined with AND logic: + +```shell +# Run tests that match (unit OR e2e) AND are NOT slow +vitest --tags-filter="unit || e2e" --tags-filter="!slow" +``` diff --git a/public/ui/dark-ui-tags.png b/public/ui/dark-ui-tags.png new file mode 100644 index 00000000..de207474 Binary files /dev/null and b/public/ui/dark-ui-tags.png differ diff --git a/public/ui/light-ui-tags.png b/public/ui/light-ui-tags.png new file mode 100644 index 00000000..baa27d4a Binary files /dev/null and b/public/ui/light-ui-tags.png differ