From 27d86b9c7a2680127c8dbccd2522e37cd9f28ad3 Mon Sep 17 00:00:00 2001 From: noise Date: Sat, 27 Jun 2026 17:30:41 +0800 Subject: [PATCH 1/8] docs(cn): update guide/recipes/db-transaction.md --- .vitepress/config.ts | 2 +- guide/recipes/db-transaction.md | 37 +++++++++++++++++---------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index c11a0ee0..fb5674f8 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -1020,7 +1020,7 @@ export default ({ mode }: { mode: string }) => { collapsed: false, items: [ { - text: 'Database Transaction per Test', + text: '每一个测试对应一个数据库事务', link: '/guide/recipes/db-transaction', }, { diff --git a/guide/recipes/db-transaction.md b/guide/recipes/db-transaction.md index c0ae9f20..749107ed 100644 --- a/guide/recipes/db-transaction.md +++ b/guide/recipes/db-transaction.md @@ -1,14 +1,14 @@ --- -title: Database Transaction per Test | Recipes +title: 每一个测试对应一个数据库事务 | 技巧 --- -# Database Transaction per Test +# 每一个测试对应一个数据库事务 {#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. +Vitest 通过 [`aroundEach`](/api/hooks#aroundeach) 4.1.0 和 [scoped fixture](/guide/test-context#fixture-scopes) 3.2.0 实现了这种形式。 -## Pattern +## 示例 {#pattern} ```ts import { test as baseTest } from 'vitest' @@ -27,19 +27,20 @@ test.aroundEach(async (runTest, { db }) => { test('insert user', async ({ db }) => { await db.insert({ name: 'Alice' }) - // rolled back automatically when the test ends + // 在测试结束时自动回滚 }) ``` -## How it works +## 工作原理 {#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. +`db` fixture 通过 `scope: 'file'` 在每个文件级别创建一次,因此连接建立只发生一次,而不是在每个测试中重复进行,并且 `onCleanup` 会在文件测试执行完成时关闭连接。`aroundEach` 将每个测试包装在 `db.transaction(runTest)` 中,测试写入的所有内容在 `runTest` +解析时都会回滚。测试通过上下文使用相同的 `db` 实例,无需感知它正运行在事务中。 -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. +只要你的数据库驱动支持嵌套事务或保存点,这种形式就能正常工作,大多数现代数据库都满足这一条件。如果你想要在测试中传播租户或追踪 ID 等内容,也可以使用同一个 `aroundEach` 钩子来包装 [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) 上下文,与事务一起使用。 -## One connection per worker +## 一个工作进程一个连接 {#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: +如果测试套件有很多文件,在每个文件上建立新的数据库连接仍会增加开销。将 fixture 切换到 `scope: 'worker'` 并关闭隔离可以让多个文件在每个工作进程内共享一个连接: ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -67,14 +68,14 @@ test.aroundEach(async (runTest, { db }) => { }) ``` -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. +默认情况下,每个测试文件在自己的工作进程中运行,因此 `scope: 'file'` 和 `scope: 'worker'` 行为相同。使用 `isolate: false` 时,Vitest 会在文件之间复用工作进程(数量上限由 [`maxWorkers`](/config/maxworkers) 限制),因此工作进程范围的 fixture 在每个工作进程中只创建一次,而不是在每个文件中都创建一次。对于 200 个文件、8 个工作进程的测试套件,只需 8 个连接,而非 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. +复用工作进程并不是零成本的优化。在禁用隔离后,文件在工作进程内共享模块实例,在修改模块顶层变量(计数器、缓存、猴子补丁(monkey-patched)的全局变量)的测试,可能会将状态泄漏到同一工作进程内后续运行的文件中。每次测试的回滚负责处理数据库中的数据隔离,但无法保护工作进程中的模块状态。在全局范围内关闭隔离之前,请参阅 [每个文件的隔离设置](/guide/recipes/disable-isolation) 技巧中的权衡。 -[`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. +[`vmThreads` 和 `vmForks`](/config/pool) 始终以隔离模式运行,无论 `isolate` 参数如何设置,在这些工作进程池中,工作进程范围的 fixture 会退化为每个文件创建一次的行为。 -## See also +## 相关链接 {#see-also} -- [`aroundEach` and `aroundAll`](/api/hooks#aroundeach) -- [Fixture scopes](/guide/test-context#fixture-scopes) -- [Builder pattern](/guide/test-context#builder-pattern) +- [`aroundEach` 和 `aroundAll`](/api/hooks#aroundeach) +- [Fixture 作用域](/guide/test-context#fixture-scopes) +- [构建模式](/guide/test-context#builder-pattern) From f29f5005e131c006cda44353f446ff7e18047b20 Mon Sep 17 00:00:00 2001 From: noise Date: Sat, 27 Jun 2026 18:04:38 +0800 Subject: [PATCH 2/8] docs(cn): update guide/recipes/cancellable.md --- .vitepress/config.ts | 2 +- guide/recipes/cancellable.md | 27 ++++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index fb5674f8..50727ecf 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -1024,7 +1024,7 @@ export default ({ mode }: { mode: string }) => { link: '/guide/recipes/db-transaction', }, { - text: 'Cancelling Long-Running Operations Gracefully', + text: '优雅地取消长时间运行的操作', link: '/guide/recipes/cancellable', }, { diff --git a/guide/recipes/cancellable.md b/guide/recipes/cancellable.md index 677ad28d..a46c73d8 100644 --- a/guide/recipes/cancellable.md +++ b/guide/recipes/cancellable.md @@ -1,14 +1,14 @@ --- -title: Cancellable Test Resources | Recipes +title: 可取消的测试资源 | 技巧 --- -# Cancellable Test Resources +# 可取消的测试资源 {#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. +测试运行过程中可能占用一些资源,测试停止后这些资源不会随之释放。无论是 `fetch`、子进程、文件流还是轮询,它们都无法感知 Vitest 已取消测试,导致工作进程只能被动等待它们自行结束 Vitest 会在测试超时超过 `timeout` 限制、在 `--bail` 模式下另一个测试失败,或有人在终端按下 Ctrl+C 时取消测试。 -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. +测试上下文提供了 [`signal`](/guide/test-context#signal) 3.2.0,它会在上述所有情况下触发。将 signal 传递给任何接受 `AbortSignal` 的对象,当 Vitest 取消测试时对应的资源就会被释放。 -## Pattern +## 示例 {#pattern} ```ts import { test } from 'vitest' @@ -19,18 +19,19 @@ test('stop request when test times out', async ({ signal }) => { ``` If the request hasn't completed within 2 seconds, `fetch` rejects with `AbortError` instead of the test hanging until the operation finishes. +如果请求未在 2 秒内完成,`fetch` 会抛出 `AbortError`,而不会让测试挂起直到操作结束。 -## Other Web APIs that accept an `AbortSignal` +## 其他接受 `AbortSignal` 的 Web API {#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 +- [`addEventListener`](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener),传递 `{ signal }` 会在中止时移除监听器 - [`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'` +- 像 Node.js API [`fs.readFile`](https://nodejs.org/api/fs.html#fspromisesreadfilepath-options), [`child_process.spawn`](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options),和 [`setTimeout` 或 `setInterval`](https://nodejs.org/api/timers.html),它们都接受 `{ signal }` +- 任何调用 `signal.throwIfAborted()` 或监听 `'abort'` 的代码 -## Forwarding the signal +## 传递信号 {#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) { @@ -49,8 +50,8 @@ test('worker becomes ready', async ({ signal }) => { }, 5000) ``` -## See also +## 相关链接 {#see-also} -- [`signal` in Test Context](/guide/test-context#signal) +- [测试上下文中的 `signal`](/guide/test-context#signal) - [`bail`](/config/bail) - [`testTimeout`](/config/testtimeout) From c2f7a5b3400355838dbff06a1bc9f2152685557b Mon Sep 17 00:00:00 2001 From: noise Date: Sat, 27 Jun 2026 19:04:34 +0800 Subject: [PATCH 3/8] docs(cn): guide/recipes/cancellable.md --- .vitepress/config.ts | 2 +- guide/recipes/cancellable.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 50727ecf..4764710c 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -1028,7 +1028,7 @@ export default ({ mode }: { mode: string }) => { link: '/guide/recipes/cancellable', }, { - text: 'Waiting for Async Conditions', + text: '等待异步条件', link: '/guide/recipes/wait-for', }, { diff --git a/guide/recipes/cancellable.md b/guide/recipes/cancellable.md index a46c73d8..4540763a 100644 --- a/guide/recipes/cancellable.md +++ b/guide/recipes/cancellable.md @@ -18,7 +18,6 @@ test('stop request when test times out', async ({ signal }) => { }, 2000) ``` -If the request hasn't completed within 2 seconds, `fetch` rejects with `AbortError` instead of the test hanging until the operation finishes. 如果请求未在 2 秒内完成,`fetch` 会抛出 `AbortError`,而不会让测试挂起直到操作结束。 ## 其他接受 `AbortSignal` 的 Web API {#other-web-apis-that-accept-an-abortsignal} From 01b53033c15ed9919bf655dbf7c661e67a2e6b0f Mon Sep 17 00:00:00 2001 From: noise Date: Sat, 27 Jun 2026 19:05:03 +0800 Subject: [PATCH 4/8] docs(cn): update guide/recipes/wait-for.md --- guide/recipes/wait-for.md | 54 ++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/guide/recipes/wait-for.md b/guide/recipes/wait-for.md index 4c5e5291..5cba3e02 100644 --- a/guide/recipes/wait-for.md +++ b/guide/recipes/wait-for.md @@ -1,16 +1,16 @@ --- -title: Waiting for Async Conditions | Recipes +title: 等待异步条件 | 技巧 --- -# Waiting for Async Conditions +# 等待异步条件 {#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. +测试中许多操作不会同步完成。服务器启动需要时间,DOM 元素在微任务结束后才渲染。使用 `setTimeout` 等待容易出现等待时间不稳定地偏短 ,或者设置过长的等待时间造成浪费,而手动轮询循环需要为每个测试编写大量代码。 -Vitest provides helpers that poll on your behalf, retrying on a fixed interval until the condition holds or a timeout elapses. +Vitest 提供了工具函数代你进行轮询,按固定间隔重试,直到条件成立或超时。 -## `expect.poll`: retry an assertion +## `expect.poll`:重试断言 {#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. +当等待条件是一个断言时,使用 [`expect.poll`](/api/expect#poll)。Vitest 会按间隔重试整个表达式,直到匹配器通过。回调函数返回待断言的值,匹配器负责比较。 ```ts import { expect, test } from 'vitest' @@ -26,20 +26,20 @@ test('server starts', async () => { }) ``` -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` 差异,无需手动维护 `throw new Error('Server not started')`。适用于大多数 “等待 X 变为 Y” 的场景。 -`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. +`expect.poll` 会让每个断言变为异步,因此调用必须使用 `await`。但某些匹配器与其不兼容:快照匹配器(轮询下总是成功)、`.resolves` 和 `.rejects`(条件已在等待中解析)以及 `toThrow`(匹配器看到值之前已被解析)。对于这些情况,改用 `vi.waitFor`。 -## `vi.waitFor`: wait and capture the value +## `vi.waitFor`:等待并捕获返回值 {#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. +[`vi.waitFor`](/api/vi#vi-waitfor) 适用于等待某个操作成功完成的场景。它在每个间隔运行回调函数;抛出错误会触发下一次尝试,首次未抛错的调用会完成等待,返回回调函数的结果。 ```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 + // `connect` 在数据库接受连接前会抛出 ECONNREFUSED const client = await vi.waitFor(() => connect(DB_URL), { timeout: 5000, interval: 100, @@ -50,11 +50,11 @@ test('database is reachable', async () => { }) ``` -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. +驱动重试的是 `connect` 本身抛出的错误,而不是你在回调函数中编写的 `expect`。`expect.poll` 并不适合这种场景,因为它是为断言设计的,而 “重试直到此调用停止抛出错误并返回结果” 不是断言逻辑。 -## `vi.waitUntil`: poll until truthy, fail fast on errors +## `vi.waitUntil`:轮询到为真值,错误时快速失败 {#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. +[`vi.waitUntil`](/api/vi#vi-waituntil) 用于值查找的场景,其中任何抛出的错误应立即导致测试失败,而不是被重试跳过。每次间隔都会重新调用回调函数。返回真值时完成等待;返回假值则等待下一个间隔。抛出错误则立即使测试失败。 ```ts import { expect, test, vi } from 'vitest' @@ -73,25 +73,27 @@ test('worker completes the job', async () => { }) ``` -`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. +`jobResults.get('build-42')` 返回 `JobResult | undefined`。`waitUntil` 轮询直到返回 true,将解析类型收窄为 `JobResult`,然后返回用于后续断言。如果取值操作本身因编写错误(如 import 中的拼写错误)抛出错误,`waitUntil` 会在首次尝试时抛出该错误,而不是跳过重试。 -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. +在浏览器模式下,DOM 查询优先使用 [`page.locator`](/api/browser/locators) 和 [`expect.element`](/api/browser/assertions) 而不是 `waitUntil`:定位器会自动重试并产生更丰富的失败信息。 -## Picking between them +## 选择合适的方法 {#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 | +| | `expect.poll` | `vi.waitFor` | `vi.waitUntil` | +| -------------- | -------------- | -------------------- | -------------------------- | +| 适用于 | 等待条件是断言 | 操作在就绪前可能失败 | 取值可能为假值,这是正常的 | +| 遇到错误时重试 | 是 | 是 | 否,立即失败 | +| 返回结果 | 断言结果 | 回调函数的返回值 | 回调函数的返回值 | -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. +这些方法都接受 `{ timeout, interval }` 选项,默认超时时间为 1000 毫秒,间隔为 50 毫秒。`vi.waitFor` 和 `vi.waitUntil` 还可以直接传入数字方式的简写,直接表示超时时间。 -## Fake timers +## 模拟定时器 {#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. +如果 [`vi.useFakeTimers`](/api/vi#vi-usefaketimers) 处于激活状态,`vi.waitFor` 会在重试之间自动调用 `vi.advanceTimersByTime(interval)`。这样可以在不引入真实时间的条件下下,使基于 `setTimeout` 的被测代码能够正常执行。 -## See also +这可以使基于 `setTimeout` 的被测代码在不引入真实时间的情况下保持可达性。 + +## 相关链接 {#see-also} - [`expect.poll`](/api/expect#poll) - [`vi.waitFor`](/api/vi#vi-waitfor) From 90a781c2ddfc3b43428fa96c3fbf5b0606ac1749 Mon Sep 17 00:00:00 2001 From: noise Date: Sat, 27 Jun 2026 19:43:33 +0800 Subject: [PATCH 5/8] docs(cn): update guide/recipes/type-narrowing.md --- .vitepress/config.ts | 2 +- guide/recipes/type-narrowing.md | 44 ++++++++++++++++----------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 4764710c..e387ba2d 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -1032,7 +1032,7 @@ export default ({ mode }: { mode: string }) => { link: '/guide/recipes/wait-for', }, { - text: 'Type Narrowing in Tests', + text: '在测试中收窄类型', link: '/guide/recipes/type-narrowing', }, { diff --git a/guide/recipes/type-narrowing.md b/guide/recipes/type-narrowing.md index 9fea33ef..66c1d567 100644 --- a/guide/recipes/type-narrowing.md +++ b/guide/recipes/type-narrowing.md @@ -1,12 +1,12 @@ --- -title: Type Narrowing in Tests | Recipes +title: 在测试中收窄类型 | 技巧 --- -# Type Narrowing in Tests +# 在测试中收窄类型 {#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. +测试中会频繁遇到可能为 null 的值。`document.querySelector` 返回 `Element | null`,`Map.get(key)` 返回 `T | undefined`,类似的可选接口规范随处可见。测试代码中常见的变通方式包括:用 `as` 进行不安全的类型转换、每次访问时使用 `!` 进行非空断言,或使用在值缺失时抛出异常的运行时检查,如 `expect(x).toBeTruthy()` ()`。这三种方式都会引入冗余代码,而运行时检查更会误导读者——因为它并未像看起来那样收窄类型。 -[`expect.assert`](/api/expect#assert) 4.0.0 throws at runtime and narrows the TypeScript type. The same call replaces all three. +[`expect.assert`](/api/expect#assert) 4.0.0 会在运行时收窄 TypeScript 类型并抛出异常。同一个调用可以替代上述三种方式。 ## Pattern @@ -17,48 +17,48 @@ 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 }` + const user = cache.get('alice') // 类型为 `{ id, name } | undefined` + expect.assert(user) // 若为 undefined 则抛出异常,并在下方收窄类型 + expect(user.name).toBe('Alice') // 无需 `!` 或 `as`,类型为 `{ 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 +job.cancel() // 已收窄为 Job ``` -## Why `toBeTruthy` doesn't narrow +## 为什么 toBeTruthy 无法收窄类型 {#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(x).toBeTruthy()` 和 `expect(x).toBeDefined()` 值缺失时会在运行时抛出异常,因此测试会按预期失败。但它们不会收窄类型,因为 TypeScript 签名返回 void,而不是特殊的断言函数形式。 -`expect.assert` is typed as an assertion function, so the same call serves both jobs. +`expect.assert` 拥有断言函数的类型,因此同一个调用能同时达到抛出异常和收窄类型的目的。 -## Narrowing beyond null +## 收窄非 null 类型 {#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: +`expect.assert` 接受任意布尔表达式,并应用 TypeScript 在 `if` 分支中会使用的相同类型收窄逻辑。这包括 `typeof` 和 `instanceof` 检查: ```ts expect.assert(typeof input === 'string') -input.toUpperCase() // input is `string` +input.toUpperCase() // input 是 `string` 类型 expect.assert(error instanceof MyError) -expect(error.code).toBe('E_FOO') // error is `MyError` +expect(error.code).toBe('E_FOO') // error 是 `MyError` 类型 ``` -For common shapes there are pre-built helpers from chai's [`assert` API](/api/assert), reachable via the same `expect.assert` namespace: +对于常见的接口规范,有 chai 的 [`assert` API](/api/assert),提供的预制工具函数,可通过相同的 `expect.assert` 命名空间访问: ```ts -expect.assert.isDefined(maybeUser) // narrows away `undefined` -expect.assert.isString(input) // narrows to string -expect.assert.instanceOf(error, MyError) // narrows to MyError +expect.assert.isDefined(maybeUser) // 收窄掉 `undefined` +expect.assert.isString(input) // 收窄为 string +expect.assert.instanceOf(error, MyError) // 收窄为 MyError ``` -## See also +## 相关链接 {#see-also} - [`expect.assert`](/api/expect#assert) - [Chai `assert` API](/api/assert) -- [Waiting for Async Conditions](/guide/recipes/wait-for) +- [等待异步条件](/guide/recipes/wait-for) From 12bc874017cb8222389ffe006488d3661d612733 Mon Sep 17 00:00:00 2001 From: noise Date: Sat, 27 Jun 2026 20:26:20 +0800 Subject: [PATCH 6/8] docs(cn): update guide/recipes/watch-templates.md --- .vitepress/config.ts | 2 +- guide/recipes/type-narrowing.md | 10 +++++----- guide/recipes/watch-templates.md | 23 ++++++++++++----------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index e387ba2d..7303690e 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -1040,7 +1040,7 @@ export default ({ mode }: { mode: string }) => { link: '/guide/recipes/custom-assertions', }, { - text: 'Watching Non-Imported Files', + text: '监视非直接导入的文件', link: '/guide/recipes/watch-templates', }, { diff --git a/guide/recipes/type-narrowing.md b/guide/recipes/type-narrowing.md index 66c1d567..6b5abc62 100644 --- a/guide/recipes/type-narrowing.md +++ b/guide/recipes/type-narrowing.md @@ -4,11 +4,11 @@ title: 在测试中收窄类型 | 技巧 # 在测试中收窄类型 {#type-narrowing-in-tests} -测试中会频繁遇到可能为 null 的值。`document.querySelector` 返回 `Element | null`,`Map.get(key)` 返回 `T | undefined`,类似的可选接口规范随处可见。测试代码中常见的变通方式包括:用 `as` 进行不安全的类型转换、每次访问时使用 `!` 进行非空断言,或使用在值缺失时抛出异常的运行时检查,如 `expect(x).toBeTruthy()` ()`。这三种方式都会引入冗余代码,而运行时检查更会误导读者——因为它并未像看起来那样收窄类型。 +测试中会频繁遇到可能为 null 的值。`document.querySelector` 返回 `Element | null`,`Map.get(key)` 返回 `T | undefined`,类似为空的联合类型随处可见。测试代码中常见的变通方式包括:用 `as` 进行不安全的类型转换、每次访问时使用 `!` 进行非空断言,或使用在值缺失时抛出异常的运行时检查,如 `expect(x).toBeTruthy()`。这三种方式都会引入冗余代码,而运行时检查更会误导读者,因为它并未像看起来那样收窄类型。 [`expect.assert`](/api/expect#assert) 4.0.0 会在运行时收窄 TypeScript 类型并抛出异常。同一个调用可以替代上述三种方式。 -## Pattern +## 示例 {#pattern} ```ts import { expect, test } from 'vitest' @@ -23,7 +23,7 @@ test('reads stored user', () => { }) ``` -同样的结构可以简化任何 “查找值、检查存在、然后使用” 的流程: +同样的结构可以简化任何 “查找一个值,检查它是否存在,然后使用它” 的流程: ```ts const job = queue.find(j => j.id === 'build-42') // Job | undefined @@ -33,7 +33,7 @@ job.cancel() // 已收窄为 Job ## 为什么 toBeTruthy 无法收窄类型 {#why-tobetruthy-doesn-t-narrow} -`expect(x).toBeTruthy()` 和 `expect(x).toBeDefined()` 值缺失时会在运行时抛出异常,因此测试会按预期失败。但它们不会收窄类型,因为 TypeScript 签名返回 void,而不是特殊的断言函数形式。 +`expect(x).toBeTruthy()` 和 `expect(x).toBeDefined()` 值缺失时会在运行时抛出异常,因此测试会按预期失败。但它们不会收窄类型,因为 TypeScript 签名返回 `void`,而不是特殊的 `asserts` 形式。 `expect.assert` 拥有断言函数的类型,因此同一个调用能同时达到抛出异常和收窄类型的目的。 @@ -49,7 +49,7 @@ expect.assert(error instanceof MyError) expect(error.code).toBe('E_FOO') // error 是 `MyError` 类型 ``` -对于常见的接口规范,有 chai 的 [`assert` API](/api/assert),提供的预制工具函数,可通过相同的 `expect.assert` 命名空间访问: +对于常见的接口规范,chai 提供了 [`assert` API](/api/assert) 预置工具函数,可通过相同的 `expect.assert` 命名空间访问: ```ts expect.assert.isDefined(maybeUser) // 收窄掉 `undefined` diff --git a/guide/recipes/watch-templates.md b/guide/recipes/watch-templates.md index 49c1abd0..eb330c91 100644 --- a/guide/recipes/watch-templates.md +++ b/guide/recipes/watch-templates.md @@ -1,14 +1,15 @@ --- -title: Watching Non-Imported Files | Recipes +title: 监视非直接导入的文件 | 技巧 --- -# Watching Non-Imported Files +# 监视非直接导入的文件 {#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. +在 Watch 模式下,Vitest 会跟踪导入依赖树:当你修改文件时,所有导入该文件的测试都会重新运行。这确实覆盖了大部分使用场景。但有些测试依赖的文件并未被直接 `import`,例如通过 `fs.readFile` 加载的邮件模板、运行时解析的 JSON fixture、通过构建步骤引入的 HTML 或 CSS,或测试会断言的生成产物。修改这些文件中的任何一个都会导致相关测试失败,而监听循环却无法感知。 -[`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. +[`watchTriggerPatterns`](/config/watchtriggerpatterns) 3.2.0 可以让这些依赖关系显式化。你需要声明一个匹配文件路径的正则表达式,以及一 +个回调函数,用于在匹配的文件变更时返回需要重新运行的测试。 -## Pattern +## 示例 {#pattern} ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -19,7 +20,7 @@ export default defineConfig({ { pattern: /src\/templates\/(.*)\.(ts|html|txt)$/, testsToRun: (file, match) => { - // edit `src/templates/welcome.html` ⇒ rerun `api/tests/mailers/welcome.test.ts` + // 编辑 `src/templates/welcome.html` ⇒ 返回 `api/tests/mailers/welcome.test.ts` return `api/tests/mailers/${match[1]}.test.ts` }, }, @@ -28,11 +29,11 @@ export default defineConfig({ }) ``` -`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. +`testsToRun` 返回一个或多个需要重新运行的测试文件路径(字符串或字符串数组),如果无需重新运行任何测试则返回 `undefined`。路径从工作区根目录解析,且不会被解析为 glob 模式。`match` 是 `RegExp.exec` 对变更文件执行的结果。 -## Variations +## 变体 {#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: +多个形式可以共存。下面的第一种形式从变更文件的目录派生测试路径;第二种形式将单个共享 fixture 映射到固定的测试文件列表: ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -56,9 +57,9 @@ export default defineConfig({ }) ``` -[`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. +[`forceRerunTriggers`](/config/forcereruntriggers) 也能解决同样的问题,但每次匹配时重新运行所有测试。相比之下,`watchTriggerPatterns` 只重新运行你为特定形式指定的测试,保持监听循环高效。 -## See also +## 相关链接 {#see-also} - [`watchTriggerPatterns`](/config/watchtriggerpatterns) - [`forceRerunTriggers`](/config/forcereruntriggers) From 86008b0d4e7eebe51f54c36807888550735b7865 Mon Sep 17 00:00:00 2001 From: noise Date: Sat, 27 Jun 2026 20:48:10 +0800 Subject: [PATCH 7/8] docs(cn): update guide/recipes/custom-assertions.md --- .vitepress/config.ts | 6 +++--- api/expect.md | 2 +- guide/extending-matchers.md | 6 +++--- guide/recipes/custom-assertions.md | 30 +++++++++++++++--------------- guide/recipes/db-transaction.md | 4 ++-- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 7303690e..b903be4c 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -906,7 +906,7 @@ export default ({ mode }: { mode: string }) => { link: '/guide/test-annotations', }, { - text: '扩展断言', + text: '扩展匹配器', link: '/guide/extending-matchers', }, { @@ -1020,7 +1020,7 @@ export default ({ mode }: { mode: string }) => { collapsed: false, items: [ { - text: '每一个测试对应一个数据库事务', + text: '一个测试对应一个数据库事务', link: '/guide/recipes/db-transaction', }, { @@ -1036,7 +1036,7 @@ export default ({ mode }: { mode: string }) => { link: '/guide/recipes/type-narrowing', }, { - text: 'Custom Assertion Helpers', + text: '自定义断言工具函数', link: '/guide/recipes/custom-assertions', }, { diff --git a/api/expect.md b/api/expect.md index 90871988..f5ce20a0 100644 --- a/api/expect.md +++ b/api/expect.md @@ -2277,7 +2277,7 @@ declare module 'vitest' { ::: :::tip -如果想了解更多信息,请查看 [扩展断言](/guide/extending-matchers)。 +如果想了解更多信息,请查看 [扩展匹配器](/guide/extending-matchers)。 ::: ## expect.addEqualityTesters {#expect-addequalitytesters} diff --git a/guide/extending-matchers.md b/guide/extending-matchers.md index bfcc4645..a897d1d2 100644 --- a/guide/extending-matchers.md +++ b/guide/extending-matchers.md @@ -1,12 +1,12 @@ --- -title: 扩展断言 | 指南 +title: 扩展匹配器 | 指南 --- -# 扩展断言 {#extending-matchers} +# 扩展匹配器 {#extending-matchers} 由于 Vitest 兼容 Chai 和 Jest,所以可以根据个人喜好使用 [`chai.use`](https://www.chaijs.com/guide/plugins/) API 或者 `expect.extend`。 -本文将以 `expect.extend` 为例探讨扩展断言。如果你对 Chai 的 API 更感兴趣,可以查看 [他们的指南](https://www.chaijs.com/guide/plugins/)。 +本文将以 `expect.extend` 为例探讨扩展匹配器。如果你对 Chai 的 API 更感兴趣,可以查看 [他们的指南](https://www.chaijs.com/guide/plugins/)。 为了扩展默认的断言,可以使用对象包裹断言的形式调用 `expect.extend` 方法。 diff --git a/guide/recipes/custom-assertions.md b/guide/recipes/custom-assertions.md index 8c04aeba..0754ebdf 100644 --- a/guide/recipes/custom-assertions.md +++ b/guide/recipes/custom-assertions.md @@ -1,32 +1,32 @@ --- -title: Custom Assertion Helpers | Recipes +title: 自定义断言工具函数 | 技巧 --- -# Custom Assertion Helpers +# 自定义断言工具函数 {#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. +[`vi.defineHelper`](/api/vi#vi-defineHelper) 4.1.0 会包装一个函数,让 Vitest 在堆栈中移除工具函数的内部实现,并将错误指向调用点。 -## Pattern +## 示例 {#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 + expect(a).toEqual(b) // ❌ 失败不指向这里 }) test('example', () => { - assertPair('left', 'right') // ✅ failure points here + assertPair('left', 'right') // ✅ 失败指向这里 }) ``` -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. +当 `assertPair` 失败时,差异对比和堆栈帧会显示调用它的测试行。和内置匹配器提供的行为一致。 -## Composing multiple expectations +## 组合多个期望 {#composing-multiple-expectations} -The same wrapper works for helpers that bundle several assertions: +同一个包装器也适用于将多个断言打包的工具函数: ```ts import { expect, test, vi } from 'vitest' @@ -43,13 +43,13 @@ test('returns a valid user', async () => { }) ``` -A failure in any of the inner `expect` calls is reported against the `expectValidUser(user)` line in the test. +内部任意 `expect` 调用失败时,都会在测试中的 `expectValidUser(user)` 这一行上报。 -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. +只要可复用的检查包含多次 `expect` 调用,都可以使用 `defineHelper`,无论是像 `expectValidJWT` 这样的领域特定工具函数,还是任何原本要内联到每个测试中的 `expect` 调用块。 -For asymmetric matchers and custom matchers attached to `expect.extend`, see [Extending Matchers](/guide/extending-matchers). +关于附加到 `expect.extend` 的不对称匹配器和自定义匹配器,请参阅 [扩展匹配器](/guide/extending-matchers)。 -## See also +## 相关链接 {#see-also} - [`vi.defineHelper`](/api/vi#vi-defineHelper) -- [Extending Matchers](/guide/extending-matchers) +- [扩展匹配器](/guide/extending-matchers) diff --git a/guide/recipes/db-transaction.md b/guide/recipes/db-transaction.md index 749107ed..6503cca2 100644 --- a/guide/recipes/db-transaction.md +++ b/guide/recipes/db-transaction.md @@ -1,8 +1,8 @@ --- -title: 每一个测试对应一个数据库事务 | 技巧 +title: 一个测试对应一个数据库事务 | 技巧 --- -# 每一个测试对应一个数据库事务 {#database-transaction-per-test} +# 一个测试对应一个数据库事务 {#database-transaction-per-test} 涉及到真实数据库的集成测试需要从一个干净的状态开始。在每个测试前清空表数据很慢,因此常用的做法是将每个测试包装在一个事务中,并在测试完成时进行回滚。这样永远不会提交事务、也不需要每次都编写清理代码。 From 80f4bd09c2e6b562a95c0cc06685a33a4dad57e6 Mon Sep 17 00:00:00 2001 From: noise Date: Sun, 28 Jun 2026 23:17:16 +0800 Subject: [PATCH 8/8] typo --- guide/recipes/cancellable.md | 2 +- guide/recipes/db-transaction.md | 2 +- guide/recipes/wait-for.md | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/guide/recipes/cancellable.md b/guide/recipes/cancellable.md index 4540763a..0c9192d1 100644 --- a/guide/recipes/cancellable.md +++ b/guide/recipes/cancellable.md @@ -6,7 +6,7 @@ title: 可取消的测试资源 | 技巧 测试运行过程中可能占用一些资源,测试停止后这些资源不会随之释放。无论是 `fetch`、子进程、文件流还是轮询,它们都无法感知 Vitest 已取消测试,导致工作进程只能被动等待它们自行结束 Vitest 会在测试超时超过 `timeout` 限制、在 `--bail` 模式下另一个测试失败,或有人在终端按下 Ctrl+C 时取消测试。 -测试上下文提供了 [`signal`](/guide/test-context#signal) 3.2.0,它会在上述所有情况下触发。将 signal 传递给任何接受 `AbortSignal` 的对象,当 Vitest 取消测试时对应的资源就会被释放。 +测试上下文提供了 [`signal`](/guide/test-context#signal) 3.2.0,它会在上述所有情况下触发。将它传递给任何接受 `AbortSignal` 的对象,当 Vitest 取消测试时对应的资源就会被释放。 ## 示例 {#pattern} diff --git a/guide/recipes/db-transaction.md b/guide/recipes/db-transaction.md index 6503cca2..5b29f4a0 100644 --- a/guide/recipes/db-transaction.md +++ b/guide/recipes/db-transaction.md @@ -6,7 +6,7 @@ title: 一个测试对应一个数据库事务 | 技巧 涉及到真实数据库的集成测试需要从一个干净的状态开始。在每个测试前清空表数据很慢,因此常用的做法是将每个测试包装在一个事务中,并在测试完成时进行回滚。这样永远不会提交事务、也不需要每次都编写清理代码。 -Vitest 通过 [`aroundEach`](/api/hooks#aroundeach) 4.1.0 和 [scoped fixture](/guide/test-context#fixture-scopes) 3.2.0 实现了这种形式。 +Vitest 通过 [`aroundEach`](/api/hooks#aroundeach) 4.1.0 和 [scoped fixture](/guide/test-context#fixture-scopes) 3.2.0 提供了这一能力。 ## 示例 {#pattern} diff --git a/guide/recipes/wait-for.md b/guide/recipes/wait-for.md index 5cba3e02..0287dffb 100644 --- a/guide/recipes/wait-for.md +++ b/guide/recipes/wait-for.md @@ -28,7 +28,7 @@ test('server starts', async () => { 失败信息是标准的 `expect` 差异,无需手动维护 `throw new Error('Server not started')`。适用于大多数 “等待 X 变为 Y” 的场景。 -`expect.poll` 会让每个断言变为异步,因此调用必须使用 `await`。但某些匹配器与其不兼容:快照匹配器(轮询下总是成功)、`.resolves` 和 `.rejects`(条件已在等待中解析)以及 `toThrow`(匹配器看到值之前已被解析)。对于这些情况,改用 `vi.waitFor`。 +`expect.poll` 会让每个断言变为异步的,因此必须使用 `await` 调用。但某些匹配器与其不兼容:快照匹配器(轮询下总是成功)、`.resolves` 和 `.rejects`(条件已在等待中解析)以及 `toThrow`(匹配器看到值之前已被解析)。对于这些情况,改用 `vi.waitFor`。 ## `vi.waitFor`:等待并捕获返回值 {#vi-waitfor-wait-and-capture-the-value} @@ -79,11 +79,11 @@ test('worker completes the job', async () => { ## 选择合适的方法 {#picking-between-them} -| | `expect.poll` | `vi.waitFor` | `vi.waitUntil` | -| -------------- | -------------- | -------------------- | -------------------------- | -| 适用于 | 等待条件是断言 | 操作在就绪前可能失败 | 取值可能为假值,这是正常的 | -| 遇到错误时重试 | 是 | 是 | 否,立即失败 | -| 返回结果 | 断言结果 | 回调函数的返回值 | 回调函数的返回值 | +| | `expect.poll` | `vi.waitFor` | `vi.waitUntil` | +| -------------- | ------------------ | -------------------- | ------------------------------ | +| 适用于 | 等待条件一个是断言 | 操作在就绪前可能失败 | 返回值可能为 false,这是正常的 | +| 遇到错误时重试 | 是 | 是 | 否,立即失败 | +| 返回结果 | 断言结果 | 回调函数的返回值 | 回调函数的返回值 | 这些方法都接受 `{ timeout, interval }` 选项,默认超时时间为 1000 毫秒,间隔为 50 毫秒。`vi.waitFor` 和 `vi.waitUntil` 还可以直接传入数字方式的简写,直接表示超时时间。