diff --git a/.vitepress/config.ts b/.vitepress/config.ts
index 17ce4ca2..a7f5075c 100644
--- a/.vitepress/config.ts
+++ b/.vitepress/config.ts
@@ -808,9 +808,9 @@ export default ({ mode }: { mode: string }) => {
docFooterText: '追踪查看器 | 浏览器模式',
},
{
- text: 'Playwright Traces',
+ text: 'Playwright 追踪',
link: '/guide/browser/playwright-traces',
- docFooterText: 'Playwright Traces | Browser Mode',
+ docFooterText: 'Playwright 追踪 | 浏览器模式',
},
{
text: 'ARIA 快照',
@@ -819,32 +819,21 @@ export default ({ mode }: { mode: string }) => {
},
],
},
+ // Authoring — how to express a test in code: constructing it,
+ // asserting, mocking dependencies, attaching metadata. The page is
+ // about *test content*, not the runner. Discriminator: "How do I
+ // write X in a test?" If yes, it belongs here. Mocking sub-pages
+ // live nested because they're a multi-page subtopic.
{
- text: '指南',
+ text: '编写测试',
collapsed: false,
items: [
- {
- text: '命令行界面',
- link: '/guide/cli',
- },
- {
- text: '测试筛选',
- link: '/guide/filtering',
- },
- {
- text: '测试标签',
- link: '/guide/test-tags',
- },
{
text: '测试上下文',
link: '/guide/test-context',
},
{
- text: '测试环境',
- link: '/guide/environment',
- },
- {
- text: '测试运行生命周期',
+ text: '运行生命周期',
link: '/guide/lifecycle',
},
{
@@ -891,45 +880,85 @@ export default ({ mode }: { mode: string }) => {
],
},
{
- text: '并行测试',
- link: '/guide/parallelism',
- },
- {
- text: '测试项目',
- link: '/guide/projects',
+ text: '测试',
+ link: '/guide/test-tags',
},
{
- text: '报告器',
- link: '/guide/reporters',
+ text: '测试注释',
+ link: '/guide/test-annotations',
},
{
- text: '覆盖率',
- link: '/guide/coverage',
+ text: '扩展断言',
+ link: '/guide/extending-matchers',
},
{
text: '类型测试',
link: '/guide/testing-types',
},
- {
- text: 'UI 模式',
- link: '/guide/ui',
- },
{
text: '内联测试',
link: '/guide/in-source',
},
+ ],
+ },
+ // Workflow — how to invoke, select, and orchestrate test runs
+ // across files/projects/processes. The page is about the *runner
+ // and tooling around it*, not what's inside a test. Discriminator:
+ // "How do I run / filter / parallelize / integrate Vitest?" If a
+ // page is about the runtime environment of the tests themselves
+ // (jsdom, node), it still belongs here — that's a workflow choice.
+ {
+ text: '工作流',
+ collapsed: false,
+ items: [
{
- text: '测试注释',
- link: '/guide/test-annotations',
+ text: '命令行界面',
+ link: '/guide/cli',
},
{
- text: '扩展断言',
- link: '/guide/extending-matchers',
+ text: '测试筛选',
+ link: '/guide/filtering',
+ },
+ {
+ text: '测试项目',
+ link: '/guide/projects',
+ },
+ {
+ text: '测试环境',
+ link: '/guide/environment',
+ },
+ {
+ text: '并行测试',
+ link: '/guide/parallelism',
+ },
+ {
+ text: '报告器',
+ link: '/guide/reporters',
+ },
+ {
+ text: 'UI 模式',
+ link: '/guide/ui',
},
{
text: 'IDE 插件',
link: '/guide/ide',
},
+ ],
+ },
+ // Quality & Debugging — how to verify the test run is healthy and
+ // diagnose it when it isn't. Coverage, perf, leak detection, error
+ // triage, observability. Discriminator: "Is my suite good?" or
+ // "Why did this fail / leak / slow down?" If a page primarily
+ // measures or fixes the suite (rather than authoring or running
+ // it), put it here.
+ {
+ text: '质量与调试',
+ collapsed: false,
+ items: [
+ {
+ text: '覆盖率',
+ link: '/guide/coverage',
+ },
{
text: '调试',
link: '/guide/debugging',
@@ -938,25 +967,6 @@ export default ({ mode }: { mode: string }) => {
text: '常见错误',
link: '/guide/common-errors',
},
- {
- text: '迁移指南',
- link: '/guide/migration',
- collapsed: false,
- items: [
- {
- text: '迁移到 Vitest 4',
- link: '/guide/migration#vitest-4',
- },
- {
- text: '从 Jest 迁移',
- link: '/guide/migration#jest',
- },
- {
- text: '从 Mocha + Chai + Sinon 迁移',
- link: '/guide/migration#mocha-chai-sinon',
- },
- ],
- },
{
text: '性能',
collapsed: false,
@@ -977,6 +987,62 @@ export default ({ mode }: { mode: string }) => {
},
],
},
+ // Recipes — end-to-end patterns that solve a concrete problem by
+ // combining multiple features. Each entry is titled by the problem
+ // ("Database Transaction per Test"), not the feature. Add a recipe
+ // when a single feature page would over-explain, when the value
+ // comes from composition, or when users would search by intent
+ // rather than by API name.
+ {
+ text: '技巧',
+ collapsed: false,
+ items: [
+ {
+ text: 'Database Transaction per Test',
+ link: '/guide/recipes/db-transaction',
+ },
+ {
+ text: 'Cancelling Long-Running Operations Gracefully',
+ link: '/guide/recipes/cancellable',
+ },
+ {
+ text: 'Waiting for Async Conditions',
+ link: '/guide/recipes/wait-for',
+ },
+ {
+ text: 'Type Narrowing in Tests',
+ link: '/guide/recipes/type-narrowing',
+ },
+ {
+ text: 'Custom Assertion Helpers',
+ link: '/guide/recipes/custom-assertions',
+ },
+ {
+ text: 'Watching Non-Imported Files',
+ link: '/guide/recipes/watch-templates',
+ },
+ {
+ text: 'Extending Browser Locators',
+ link: '/guide/recipes/browser-locators',
+ },
+ {
+ text: 'Schema-Driven Assertions',
+ link: '/guide/recipes/schema-matching',
+ },
+ {
+ text: 'Auto-Cleanup with `using`',
+ link: '/guide/recipes/explicit-resources',
+ },
+ {
+ text: 'Per-File Isolation Settings',
+ link: '/guide/recipes/disable-isolation',
+ },
+ {
+ text: 'Parallel and Sequential Test Files',
+ link: '/guide/recipes/parallel-sequential',
+ },
+ ],
+ },
{
text: '高级指南',
collapsed: true,
@@ -999,12 +1065,31 @@ export default ({ mode }: { mode: string }) => {
},
],
},
+ // Migration — one-time transitional content: cross-version
+ // upgrades and porting from other test runners (Jest, Mocha).
+ // Sits near the bottom because it's not daily-use and would push
+ // active-use guides further from the user's first scroll.
{
+ text: '迁移',
+ link: '/guide/migration',
+ collapsed: false,
items: [
{
- text: '测试技巧',
- link: '/guide/recipes',
+ text: '迁移至 Vitest 4.0',
+ link: '/guide/migration#vitest-4',
},
+ {
+ text: '从 Jest 迁移',
+ link: '/guide/migration#jest',
+ },
+ {
+ text: '从 Mocha + Chai + Sinon 迁移',
+ link: '/guide/migration#mocha-chai-sinon',
+ },
+ ],
+ },
+ {
+ items: [
{
text: '测试框架比较',
link: '/guide/comparisons',
diff --git a/api/advanced/vitest.md b/api/advanced/vitest.md
index bd0857e1..bd75a509 100644
--- a/api/advanced/vitest.md
+++ b/api/advanced/vitest.md
@@ -8,7 +8,7 @@ title: Vitest API
Vitest 实例需要当前的测试模式。它可以是以下之一:
- `test`:运行运行时测试时
-- `benchmark`:运行基准测试时 实验性
+- `benchmark`:运行基准测试时
::: details New in Vitest 4
Vitest 4 新增了多个 API(它们都标记有 "4.0.0+" 徽章),并移除了已弃用的 API:
@@ -31,7 +31,7 @@ Vitest 4 新增了多个 API(它们都标记有 "4.0.0+" 徽章),并移除
测试模式只会调用 `test` 或 `it` 中的函数,并在遇到 `bench` 时抛出错误。此模式使用配置中的 `include` 和 `exclude` 选项来查找测试文件。
-### benchmark 实验性 {#benchmark-experimental}
+### benchmark {#benchmark}
基准测试模式调用 `bench` 函数,并在遇到 `test` 或 `it` 时抛出错误。此模式使用配置中的 `benchmark.include` 和 `benchmark.exclude` 选项来查找基准测试文件。
@@ -47,7 +47,7 @@ Vitest 4 新增了多个 API(它们都标记有 "4.0.0+" 徽章),并移除
这是全局的 [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver)。
-## state 实验性 {#state-experimental}
+## state {#state}
::: warning
公共 `state` 是一个实验性 API(除了 `vitest.state.getReportedEntity`)。破坏性更改可能不遵循 SemVer,请在使用时固定 Vitest 的版本。
diff --git a/api/browser/assertions.md b/api/browser/assertions.md
index babe3e7a..f31eb6af 100644
--- a/api/browser/assertions.md
+++ b/api/browser/assertions.md
@@ -1003,7 +1003,7 @@ await expect.element(queryByTestId('prev')).not.toHaveSelection()
await expect.element(queryByTestId('next')).toHaveSelection('ne')
```
-## toMatchScreenshot 实验性 {#tomatchscreenshot}
+## toMatchScreenshot {#tomatchscreenshot}
```ts
function toMatchScreenshot(
diff --git a/api/vi.md b/api/vi.md
index 76990d30..0530efb4 100644
--- a/api/vi.md
+++ b/api/vi.md
@@ -1055,7 +1055,7 @@ vi.useRealTimers()
### vi.useFakeTimers
```ts
-function useFakeTimers(config?: FakeTimerInstallOpts): Vitest
+function useFakeTimers(config?: FakeTimersConfig): Vitest
```
要启用模拟定时器,需要调用此方法。在调用 [`vi.useRealTimers()`](#vi-userealtimers) 之前,它将封装所有对定时器的进一步调用(如 `setTimeout` 、`setInterval` 、`clearTimeout` 、`clearInterval` 、`setImmediate` 、`clearImmediate` 和 `Date`)。
@@ -1068,6 +1068,16 @@ function useFakeTimers(config?: FakeTimerInstallOpts): Vitest
`vi.useFakeTimers()` 不再自动模拟 `process.nextTick` 。
仍然可以通过在 `toFake` 参数中指定选项来模拟: `vi.useFakeTimers({ toFake: ['nextTick', 'queueMicrotask'] })` 。
:::
+
+You can use `toFake` to specify which timers to mock, or `toNotFake` to specify which timers to keep native. Note that `toFake` and `toNotFake` cannot be specified together.
+
+```ts
+// only mock setTimeout and clearTimeout
+vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
+
+// mock all timers except setInterval
+vi.useFakeTimers({ toNotFake: ['setInterval'] })
+```
### vi.setTimerTickMode 4.1.0 {#vi-settimertickmode}
@@ -1342,7 +1352,7 @@ function resetConfig(): void
如果之前调用过 [`vi.setConfig`](#vi-setconfig) ,则会将配置重置为原始状态。
-### vi.defineHelper 4.1.0 {#vi-defineHelper}
+### vi.defineHelper 4.1.0 {#vi-definehelper}
```ts
function defineHelper any>(fn: F): F
diff --git a/blog/vitest-3-2.md b/blog/vitest-3-2.md
index a8de2683..da0475b8 100644
--- a/blog/vitest-3-2.md
+++ b/blog/vitest-3-2.md
@@ -219,7 +219,7 @@ export default defineConfig({
test: {
watchTriggerPatterns: [
{
- pattern: /^src\/templates\/(.*)\.(ts|html|txt)$/,
+ pattern: /src\/templates\/(.*)\.(ts|html|txt)$/,
testsToRun: (file, match) => {
return `api/tests/mailers/${match[2]}.test.ts`
},
diff --git a/config/browser/webdriverio.md b/config/browser/webdriverio.md
index f1f3d302..7febaaa4 100644
--- a/config/browser/webdriverio.md
+++ b/config/browser/webdriverio.md
@@ -62,3 +62,21 @@ export default defineConfig({
请注意,Vitest 将忽略 `capabilities.browserName` — 请改用 [`test.browser.instances.browser`](/config/browser/instances#browser)。
:::
+
+## Headful Chrome in CI
+
+Vitest enables [`browser.headless`](/config/browser/headless) automatically in CI.
+If you explicitly set `headless: false` for Chrome on a Linux CI runner, Chrome
+still needs a display server. Without one, WebDriverIO or ChromeDriver can fail
+with a misleading error such as `session not created: probably user data
+directory is already in use`.
+
+Run the test command through `xvfb-run` when you need headful Chrome in GitHub
+Actions or another Linux CI environment:
+
+```bash
+xvfb-run npm test
+```
+
+Alternatively, keep `browser.headless` enabled in CI and use headful mode only
+for local debugging.
diff --git a/config/faketimers.md b/config/faketimers.md
index b01c550f..f92d1400 100644
--- a/config/faketimers.md
+++ b/config/faketimers.md
@@ -5,7 +5,7 @@ outline: deep
# fakeTimers
-- **类型:** `FakeTimerInstallOpts`
+- **类型:** `FakeTimerConfig`
当使用 [`vi.useFakeTimers()`](/api/vi#vi-usefaketimers) 时,Vitest 会将此选项传递给 [`@sinon/fake-timers`](https://npmx.dev/package/@sinonjs/fake-timers)。
@@ -21,11 +21,22 @@ outline: deep
- **类型:** `('setTimeout' | 'clearTimeout' | 'setImmediate' | 'clearImmediate' | 'setInterval' | 'clearInterval' | 'Date' | 'nextTick' | 'hrtime' | 'requestAnimationFrame' | 'cancelAnimationFrame' | 'requestIdleCallback' | 'cancelIdleCallback' | 'performance' | 'queueMicrotask')[]`
- **默认值:** 全局可用的所有方法,除了 `nextTick` 和 `queueMicrotask`
-需要模拟的全局方法和 API 名称数组。
-
-如果仅需模拟 `setTimeout()` 和 `nextTick()`,可将此属性指定为 `['setTimeout', 'nextTick']`。
+需要模拟的全局方法和 API 名称数组。例如仅需模拟 `setTimeout()` 和 `nextTick()`,可将此属性指定为 `['setTimeout', 'nextTick']`。
当使用 `--pool=forks` 在 `node:child_process` 中运行 Vitest 时,不支持模拟 `nextTick`。NodeJS 会在 `node:child_process` 内部使用 `process.nextTick`,模拟后会导致进程挂起。使用 `--pool=threads` 运行 Vitest 时支持模拟 `nextTick`。
+
+## fakeTimers.toNotFake
+
+- **Type:** `('setTimeout' | 'clearTimeout' | 'setImmediate' | 'clearImmediate' | 'setInterval' | 'clearInterval' | 'Date' | 'nextTick' | 'hrtime' | 'requestAnimationFrame' | 'cancelAnimationFrame' | 'requestIdleCallback' | 'cancelIdleCallback' | 'performance' | 'queueMicrotask')[]`
+- **Default:** `[]`
+
+An array with names of global methods and APIs to keep native. All other available timers will be mocked. For example, to keep `setInterval()` native and mock all other timers, specify this property as `['setInterval']`.
+
+Mocking `nextTick` is not supported when running Vitest inside `node:child_process` by using `--pool=forks`. When running with `--pool=forks`, Vitest automatically adds `nextTick` to the `toNotFake` array.
+
+::: warning
+Using both `toFake` and `toNotFake` together is not supported.
+:::
## fakeTimers.loopLimit
diff --git a/config/watchtriggerpatterns.md b/config/watchtriggerpatterns.md
index 56d6a3f5..04c537b4 100644
--- a/config/watchtriggerpatterns.md
+++ b/config/watchtriggerpatterns.md
@@ -18,7 +18,7 @@ export default defineConfig({
test: {
watchTriggerPatterns: [
{
- pattern: /^src\/(mailers|templates)\/(.*)\.(ts|html|txt)$/,
+ pattern: /src\/(mailers|templates)\/(.*)\.(ts|html|txt)$/,
testsToRun: (id, match) => {
// 相对于根路径
return `./api/tests/mailers/${match[2]}.test.ts`
diff --git a/guide/browser/aria-snapshots.md b/guide/browser/aria-snapshots.md
index 4766c4d7..aba1d5a6 100644
--- a/guide/browser/aria-snapshots.md
+++ b/guide/browser/aria-snapshots.md
@@ -3,7 +3,7 @@ title: ARIA 快照 | 指南
outline: deep
---
-# ARIA 快照 4.1.4 {#aria-snapshots}
+# ARIA 快照 4.1.4 {#aria-snapshots}
ARIA 快照可以让你测试页面的无障碍结构。你不是去断言原始 HTML 或视觉输出,而是去断言无障碍树,也就是屏幕阅读器和其他辅助技术所使用的那套结构。
diff --git a/guide/browser/playwright-traces.md b/guide/browser/playwright-traces.md
index 72135180..1bc1dc83 100644
--- a/guide/browser/playwright-traces.md
+++ b/guide/browser/playwright-traces.md
@@ -1,9 +1,9 @@
-# Playwright 跟踪 {#playwright-traces}
+# Playwright 追踪 {#playwright-traces}
-Vitest 浏览器模式支持生成 Playwright 的 [跟踪文件](https://playwright.dev/docs/trace-viewer#viewing-remote-traces)。要启用跟踪,请在 `test.browser` 配置中设置 [`trace`](/config/browser/trace) 选项。
+Vitest 浏览器模式支持生成 Playwright 的 [追踪文件](https://playwright.dev/docs/trace-viewer#viewing-remote-traces)。要启用追踪,请在 `test.browser` 配置中设置 [`trace`](/config/browser/trace) 选项。
::: warning
-生成跟踪文件仅在 [Playwright provider](/config/browser/playwright) 下可用。
+生成追踪文件仅在 [Playwright provider](/config/browser/playwright) 下可用。
:::
::: code-group
@@ -28,7 +28,7 @@ vitest --browser.trace=on
:::
-默认情况下,Vitest 会为每个测试生成一个跟踪文件。你也可以将 `trace` 设置为 `'on-first-retry'`、`'on-all-retries'` 或 `'retain-on-failure'`,使其仅在测试失败时生成跟踪。跟踪文件将保存在测试文件旁边的 `__traces__` 文件夹中。跟踪文件的名称包含项目名称、测试名称、[`repeats`](/api/test#repeats) 计数和 [`retry`](/api/test#retry) 计数:
+默认情况下,Vitest 会为每个测试生成一个追踪文件。你也可以将 `trace` 设置为 `'on-first-retry'`、`'on-all-retries'` 或 `'retain-on-failure'`,使其仅在测试失败时生成追踪。追踪文件将保存在测试文件旁边的 `__traces__` 文件夹中。追踪文件的名称包含项目名称、测试名称、[`repeats`](/api/test#repeats) 计数和 [`retry`](/api/test#retry) 计数:
```
chromium-my-test-0-0.trace.zip
@@ -38,7 +38,7 @@ chromium-my-test-0-0.trace.zip
^ retry count
```
-如果要更改输出目录,你可以在 `test.browser.trace` 配置中设置 `tracesDir` 参数。这样所有跟踪文件将按测试文件分组,存储在同一个目录中,。
+如果要更改输出目录,你可以在 `test.browser.trace` 配置中设置 `tracesDir` 参数。这样所有追踪文件将按测试文件分组,存储在同一个目录中,。
```ts [vitest.config.js]
import { defineConfig } from 'vitest/config'
@@ -58,13 +58,13 @@ export default defineConfig({
})
```
-跟踪文件在报告器中以 [注释](/guide/test-annotations) 形式提供。例如,在 HTML 报告器中,你可以在测试详情中找到指向跟踪文件的链接。
+追踪文件在报告器中以 [注释](/guide/test-annotations) 形式提供。例如,在 HTML 报告器中,你可以在测试详情中找到指向追踪文件的链接。
-## 跟踪标记 {#trace-markers}
+## 追踪标记 {#trace-markers}
你也可以使用 `page.mark(name, callback)` 将多个操作分组在一个标记下:
-你可以添加显式的命名标记,让跟踪时间线更易于阅读:
+你可以添加显式的命名标记,让追踪时间线更易于阅读:
```ts
import { page } from 'vitest/browser'
@@ -88,7 +88,7 @@ await page.mark('sign in flow', async () => {
})
```
-你还可以使用 [`vi.defineHelper()`](/api/vi#vi-defineHelper) 包装可复用的工具函数,以便跟踪条目会指向工具函数的调用位置,而不是具体内部实现:
+你还可以使用 [`vi.defineHelper()`](/api/vi#vi-defineHelper) 包装可复用的工具函数,以便追踪条目会指向工具函数的调用位置,而不是具体内部实现:
```ts
import { vi } from 'vitest'
@@ -106,18 +106,18 @@ test('renders content', async () => {
## 预览 {#preview}
-打开跟踪文件,你可以使用 Playwright Trace Viewer。在终端中运行以下命令:
+打开追踪文件,你可以使用 Playwright Trace Viewer。在终端中运行以下命令:
```bash
npx playwright show-trace "path-to-trace-file"
```
-这将启动 Trace Viewer 并加载指定的跟踪文件。
+这将启动 Trace Viewer 并加载指定的追踪文件。
-或者,你可以在浏览器中打开 https://trace.playwright.dev 并在此处上传跟踪文件。
+或者,你可以在浏览器中打开 https://trace.playwright.dev 并在此处上传追踪文件。
-
-
+
+
## 源码位置 {#source-location}
@@ -126,6 +126,6 @@ npx playwright show-trace "path-to-trace-file"
- `expect.element(...)` 断言
- 交互式操作,如 `click`、`fill`、`type`、`hover`、`selectOptions`、`upload`、`dragAndDrop`、`tab`、`keyboard`、`wheel` 和截图
-在底层,Playwright 仍然像往常一样记录其自己的底层操作事件。Vitest 用源码位置组包装它们,这样你可以直接从跟踪时间线跳转到测试中的相关行。
+在底层,Playwright 仍然像往常一样记录其自己的底层操作事件。Vitest 用源码位置组包装它们,这样你可以直接从追踪时间线跳转到测试中的相关行。
-对于未自动覆盖的内容,你可以使用 `page.mark()` 或 `locator.mark()` 添加自己的跟踪组,详情请参阅上文 [跟踪标记](#trace-markers)。
+对于未自动覆盖的内容,你可以使用 `page.mark()` 或 `locator.mark()` 添加自己的追踪组,详情请参阅上文 [追踪标记](#trace-markers)。
diff --git a/guide/debugging.md b/guide/debugging.md
index 6cef7cec..5c46aef3 100644
--- a/guide/debugging.md
+++ b/guide/debugging.md
@@ -27,6 +27,8 @@ ndb npm run test
:::
## VS Code
+
+The [official VS Code](https://vitest.dev/vscode) extension supports debugging tests via "Debug Tests" button. However Vitest also exposes tools to define a custom configuration.
在 VSCode 中快速调试测试的方法是通过 `JavaScript Debug Terminal` 。打开一个新的 `JavaScript Debug Terminal` 并直接运行 `npm run test` 或 `vitest` 。*这适用于在 Node 中运行的任何代码,因此将适用于大多数 JS 测试框架*。
@@ -57,8 +59,10 @@ ndb npm run test
然后在调试选项卡中确保选择 'Debug Current Test File',然后你可以打开要调试的测试文件并按 F5 开始调试。
### 浏览器模式 {#browser-mode}
+
+The simplest way to debug browser tests is to use the [official VS Code](https://vitest.dev/vscode) extension.
-要调试 [Vitest 浏览器模式](/guide/browser/),请在 CLI 中传递 `--inspect` 或 `--inspect-brk`,或在 Vitest 配置中定义它:
+However you can also pass `--inspect` or `--inspect-brk` in CLI or define it in your Vitest configuration:
::: code-group
```bash [CLI]
diff --git a/guide/features.md b/guide/features.md
index 19bc5191..136affe0 100644
--- a/guide/features.md
+++ b/guide/features.md
@@ -220,7 +220,7 @@ if (import.meta.vitest) {
了解更多信息 [源码内联测试](/guide/in-source)
-## 基准测试 实验性 {#benchmarking}
+## 基准测试 {#benchmarking}
你可以使用 [`bench`](/api/test#bench) 运行基准测试通过 [Tinybench](https://github.com/tinylibs/tinybench) 函数来比较基准测试结果。
@@ -247,7 +247,7 @@ describe('sort', () => {
-## 类型测试 实验性 {#type-testing}
+## 类型测试 {#type-testing}
你可以 [编写测试](/guide/testing-types) 来捕获类型回归。 Vitest 附带 [`expect-type`](https://github.com/mmkal/expect-type) 包,为你提供类似且易于理解的 API。
diff --git a/guide/improving-performance.md b/guide/improving-performance.md
index 4400eabe..a640d81f 100644
--- a/guide/improving-performance.md
+++ b/guide/improving-performance.md
@@ -173,7 +173,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 20
+ node-version: 24
- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
@@ -204,7 +204,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 20
+ node-version: 24
- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
diff --git a/guide/migration.md b/guide/migration.md
index 742d76d0..a07ce24d 100644
--- a/guide/migration.md
+++ b/guide/migration.md
@@ -483,8 +483,8 @@ export default defineConfig({
```
:::
-
-更多示例请参阅 [测试技巧](/guide/recipes)。
+
+See [Per-File Isolation Settings](/guide/recipes/disable-isolation) and [Parallel and Sequential Test Files](/guide/recipes/parallel-sequential) for more examples.
### 报告器升级 {#reporter-updates}
@@ -715,7 +715,7 @@ export default defineConfig({
否则快照中会出现大量转义的 `"` 字符。
-### 自定义快照匹配器 4.1.3 {#custom-snapshot-matchers}
+### 自定义快照匹配器 4.1.3 {#custom-snapshot-matcher}
Jest 从 `jest-snapshot` 导入快照组合函数。在 Vitest 中,请改用 `vitest` 中的 `Snapshots`:
diff --git a/guide/recipes/browser-locators.md b/guide/recipes/browser-locators.md
new file mode 100644
index 00000000..91e8445c
--- /dev/null
+++ b/guide/recipes/browser-locators.md
@@ -0,0 +1,116 @@
+---
+title: Extending Browser Locators | Recipes
+---
+
+# Domain Locators
+
+Built-in [locators](/api/browser/locators) like `getByRole` and `getByText` cover queries that map onto accessibility attributes. They run out when an app has shapes that don't fit ARIA, like a "comment with N replies" or a row in a custom table component.
+
+The fallback is to use `querySelector`. That works, but the result is a plain query rather than a locator, so you lose auto-retry and strict-mode protection.
+
+[`locators.extend`](/api/browser/locators#custom-locators) 3.2.0 adds a domain-specific locator without giving up the locator API. The value the method returns is still a locator, so auto-retry, strict-mode protection, and chaining all carry through to your custom methods. The names you give those methods become part of the team's test vocabulary: `page.getByCard({ id: 'product-1' })` reads like the product instead of the DOM, and the same name shows up consistently across the suite.
+
+## Returning a Playwright string
+
+The simplest form returns a [Playwright locator string](https://playwright.dev/docs/other-locators). Vitest treats the returned string as a child query of whatever locator the method was called on: when called on `page`, the string runs against the entire page; when called on a parent locator, it runs scoped to that parent's subtree.
+
+Reach for this form when the new query has no good expression in built-in locators, like a CSS-with-text selector for a widget that doesn't map onto a built-in role, or an XPath for a legacy component you don't control.
+
+```ts
+import { locators } from 'vitest/browser'
+
+locators.extend({
+ getByCommentsCount(count: number) {
+ return `.comments :text("${count} comments")`
+ },
+})
+```
+
+```ts
+import { expect, test } from 'vitest'
+import { page } from 'vitest/browser'
+
+test('article shows comment count', async () => {
+ await expect.element(page.getByCommentsCount(1)).toBeVisible()
+ await expect.element(
+ page.getByRole('article', { name: 'Hello World' })
+ .getByCommentsCount(1)
+ ).toBeVisible()
+})
+```
+
+## Composing existing locators
+
+When you return a locator instead of a string, Vitest uses that locator directly. Inside the extension, `this` is bound to the locator the method was called on (or to `page` for top-level calls), so you can chain existing locators or apply `filter` to express relationships between elements that no single built-in option captures.
+
+The example below uses `filter({ has })` to narrow a row locator to those that contain a button with a given name, encoding a common per-row-actions pattern as a single named lookup:
+
+```ts
+import { locators } from 'vitest/browser'
+import type { Locator } from 'vitest/browser'
+
+locators.extend({
+ getRowWithAction(this: Locator, action: string) {
+ return this.getByRole('row').filter({
+ has: this.getByRole('button', { name: action }),
+ })
+ },
+})
+```
+
+```ts
+await page.getRowWithAction('Delete').first().click()
+```
+
+Prefer this over the raw-string form when both options can express the query. Built-in locators encode accessibility-aware lookups, and chaining or filtering them preserves those guarantees. Reach for the raw-string form only when no chain of built-ins covers the query, since the string runs whatever selector you wrote and bypasses the locator mechanism you're trying to keep.
+
+## Custom interactions
+
+Methods that perform an interaction instead of returning a locator also work. This is the same mechanism used for shaping your own DSL of user actions, defined alongside your queries so the test vocabulary stays consistent.
+
+`locators.extend` types `this` as `BrowserPage | Locator`, since custom methods are reachable from both. For query helpers that's fine, since `getByRole` and other query methods exist on both. For interaction helpers it isn't: `page` has no `click` or `fill`, so calling `page.clickAndFill('x')` would fail at runtime. Guard against that by comparing `this` against the `page` singleton, which lets TypeScript narrow `this` to `Locator` after the throw:
+
+```ts
+import { locators, page } from 'vitest/browser'
+import type { BrowserPage, Locator } from 'vitest/browser'
+
+locators.extend({
+ async clickAndFill(this: BrowserPage | Locator, text: string) {
+ if (this === page) {
+ throw new TypeError(
+ 'clickAndFill must be called on a locator, like page.getByRole(\'textbox\').clickAndFill(...)',
+ )
+ }
+ await this.click()
+ await this.fill(text)
+ },
+})
+
+await page.getByRole('textbox').clickAndFill('Hello World')
+```
+
+Interaction methods don't compose into selectors. `page.getByRole('textbox').clickAndFill('Hello')` works because `getByRole` returns a locator; `page.clickAndFill('Hello')` would hit the guard. Reach for this form for action helpers, not for query helpers.
+
+## Augmenting locator types
+
+`locators.extend` is a runtime registration. TypeScript doesn't know about the new methods until you augment the [`LocatorSelectors`](/api/browser/locators) interface, usually in a shared `.d.ts` file:
+
+```ts
+import 'vitest/browser'
+
+declare module 'vitest/browser' {
+ interface LocatorSelectors {
+ getByCommentsCount: (count: number) => Locator
+ getRowWithAction: (action: string) => Locator
+ clickAndFill: (text: string) => Promise
+ }
+}
+```
+
+`LocatorSelectors` is the interface that both `Locator` and `BrowserPage` extend, so any method declared on it shows up on both. That matches what `locators.extend` does at runtime, and it's why interaction helpers like `clickAndFill` need the guard above: TypeScript will let `page.clickAndFill('x')` type-check, but the guard catches the misuse before it hits a missing method.
+
+## See also
+
+- [Custom Locators API](/api/browser/locators#custom-locators)
+- [Built-in Locators](/api/browser/locators)
+- [Playwright "other locators"](https://playwright.dev/docs/other-locators)
diff --git a/guide/recipes/cancellable.md b/guide/recipes/cancellable.md
new file mode 100644
index 00000000..677ad28d
--- /dev/null
+++ b/guide/recipes/cancellable.md
@@ -0,0 +1,56 @@
+---
+title: Cancellable Test Resources | Recipes
+---
+
+# Cancellable Test Resources
+
+A test can hold onto resources that don't stop when the test stops. A `fetch`, a child process, a file stream, a polling loop: none of those notice when Vitest has cancelled the test, and the worker has to sit there waiting for them to finish on their own. Vitest cancels a test when it exceeds its `timeout`, when another test fails under `--bail`, or when someone presses Ctrl+C in the terminal.
+
+The test context provides a [`signal`](/guide/test-context#signal) 3.2.0 that fires in all of those cases. Pass it to anything that accepts an `AbortSignal` and the resource is released when Vitest cancels.
+
+## Pattern
+
+```ts
+import { test } from 'vitest'
+
+test('stop request when test times out', async ({ signal }) => {
+ await fetch('/heavy-resource', { signal })
+}, 2000)
+```
+
+If the request hasn't completed within 2 seconds, `fetch` rejects with `AbortError` instead of the test hanging until the operation finishes.
+
+## Other Web APIs that accept an `AbortSignal`
+
+- [`fetch`](https://developer.mozilla.org/docs/Web/API/fetch)
+- [`addEventListener`](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener), where passing `{ signal }` removes the listener on abort
+- [`ReadableStream.pipeTo`](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeTo)
+- Node.js APIs like [`fs.readFile`](https://nodejs.org/api/fs.html#fspromisesreadfilepath-options), [`child_process.spawn`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options), and [`setTimeout` or `setInterval`](https://nodejs.org/api/timers.html), all of which accept `{ signal }`
+- Any custom code that calls `signal.throwIfAborted()` or listens for `'abort'`
+
+## Forwarding the signal
+
+Wire the test's signal into your own helpers so cancellation propagates all the way down:
+
+```ts
+async function pollUntilReady(url: string, signal: AbortSignal) {
+ while (!signal.aborted) {
+ const res = await fetch(url, { signal })
+ if (res.ok) {
+ return
+ }
+ await new Promise(r => setTimeout(r, 200))
+ }
+ signal.throwIfAborted()
+}
+
+test('worker becomes ready', async ({ signal }) => {
+ await pollUntilReady('http://localhost:4000/health', signal)
+}, 5000)
+```
+
+## See also
+
+- [`signal` in Test Context](/guide/test-context#signal)
+- [`bail`](/config/bail)
+- [`testTimeout`](/config/testtimeout)
diff --git a/guide/recipes/custom-assertions.md b/guide/recipes/custom-assertions.md
new file mode 100644
index 00000000..8c04aeba
--- /dev/null
+++ b/guide/recipes/custom-assertions.md
@@ -0,0 +1,55 @@
+---
+title: Custom Assertion Helpers | Recipes
+---
+
+# Custom Assertion Helpers
+
+Reusable assertion helpers make tests easier to read, at the cost of stack traces. When an assertion fails inside a helper, the trace points at the line inside the helper rather than the test that called it. With the same helper used across many tests, the stack trace alone doesn't identify which call site failed.
+
+[`vi.defineHelper`](/api/vi#vi-defineHelper) 4.1.0 wraps a function so Vitest strips its internals from the stack and points the error back at the call site instead.
+
+## Pattern
+
+```ts
+import { expect, test, vi } from 'vitest'
+
+const assertPair = vi.defineHelper((a: unknown, b: unknown) => {
+ expect(a).toEqual(b) // ❌ failure does NOT point here
+})
+
+test('example', () => {
+ assertPair('left', 'right') // ✅ failure points here
+})
+```
+
+When `assertPair` fails, the diff and stack frame surface the test line that called it. That's the same behaviour built-in matchers give you.
+
+## Composing multiple expectations
+
+The same wrapper works for helpers that bundle several assertions:
+
+```ts
+import { expect, test, vi } from 'vitest'
+
+const expectValidUser = vi.defineHelper((user: unknown) => {
+ expect(user).toHaveProperty('id')
+ expect(user).toHaveProperty('email')
+ expect(user.email).toMatch(/@/)
+})
+
+test('returns a valid user', async () => {
+ const user = await fetchUser('alice')
+ expectValidUser(user)
+})
+```
+
+A failure in any of the inner `expect` calls is reported against the `expectValidUser(user)` line in the test.
+
+Reach for `defineHelper` whenever a reusable check calls `expect` more than once, whether that's a domain-specific helper like `expectValidJWT` or any block of `expect` calls you'd otherwise inline into every test.
+
+For asymmetric matchers and custom matchers attached to `expect.extend`, see [Extending Matchers](/guide/extending-matchers).
+
+## See also
+
+- [`vi.defineHelper`](/api/vi#vi-defineHelper)
+- [Extending Matchers](/guide/extending-matchers)
diff --git a/guide/recipes/db-transaction.md b/guide/recipes/db-transaction.md
new file mode 100644
index 00000000..c0ae9f20
--- /dev/null
+++ b/guide/recipes/db-transaction.md
@@ -0,0 +1,80 @@
+---
+title: Database Transaction per Test | Recipes
+---
+
+# Database Transaction per Test
+
+Integration tests that touch a real database need to start from a clean state. Truncating tables between every test is slow, so the conventional workaround is to wrap each test in a transaction that's rolled back when it finishes. Nothing ever commits, and there's no per-test cleanup to write.
+
+Vitest exposes this through [`aroundEach`](/api/hooks#aroundeach) 4.1.0 and a [scoped fixture](/guide/test-context#fixture-scopes) 3.2.0.
+
+## Pattern
+
+```ts
+import { test as baseTest } from 'vitest'
+import { createTestDatabase } from './db.ts'
+
+export const test = baseTest
+ .extend('db', { scope: 'file' }, async ({}, { onCleanup }) => {
+ const db = await createTestDatabase()
+ onCleanup(() => db.close())
+ return db
+ })
+
+test.aroundEach(async (runTest, { db }) => {
+ await db.transaction(runTest)
+})
+
+test('insert user', async ({ db }) => {
+ await db.insert({ name: 'Alice' })
+ // rolled back automatically when the test ends
+})
+```
+
+## How it works
+
+The `db` fixture is created once per file via `scope: 'file'`, so connection setup happens once instead of on every test, and `onCleanup` closes the connection when the file is done. `aroundEach` wraps every test in `db.transaction(runTest)`, and anything the test writes gets rolled back when `runTest` resolves. The test receives the same `db` instance through its context, with no awareness that it's running inside a transaction.
+
+This works as long as your database driver supports nested transactions or savepoints, which covers most modern databases. The same `aroundEach` hook can also wrap an [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) context if you want to propagate things like tenant or trace IDs through the test alongside the transaction.
+
+## One connection per worker
+
+If the suite has many files, paying for a fresh database connection on every file adds up. Switching the fixture to `scope: 'worker'` and turning off isolation lets multiple files share a single connection per worker process:
+
+```ts [vitest.config.ts]
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ isolate: false,
+ },
+})
+```
+
+```ts
+import { test as baseTest } from 'vitest'
+import { createTestDatabase } from './db.ts'
+
+export const test = baseTest
+ .extend('db', { scope: 'worker' }, async ({}, { onCleanup }) => {
+ const db = await createTestDatabase()
+ onCleanup(() => db.close())
+ return db
+ })
+
+test.aroundEach(async (runTest, { db }) => {
+ await db.transaction(runTest)
+})
+```
+
+By default, every test file runs in its own worker, so `scope: 'file'` and `scope: 'worker'` behave identically. With `isolate: false`, Vitest reuses workers across files (capped by [`maxWorkers`](/config/maxworkers)), so a worker-scoped fixture is created once per worker instead of once per file. For a suite of 200 files running on 8 workers, that's 8 connections instead of 200.
+
+Reusing workers isn't a free optimization. With isolation off, files share module instances inside the worker, and tests that mutate top-level state (counters, caches, monkey-patched globals) can leak that state to whichever file runs next in the same worker. The per-test rollback handles data isolation in the database. It can't protect module state in the worker. Read the trade-offs in the [Per-File Isolation Settings](/guide/recipes/disable-isolation) recipe before turning isolation off suite-wide.
+
+[`vmThreads` and `vmForks`](/config/pool) always run isolated regardless of the `isolate` flag, so worker-scoped fixtures fall back to per-file behavior in those pools.
+
+## See also
+
+- [`aroundEach` and `aroundAll`](/api/hooks#aroundeach)
+- [Fixture scopes](/guide/test-context#fixture-scopes)
+- [Builder pattern](/guide/test-context#builder-pattern)
diff --git a/guide/recipes/disable-isolation.md b/guide/recipes/disable-isolation.md
new file mode 100644
index 00000000..03ad3997
--- /dev/null
+++ b/guide/recipes/disable-isolation.md
@@ -0,0 +1,71 @@
+---
+title: Per-File Isolation Settings | Recipes
+---
+
+# Per-File Isolation Settings
+
+By default, every test file runs in its own isolated module graph, which protects against one file leaking state into another. That isolation costs setup time on every file, which is fine for integration tests that genuinely need it and wasted on pure unit tests that don't share mutable state.
+
+Use [`projects`](/guide/projects) to apply [`isolate: false`](/config/isolate) to the unit suite while keeping the integration suite isolated.
+
+## Pattern
+
+```ts [vitest.config.ts]
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ projects: [
+ {
+ test: {
+ // Non-isolated unit tests
+ name: 'Unit tests',
+ isolate: false,
+ exclude: ['**.integration.test.ts'],
+ },
+ },
+ {
+ test: {
+ // Isolated integration tests
+ name: 'Integration tests',
+ include: ['**.integration.test.ts'],
+ },
+ },
+ ],
+ },
+})
+```
+
+## When isolation matters
+
+A test file is safe to deisolate when it does not:
+
+- mutate module-level state (counters, caches, top-level `let` bindings)
+- call [`vi.stubGlobal`](/api/vi#vi-stubglobal) or [`vi.stubEnv`](/api/vi#vi-stubenv)
+- monkey-patch prototypes (`Date.prototype`, `Array.prototype`, …)
+- register listeners on `process` or other long-lived emitters
+- depend on a fresh module instance for `vi.mock` factories
+
+If any of those apply, isolation is doing real work and should stay on.
+
+## Verifying it's safe
+
+Run the suite twice with shuffling to surface inter-file pollution:
+
+```sh
+vitest --shuffle --run --project='Unit tests'
+vitest --shuffle --run --project='Unit tests'
+```
+
+If the second run produces different results, you have order-dependent tests. Either fix the offender or leave isolation enabled for that file.
+
+## Per-pool isolation
+
+`isolate` only governs the [`threads`](/config/pool) and [`forks`](/config/pool) pools. The `vmThreads` and `vmForks` pools always run isolated regardless of the flag, since they trade startup cost for stronger guarantees.
+
+## See also
+
+- [`isolate`](/config/isolate)
+- [Test Projects](/guide/projects)
+- [Improving Performance](/guide/improving-performance)
+- [Parallel and Sequential Test Files](/guide/recipes/parallel-sequential)
diff --git a/guide/recipes/explicit-resources.md b/guide/recipes/explicit-resources.md
new file mode 100644
index 00000000..67dc35c1
--- /dev/null
+++ b/guide/recipes/explicit-resources.md
@@ -0,0 +1,105 @@
+---
+title: Auto-Cleanup with `using` | Recipes
+---
+
+# Auto-Cleanup with `using`
+
+Spies and mocks need to be restored after the test that installed them, otherwise state leaks between tests. The usual approaches are an `afterEach(() => vi.restoreAllMocks())` at the suite level or a per-test [`onTestFinished(() => spy.mockRestore())`](/api/hooks#ontestfinished) inline.
+
+If your runtime supports [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) (Node.js 24+, or via TypeScript 5.2+ in modern bundlers), there's a tighter option: declare the spy with `using` instead of `const`, and restoration happens automatically when the block exits.
+
+This works for [`vi.spyOn`](/api/vi#vi-spyon), [`vi.fn`](/api/vi#vi-fn), and [`vi.doMock`](/api/vi#vi-domock). 3.2.0
+
+## Pattern
+
+```ts
+import { expect, it, vi } from 'vitest'
+
+function debug(message: string) {
+ console.log(message)
+}
+
+it('calls console.log', () => {
+ using spy = vi.spyOn(console, 'log').mockImplementation(() => {})
+ debug('message')
+ expect(spy).toHaveBeenCalled()
+})
+
+// console.log is restored here without an afterEach
+```
+
+The same pattern works with `vi.doMock`, which returns a disposable that queues an unmock when the scope exits:
+
+```ts
+import { expect, it, vi } from 'vitest'
+
+it('uses the mocked module, then the real one', async () => {
+ {
+ using _mock = vi.doMock('./users', () => ({
+ loadUser: () => ({ id: '1', name: 'Alice' }),
+ }))
+ const { loadUser } = await import('./users')
+ expect(loadUser('alice').name).toBe('Alice')
+ }
+
+ // ./users is unmocked from here on
+})
+```
+
+## Scoped to any block
+
+`using` is block-scoped, so you can install a spy for just part of a test. This is the case neither `afterEach` nor `onTestFinished` covers, since both run after the test ends:
+
+```ts
+import { expect, it, vi } from 'vitest'
+
+it('only mocks fetch for the auth call', async () => {
+ // real fetch here
+ await preloadConfig()
+
+ {
+ using fetchSpy = vi.spyOn(globalThis, 'fetch')
+ .mockResolvedValue(new Response('{"ok":true}'))
+
+ await login('alice', 'secret')
+ expect(fetchSpy).toHaveBeenCalledOnce()
+ }
+
+ // real fetch is back
+ await reportSuccess()
+})
+```
+
+This is also a way to avoid turning on the global [`restoreMocks: true`](/config/restoremocks) config when only a handful of calls actually need restoration.
+
+## Compatibility
+
+`using` requires support for the TC39 Explicit Resource Management proposal:
+
+- TypeScript ≥ 5.2 (with `target: 'es2022'` or higher and the `disposable` lib included by default).
+- Node.js ≥ 24 (or Node 22+ with `--harmony`-style flags) for native runtime support.
+
+If your environment doesn't support it yet, the closest equivalent for whole-test cleanup is [`onTestFinished`](/api/hooks#ontestfinished), which registers the cleanup inline and runs after the test completes regardless of pass or failure:
+
+```ts
+import { expect, it, onTestFinished, vi } from 'vitest'
+
+it('calls console.log', () => {
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
+ onTestFinished(() => spy.mockRestore())
+
+ debug('message')
+ expect(spy).toHaveBeenCalled()
+})
+```
+
+`onTestFinished` can't tear down a spy mid-test the way `using` can, so the block-scoped pattern above remains specific to ERM.
+
+## See also
+
+- [`vi.spyOn`](/api/vi#vi-spyon)
+- [`vi.fn`](/api/vi#vi-fn)
+- [`vi.doMock`](/api/vi#vi-domock)
+- [`onTestFinished`](/api/hooks#ontestfinished)
+- [`restoreMocks`](/config/restoremocks)
+- [TC39 Explicit Resource Management proposal](https://github.com/tc39/proposal-explicit-resource-management)
diff --git a/guide/recipes/parallel-sequential.md b/guide/recipes/parallel-sequential.md
new file mode 100644
index 00000000..f8dd5a47
--- /dev/null
+++ b/guide/recipes/parallel-sequential.md
@@ -0,0 +1,89 @@
+---
+title: Parallel and Sequential Test Files | Recipes
+---
+
+# Parallel and Sequential Test Files
+
+Most test files are independent and run faster in parallel. The exception is the handful that share an exclusive resource, like a fixed port, a writable temp directory, or a database without per-test isolation. Those files flake when other tests run concurrently with them.
+
+Disabling parallelism globally would slow down every test in the suite. Splitting the suite into two [`projects`](/guide/projects), one parallel and one sequential, lets only the affected files pay the cost.
+
+## Pattern
+
+```ts [vitest.config.ts]
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ projects: [
+ {
+ test: {
+ name: 'Parallel',
+ exclude: ['**.sequential.test.ts'],
+ },
+ },
+ {
+ test: {
+ name: 'Sequential',
+ include: ['**.sequential.test.ts'],
+ fileParallelism: false,
+ },
+ },
+ ],
+ },
+})
+```
+
+[`fileParallelism: false`](/config/fileparallelism) at the project level keeps the rest of your suite running concurrently while the matched files run one at a time. It's a shorthand for [`maxWorkers: 1`](/config/maxworkers); the two settings are equivalent.
+
+## Run sequential after parallel
+
+By default, projects run in parallel with each other, so the sequential project's first file may overlap with parallel files that still hold the same resource. Use [`sequence.groupOrder`](/config/sequence#sequence-grouporder) 3.2.0 to force the parallel batch to finish first:
+
+```ts [vitest.config.ts]
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ projects: [
+ {
+ test: {
+ name: 'Parallel',
+ exclude: ['**.sequential.test.ts'],
+ sequence: { groupOrder: 0 },
+ },
+ },
+ {
+ test: {
+ name: 'Sequential',
+ include: ['**.sequential.test.ts'],
+ fileParallelism: false,
+ sequence: { groupOrder: 1 },
+ },
+ },
+ ],
+ },
+})
+```
+
+The parallel batch finishes, *then* the sequential batch starts. Total wall clock stays close to the parallel time plus sum of the sequential test run time.
+
+## File scope vs. test scope
+
+There are two different "parallel" knobs in Vitest. Don't confuse them:
+
+| Scope | Knob | Controls |
+| --- | --- | --- |
+| Across files | [`fileParallelism`](/config/fileparallelism) | Whether two test *files* run in parallel workers |
+| Within a file | `describe.concurrent` / `test.concurrent` | Whether tests *inside one file* run concurrently |
+
+`fileParallelism: false` doesn't make tests inside a file concurrent; tests inside a file are sequential by default. And `concurrent` on a `describe` or `test` doesn't affect how files are scheduled.
+
+## See also
+
+- [`fileParallelism`](/config/fileparallelism)
+- [`maxWorkers`](/config/maxworkers)
+- [`sequence.groupOrder`](/config/sequence#sequence-grouporder)
+- [Parallelism](/guide/parallelism)
+- [Test Projects](/guide/projects)
+- [Per-File Isolation Settings](/guide/recipes/disable-isolation)
diff --git a/guide/recipes/schema-matching.md b/guide/recipes/schema-matching.md
new file mode 100644
index 00000000..b22ace77
--- /dev/null
+++ b/guide/recipes/schema-matching.md
@@ -0,0 +1,90 @@
+---
+title: Schema-Driven Assertions | Recipes
+---
+
+# Schema-Driven Assertions
+
+If your project already validates data with [Zod](https://zod.dev), [Valibot](https://valibot.dev), or [ArkType](https://arktype.io), those schemas already describe what a valid value looks like. Reusing them in tests is more direct than duplicating shape checks across `toEqual` and `toMatchObject`.
+
+[`expect.schemaMatching`](/api/expect#expect-schemamatching) 4.0.0 is an asymmetric matcher that takes any [Standard Schema v1](https://standardschema.dev) object and passes if the value conforms to it.
+
+## Pattern
+
+```ts
+import { expect, test } from 'vitest'
+import { z } from 'zod'
+
+test('email validation', () => {
+ const user = { email: 'john@example.com' }
+
+ expect(user).toEqual({
+ email: expect.schemaMatching(z.string().email()),
+ })
+})
+```
+
+`expect.schemaMatching` is an asymmetric matcher, so it composes inside any equality check the same way `expect.any` or `expect.stringMatching` do:
+
+- `toEqual` / `toStrictEqual`
+- `toMatchObject`
+- `toContainEqual`
+- `toThrow`
+- `toHaveBeenCalledWith`
+- `toHaveReturnedWith`
+- `toHaveBeenResolvedWith`
+
+## Works with any Standard Schema library
+
+```ts
+import { expect, test } from 'vitest'
+import { z } from 'zod'
+import * as v from 'valibot'
+import { type } from 'arktype'
+
+const user = { email: 'john@example.com' }
+
+// Zod
+expect(user).toEqual({
+ email: expect.schemaMatching(z.string().email()),
+})
+
+// Valibot
+expect(user).toEqual({
+ email: expect.schemaMatching(v.pipe(v.string(), v.email())),
+})
+
+// ArkType
+expect(user).toEqual({
+ email: expect.schemaMatching(type('string.email')),
+})
+```
+
+## Verifying call arguments
+
+A common use is asserting that a mock was called with data that conforms to a schema, without spelling out every field:
+
+```ts
+import { expect, test, vi } from 'vitest'
+import { z } from 'zod'
+
+const UserSchema = z.object({
+ id: z.string().uuid(),
+ email: z.string().email(),
+ createdAt: z.date(),
+})
+
+test('persists a valid user', () => {
+ const repo = { save: vi.fn() }
+ registerUser(repo, { email: 'a@b.com' })
+
+ expect(repo.save).toHaveBeenCalledWith(expect.schemaMatching(UserSchema))
+})
+```
+
+Reach for `schemaMatching` when you already have a schema for the value and would otherwise spell out every property by hand. It's especially useful for assertions over generated fields like UUIDs or timestamps, where you can validate the format without predicting the exact value.
+
+## See also
+
+- [`expect.schemaMatching`](/api/expect#expect-schemamatching)
+- [Standard Schema](https://standardschema.dev)
+- [Asymmetric Matchers](/api/expect)
diff --git a/guide/recipes/type-narrowing.md b/guide/recipes/type-narrowing.md
new file mode 100644
index 00000000..9fea33ef
--- /dev/null
+++ b/guide/recipes/type-narrowing.md
@@ -0,0 +1,64 @@
+---
+title: Type Narrowing in Tests | Recipes
+---
+
+# Type Narrowing in Tests
+
+Tests deal with possibly-null values everywhere. `document.querySelector` returns `Element | null`, `Map.get(key)` returns `T | undefined`, and similar optional shapes show up throughout. The usual workarounds in test code are an unsafe cast with `as`, a non-null assertion with `!` on every access, or a runtime check like `expect(x).toBeTruthy()` that throws when the value is missing. All three add noise, and the runtime check is actively misleading because it doesn't narrow the type the way it looks like it should.
+
+[`expect.assert`](/api/expect#assert) 4.0.0 throws at runtime and narrows the TypeScript type. The same call replaces all three.
+
+## Pattern
+
+```ts
+import { expect, test } from 'vitest'
+
+test('reads stored user', () => {
+ const cache = new Map()
+ cache.set('alice', { id: '1', name: 'Alice' })
+
+ const user = cache.get('alice') // typed as `{ id, name } | undefined`
+ expect.assert(user) // throws if undefined, narrows below
+ expect(user.name).toBe('Alice') // no `!`, no `as`, type is `{ id, name }`
+})
+```
+
+The same shape collapses any "look up a value, check it exists, then use it" sequence:
+
+```ts
+const job = queue.find(j => j.id === 'build-42') // Job | undefined
+expect.assert(job)
+job.cancel() // narrowed to Job
+```
+
+## Why `toBeTruthy` doesn't narrow
+
+`expect(x).toBeTruthy()` and `expect(x).toBeDefined()` throw at runtime when the value is missing, so the test fails the way you want. They don't narrow the type, though, because their TypeScript signature returns `void` rather than the special `asserts` form.
+
+`expect.assert` is typed as an assertion function, so the same call serves both jobs.
+
+## Narrowing beyond null
+
+`expect.assert` accepts any boolean expression and applies the same narrowing TypeScript would do for an `if` branch. That covers `typeof` and `instanceof` checks:
+
+```ts
+expect.assert(typeof input === 'string')
+input.toUpperCase() // input is `string`
+
+expect.assert(error instanceof MyError)
+expect(error.code).toBe('E_FOO') // error is `MyError`
+```
+
+For common shapes there are pre-built helpers from chai's [`assert` API](/api/assert), reachable via the same `expect.assert` namespace:
+
+```ts
+expect.assert.isDefined(maybeUser) // narrows away `undefined`
+expect.assert.isString(input) // narrows to string
+expect.assert.instanceOf(error, MyError) // narrows to MyError
+```
+
+## See also
+
+- [`expect.assert`](/api/expect#assert)
+- [Chai `assert` API](/api/assert)
+- [Waiting for Async Conditions](/guide/recipes/wait-for)
diff --git a/guide/recipes/wait-for.md b/guide/recipes/wait-for.md
new file mode 100644
index 00000000..4c5e5291
--- /dev/null
+++ b/guide/recipes/wait-for.md
@@ -0,0 +1,99 @@
+---
+title: Waiting for Async Conditions | Recipes
+---
+
+# Waiting for Async Conditions
+
+Plenty of things in tests don't happen synchronously. A server takes a moment to boot, or a DOM element renders after a microtask. Waiting with `setTimeout` tends to land on either a flaky undershoot or a wasteful long sleep, and a manual polling loop is more code than you want to write per test.
+
+Vitest provides helpers that poll on your behalf, retrying on a fixed interval until the condition holds or a timeout elapses.
+
+## `expect.poll`: retry an assertion
+
+Use [`expect.poll`](/api/expect#poll) when the wait condition is an assertion. The callback returns the value to assert on, the matcher does the comparison, and Vitest retries the whole expression at each interval until the matcher passes.
+
+```ts
+import { expect, test } from 'vitest'
+import { createServer } from './server.ts'
+
+test('server starts', async () => {
+ const server = createServer()
+
+ await expect.poll(() => server.isReady, {
+ timeout: 500,
+ interval: 20
+ }).toBe(true)
+})
+```
+
+The failure message is the standard `expect` diff, with no manual `throw new Error('Server not started')` to maintain. This is the right tool for most "wait for X to become Y" cases.
+
+`expect.poll` makes every assertion asynchronous, so the call must be awaited. Some matchers don't pair with it: snapshot matchers (which would always succeed under polling), `.resolves` and `.rejects` (the condition is already awaited), and `toThrow` (the value is resolved before the matcher sees it). For any of those, reach for `vi.waitFor` instead.
+
+## `vi.waitFor`: wait and capture the value
+
+[`vi.waitFor`](/api/vi#vi-waitfor) is the right tool when the wait condition is the work itself succeeding rather than an assertion you write. It runs the callback at each interval; a thrown error queues another attempt, and the first call that doesn't throw resolves the wait with whatever the callback returned.
+
+```ts
+import { expect, test, vi } from 'vitest'
+import { connect, DB_URL } from './db.ts'
+
+test('database is reachable', async () => {
+ // `connect` throws ECONNREFUSED until the database accepts connections
+ const client = await vi.waitFor(() => connect(DB_URL), {
+ timeout: 5000,
+ interval: 100,
+ })
+
+ const rows = await client.query('SELECT 1 AS ok')
+ expect(rows[0].ok).toBe(1)
+})
+```
+
+The throw that drives the retry comes from `connect` itself, not from an `expect` you wrote inside the callback. `expect.poll` doesn't fit this shape because it's built around assertions, and "retry until this call stops throwing and hand me the result" isn't an assertion. Wrapping the call in a `try`/`catch` to fake one would either duplicate the work after the wait or require building the retry loop by hand.
+
+## `vi.waitUntil`: poll until truthy, fail fast on errors
+
+Use [`vi.waitUntil`](/api/vi#vi-waituntil) for a value lookup where any thrown error should fail the test on the spot rather than be retried away. Each interval calls the callback again. A truthy return resolves the wait; a falsy return waits for the next interval. A thrown error fails the test immediately.
+
+```ts
+import { expect, test, vi } from 'vitest'
+import { jobResults, startJob } from './worker.ts'
+
+test('worker completes the job', async () => {
+ startJob('build-42')
+
+ const result = await vi.waitUntil(
+ () => jobResults.get('build-42'),
+ { timeout: 5000, interval: 100 },
+ )
+
+ expect(result.status).toBe('ok')
+ expect(result.steps).toHaveLength(4)
+})
+```
+
+`jobResults.get('build-42')` returns `JobResult | undefined`. `waitUntil` polls until it returns a truthy value, narrows the resolved type to `JobResult`, and hands it back for further assertions. If the lookup itself throws because of a programming error like a typo in the import, `waitUntil` surfaces the error on the first attempt rather than retrying past it.
+
+In browser mode, prefer [`page.locator`](/api/browser/locators) and [`expect.element`](/api/browser/assertions) over `waitUntil` for DOM queries: locators retry on their own and produce richer failure messages.
+
+## Picking between them
+
+| | `expect.poll` | `vi.waitFor` | `vi.waitUntil` |
+| --- | --- | --- | --- |
+| Reach for it when | the wait is an assertion | the work might fail until it's ready | a lookup might be falsy and that's fine |
+| Retries on thrown error | yes | yes | no, fails fast |
+| Resolves with | the assertion | callback's return value | callback's return value |
+
+Each of these accepts `{ timeout, interval }` options, defaulting to a 1000 ms timeout and 50 ms intervals. `vi.waitFor` and `vi.waitUntil` also accept a number in place of the options object as shorthand for the timeout.
+
+## Fake timers
+
+If [`vi.useFakeTimers`](/api/vi#vi-usefaketimers) is active, `vi.waitFor` automatically calls `vi.advanceTimersByTime(interval)` between attempts. That keeps `setTimeout`-based code under test reachable without leaking real time into the test.
+
+## See also
+
+- [`expect.poll`](/api/expect#poll)
+- [`vi.waitFor`](/api/vi#vi-waitfor)
+- [`vi.waitUntil`](/api/vi#vi-waituntil)
+- [`vi.useFakeTimers`](/api/vi#vi-usefaketimers)
diff --git a/guide/recipes/watch-templates.md b/guide/recipes/watch-templates.md
new file mode 100644
index 00000000..49c1abd0
--- /dev/null
+++ b/guide/recipes/watch-templates.md
@@ -0,0 +1,64 @@
+---
+title: Watching Non-Imported Files | Recipes
+---
+
+# Watching Non-Imported Files
+
+In watch mode, Vitest tracks the import graph: when you change a file, every test whose imports reach that file reruns. That covers most cases. It misses tests that depend on files they don't `import`, like email templates loaded with `fs.readFile`, JSON fixtures parsed at runtime, HTML or CSS pulled in by a build step, or generated artifacts the tests assert against. Editing one of those files leaves the related tests stale, and the watch loop has no way to know.
+
+[`watchTriggerPatterns`](/config/watchtriggerpatterns) 3.2.0 makes these dependencies explicit. You declare a regex over file paths and a callback that returns which tests to rerun when a matching file changes.
+
+## Pattern
+
+```ts [vitest.config.ts]
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ watchTriggerPatterns: [
+ {
+ pattern: /src\/templates\/(.*)\.(ts|html|txt)$/,
+ testsToRun: (file, match) => {
+ // edit `src/templates/welcome.html` ⇒ rerun `api/tests/mailers/welcome.test.ts`
+ return `api/tests/mailers/${match[1]}.test.ts`
+ },
+ },
+ ],
+ },
+})
+```
+
+`testsToRun` returns one or more test file paths to rerun (as a string or string array), or `undefined` if no tests should rerun. Paths are resolved against the workspace root and are not interpreted as globs. `match` is the result of `RegExp.exec` against the changed file.
+
+## Variations
+
+Multiple patterns can coexist. The first below derives the test path from the directory of the changed file; the second maps a single shared fixture to a fixed list of test files:
+
+```ts [vitest.config.ts]
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ watchTriggerPatterns: [
+ {
+ pattern: /src\/(.*)\/schema\.json$/,
+ testsToRun: (_file, match) => `src/${match[1]}/__tests__/index.test.ts`,
+ },
+ {
+ pattern: /test\/shared-fixture\.json$/,
+ testsToRun: () => [
+ 'test/integration/users.test.ts',
+ 'test/integration/billing.test.ts',
+ ],
+ },
+ ],
+ },
+})
+```
+
+[`forceRerunTriggers`](/config/forcereruntriggers) covers the same general gap, except it reruns every test on every match. `watchTriggerPatterns` reruns only the tests you map for a given pattern, which keeps the watch loop fast.
+
+## See also
+
+- [`watchTriggerPatterns`](/config/watchtriggerpatterns)
+- [`forceRerunTriggers`](/config/forcereruntriggers)
diff --git a/guide/snapshot.md b/guide/snapshot.md
index 3a2652c4..8dfef293 100644
--- a/guide/snapshot.md
+++ b/guide/snapshot.md
@@ -124,7 +124,7 @@ test('button looks correct', async () => {
它会捕获屏幕截图并与参考图像进行比较,以检测意外的视觉变化。在 [视觉回归测试指南](/guide/browser/visual-regression-testing)中了解更多内容。
-## ARIA 快照 实验性 4.1.4 {#aria-snapshots}
+## ARIA 快照 4.1.4 {#aria-snapshots}
ARIA 快照会捕获 DOM 元素的无障碍访问树,并与存储的模板进行比对。基于 [Playwright 的 ARIA 快照](https://playwright.dev/docs/aria-snapshots) 实现,它提供了视觉回归测试之外的语义化替代方案,断言结构和含义而非像素。
@@ -230,7 +230,7 @@ Pretty foo: Object {
}
```
-## 自定义快照匹配器 实验性 4.1.3 {#custom-snapshot-matchers}
+## 自定义快照匹配器 4.1.3 {#custom-snapshot-matchers}
可通过 `vitest` 提供的 `Snapshots` 组合式函数构建自定义快照匹配器。这些函数允许你在生成快照前对值进行转换,同时完整保留快照生命周期支持(创建、更新、内联重写)。
@@ -329,7 +329,7 @@ declare module 'vitest' {
更多关于 `expect.extend` 和自定义匹配器约定的内容,请参阅 [扩展匹配器](/guide/extending-matchers)。
:::
-## 自定义领域快照 experimental 4.1.4 {#custom-snapshot-domain}
+## 自定义领域快照 4.1.4 {#custom-snapshot-domain}
自定义序列化器控制值如何被 **渲染** 成快照字符串,但比较过程仍然基于字符串相等。**领域快照适配器** 则更进一步:它拥有自定义匹配器的整个比较流水线,包括如何捕获值、渲染值、解析存储的快照,以及如何对它们进行语义匹配。
diff --git a/guide/test-tags.md b/guide/test-tags.md
index e2b8f87d..d5ed8351 100644
--- a/guide/test-tags.md
+++ b/guide/test-tags.md
@@ -7,9 +7,28 @@ outline: deep
允许你在测试用例上添加 [`标签`](/config/tags),在必要时可以使用标签进行过滤测试,或覆盖测试配置。
+## Why tags
+
+Tags become useful once a suite has groups of tests that share runner options, like a longer timeout for database queries or retries for integration tests on CI. Repeating those options on every relevant test by hand is brittle, and the categories often don't line up with file paths anyway, so splitting them out by file isn't an option. Flaky tests in particular tend to accumulate wherever the bugs landed, not in a `flaky/` folder.
+
+A tag captures that kind of category: the definition holds the shared options, and any test marked with the tag inherits them. Those tag names can also be combined into expressions: `--tags-filter='db && !flaky'` runs database tests that aren't marked flaky. [`TestRunner.matchesTags`](#checking-tags-filter-at-runtime) exposes the same expression at runtime, useful when `globalSetup` does expensive work that should be skipped if no tagged tests are scheduled.
+
+## When to reach for tags
+
+| If you want to… | Use |
+| --- | --- |
+| Apply timeout/retry to a *category* of tests | **Tags** |
+| Mark cross-cutting categories (`flaky`, `slow`, `frontend`) scattered across many files | **Tags** |
+| Conditionally run expensive setup based on what's filtered | **Tags** + [`matchesTags`](#checking-tags-filter-at-runtime) |
+| Run a subset by test name match | [`-t` / `testNamePattern`](/config/testnamepattern) |
+| Run a subset by file path | `--include` / `--exclude` |
+| Run different files with different *runner settings* (isolation, pool, environment) | [Test Projects](/guide/projects) |
+
+You can combine projects and tags. A test that sits in a `Sequential` project can also carry a `flaky` tag, and Vitest applies both.
+
## 定义标签 {#defining-tags}
-Vitest 并未提供任何的内置标签,标签必须在配置文件中提前进行定义。如果在测试中使用了未在配置文件中定义的标签,测试运行器将会抛出错误。这一行为可以防止因标签名称拼写错误而导致的意外行为。当然你可以修改 [`strictTags`](/config/stricttags) 选项进行禁用。
+Vitest 并未提供任何的内置标签,在默认情况下,标签必须在配置文件中提前进行定义。如果在测试中使用了未在配置文件中定义的标签,测试运行器将会抛出错误。这一行为可以防止因标签名称拼写错误而导致的意外行为。当然你可以修改 [`strictTags`](/config/stricttags) 选项进行禁用。
在标签定义时至少必须包含 `name` 参数,与此同时你还可以定义其他配置参数如 `timeout` 或 `retry`,这些配置参数将应用于使用该标签的所有测试。完整的可用配置参数,参见 [`tags`](/config/tags)。
@@ -44,25 +63,6 @@ export default defineConfig({
})
```
-::: warning
-如果多个标签具有相同配置项且应用于同一个测试时,将按从上至下的顺序解析,或按优先级排序解析(数值越低,优先级越高)。未定义优先级的标签会先合并,随后被优先级更高的标签覆盖。
-
-```ts
-test('flaky database test', { tags: ['flaky', 'db'] })
-// { timeout: 30_000, retry: 3 }
-```
-
-注意此时的 `timeout` 是 30 秒而不是 60 秒,因为 `flaky` 标签的优先级为 `1`,而定义了 60 秒超时的 `db` 标签未设置优先级。
-
-如果在当前测试上直接定义,则测试配置项优先级最高:
-
-```ts
-test('flaky database test', { tags: ['flaky', 'db'], timeout: 120_000 })
-// { timeout: 120_000, retry: 3 }
-```
-
-:::
-
如果你正在使用 TypeScript,可以扩展 `TestTags` 类型添加一个包含字符串的联合类型来限定的标签可用范围,请确保该文件被包含在 `tsconfig` 中:
```ts [vitest.shims.ts]
@@ -120,6 +120,24 @@ flaky: Flaky CI tests.
}
```
+### Resolving option conflicts
+
+如果多个标签具有相同配置项且应用于同一个测试时,将按从上至下的顺序解析,或按优先级排序解析(数值越低,优先级越高)。未定义优先级的标签会先合并,随后被优先级更高的标签覆盖。
+
+```ts
+test('flaky database test', { tags: ['flaky', 'db'] })
+// { timeout: 30_000, retry: 3 }
+```
+
+注意此时的 `timeout` 是 30 秒而不是 60 秒,因为 `flaky` 标签的优先级为 `1`,而定义了 60 秒超时的 `db` 标签未设置优先级。
+
+如果在当前测试上直接定义,则测试配置项优先级最高:
+
+```ts
+test('flaky database test', { tags: ['flaky', 'db'], timeout: 120_000 })
+// { timeout: 120_000, retry: 3 }
+```
+
## 在测试中使用标签 {#using-tags-in-tests}
可以通过 `tags` 参数 为单个测试用例或整个测试套件添加标签
@@ -305,7 +323,7 @@ vitest --tags-filter="unit || e2e" --tags-filter="!slow"
### 运行时检查标签过滤器 {#checking-tags-filter-at-runtime}
-自 Vitest 4.1.1 起,你可以使用 `TestRunner.matchesTags` 方法来检查当前标签过滤器是否匹配一组标签。该特性特别适用于按需执行高开销的初始化逻辑,当相关测试的标签被包含时才运行:
+你可以使用 `TestRunner.matchesTags` 方法来检查当前标签过滤器是否匹配一组标签。该特性特别适用于按需执行高开销的初始化逻辑,当相关测试的标签被包含时才运行:
```ts
import { beforeAll, TestRunner } from 'vitest'
@@ -319,3 +337,10 @@ beforeAll(async () => {
```
该方法接收一个标签数组作为参数,如果当前 `--tags-filter` 会包含带有这些标签的测试,则返回 `true`。如果未启用标签过滤器,则始终返回 `true`。
+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`.
+
+## See also
+
+- [Per-File Isolation Settings](/guide/recipes/disable-isolation) and [Parallel and Sequential Test Files](/guide/recipes/parallel-sequential) use projects to partition tests by file. Reach for projects when categories need different runner settings rather than different timeouts or retries.
+- [Test Filtering](/guide/filtering) covers `-t`, `--include`, and the rest of the CLI filters.
+- [`tags`](/config/tags) and [`strictTags`](/config/stricttags) configuration reference.