diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 7326b51c..2baea493 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -1047,27 +1047,27 @@ export default ({ mode }: { mode: string }) => { link: '/guide/recipes/watch-templates', }, { - text: 'Extending Browser Locators', + text: '扩展浏览器定位器', link: '/guide/recipes/browser-locators', }, { - text: 'Schema-Driven Assertions', + text: 'Schema 驱动断言', link: '/guide/recipes/schema-matching', }, { - text: 'Auto-Cleanup with `using`', + text: '使用 `using` 自动清理', link: '/guide/recipes/explicit-resources', }, { - text: 'Conditional Mocking with `vi.when`', + text: '使用 `vi.when` 进行条件模拟', link: '/guide/recipes/conditional-mocking', }, { - text: 'Per-File Isolation Settings', + text: '按文件配置隔离', link: '/guide/recipes/disable-isolation', }, { - text: 'Parallel and Sequential Test Files', + text: '并行和串行测试文件', link: '/guide/recipes/parallel-sequential', }, ], diff --git a/guide/recipes/browser-locators.md b/guide/recipes/browser-locators.md index 91e8445c..71ba93b0 100644 --- a/guide/recipes/browser-locators.md +++ b/guide/recipes/browser-locators.md @@ -1,20 +1,20 @@ --- -title: Extending Browser Locators | Recipes +title: 扩展浏览器定位器 | 技巧 --- -# Domain Locators +# 领域定位器 {#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. +内置的 [定位器](/api/browser/locators) 如 `getByRole` 和 `getByText` 覆盖了映射到无障碍属性的查询。当应用具有不符合 ARIA 的结构时,例如 “带有 N 条回复的评论” 或自定义表格组件中的行,它们就无能为力了。 -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. +备选方案是使用 `querySelector`。结果是普通查询而非定位器,虽然可以工作,因此你会失去自动重试和严格模式保护。 -[`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. +[`locators.extend`](/api/browser/locators#custom-locators) 3.2.0 可以在继承定位器 API 特性的前提下,添加一个领域特定的定位器。该方法返回的值仍然是定位器,通过这样方式自定义方法继承了定位器的自动重试、严格模式保护和链式调用功能。你为这些方法命名的名称会成为团队测试词汇的一部分:`page.getByCard({ id: 'product-1' })` 读起来是领域语言而非 DOM,并且同一名称会在整个测试套件中保持一致。 -## Returning a Playwright string +## 返回 Playwright 字符串 {#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. +最简单的形式返回一个 [Playwright 定位器字符串](https://playwright.dev/docs/other-locators)。Vitest 将返回的字符串视为调用该定位器方法的子查询:当在 `page` 上调用时,将会在整个页面中查找;当在父定位器上调用时,查找范围限定在该父定位器的子树内。 -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. +当新的查询无法使用内置定位器恰当表达时,可以使用这种形式,例如对于没有对应内置角色的组件使用 CSS-with-text 选择器,或者对于你无法控制的遗留组件使用 XPath。 ```ts import { locators } from 'vitest/browser' @@ -39,11 +39,11 @@ test('article shows comment count', async () => { }) ``` -## Composing existing locators +## 组合现有定位器 {#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. +当你返回的是定位器而不是字符串时,Vitest 会直接使用该定位器。在扩展方法内部,`this` 会绑定到调用该方法的定位器上(顶层调用时则绑定到 `page`),因此你可以链式组合现有定位器,或通过 `filter` 表达那些单个内置方法无法描述的元素关系。 -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: +下面示例使用 `filter({ has })` 将行定位器收窄到包含指定 name 按钮的那些行,把常见的匹配 “每行操作按钮” 封装成一个具名查询: ```ts import { locators } from 'vitest/browser' @@ -62,13 +62,13 @@ locators.extend({ 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 +## 自定义交互操作 {#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. +执行交互操作且不返回定位器的方法同样可行。这与构建用户操作 DSL 使用相同机制,与查询方法一起定义,使测试词汇保持一致。 -`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: +`locators.extend` 将 `this` 的类型标注为 `BrowserPage | Locator`,因为自定义方法可能调用任意一个。这对于查询工具函数来说没问题,因为 `getByRole` 和其他查询方法在两者上都存在。对于交互工具函数则不然:`page` 没有 `click` 或 `fill` 方法,因此调用 `page.clickAndFill('x')` 会在运行时报错。可以将 `this` 与 `page` 单例进行比较来防范这种情况,这样 TypeScript 能在抛出错误后将 `this` 类型收窄为 `Locator`: ```ts import { locators, page } from 'vitest/browser' @@ -89,11 +89,11 @@ locators.extend({ 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. +这种交互方法不会组合成定位器。`page.getByRole('textbox').clickAndFill('Hello')` 可以正常工作的原因是 `getByRole` 返回的是定位器;`page.clickAndFill('Hello')` 则会触发类型保护。上述追溯形式仅适用于扩展操作工具函数,并不适用扩展查询工具函数。 -## Augmenting locator types +## 扩展定位器类型 {#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: +`locators.extend` 属于运行时注册。TypeScript 在类型层面并不知道这些新增方法,除非你在一个共享的 `.d.ts` 声明文件显式扩展 [`LocatorSelectors`](/api/browser/locators) 接口。 ```ts import 'vitest/browser' @@ -107,10 +107,10 @@ declare module 'vitest/browser' { } ``` -`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. +`LocatorSelectors` 是 `Locator` 和 `BrowserPage` 联合接口,所以你在这里声明的方法会同时出现在两者上。这也和 `locators.extend` 的运行时行为一致。正因如此,`clickAndFill` 这类交互工具方法才需要上面的保护:TypeScript 允许 `page.clickAndFill('x')` 通过类型检查,但运行时保护会在调用不存在的方法前先报错。 -## See also +## 相关链接 {#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) +- [自定义定位器 API](/api/browser/locators#custom-locators) +- [内置定位器](/api/browser/locators) +- [Playwright “其他定位器”](https://playwright.dev/docs/other-locators) diff --git a/guide/recipes/cancellable.md b/guide/recipes/cancellable.md index 0c9192d1..4a28b952 100644 --- a/guide/recipes/cancellable.md +++ b/guide/recipes/cancellable.md @@ -8,7 +8,7 @@ title: 可取消的测试资源 | 技巧 测试上下文提供了 [`signal`](/guide/test-context#signal) 3.2.0,它会在上述所有情况下触发。将它传递给任何接受 `AbortSignal` 的对象,当 Vitest 取消测试时对应的资源就会被释放。 -## 示例 {#pattern} +## 用法 {#pattern} ```ts import { test } from 'vitest' diff --git a/guide/recipes/conditional-mocking.md b/guide/recipes/conditional-mocking.md index 7d8c68dd..335cf251 100644 --- a/guide/recipes/conditional-mocking.md +++ b/guide/recipes/conditional-mocking.md @@ -1,15 +1,14 @@ --- -title: Conditional Mocking with vi.when | Recipes +title: 使用 vi.when 进行条件模拟 | 技巧 --- - -# Conditional Mocking with `vi.when` +# 使用 vi.when 进行条件模拟 {#conditional-mocking-with-vi-when} -::: tip Prerequisites -This recipe assumes you already have some familiarity with [mocking](/guide/mocking) in Vitest. +::: tip 前置要求 +本技巧假定你已经熟悉 Vitest 中的 [模拟](/guide/mocking)。 ::: -When a mock needs to return different values depending on the arguments it receives, [`mockReturnValue`](/api/mock#mockreturnvalue) doesn't help because it always returns the same value. The standard approach would be to use [`mockImplementation`](/api/mock#mockimplementation) with a `switch` or a series of `if/else` statements: +当模拟函数需要根据接收到的参数返回不同的值时,[`mockReturnValue`](/api/mock#mockreturnvalue) 无法满足需求,因为它始终返回同一个值。标准的做法是结合 `switch` 或一系列 `if/else` 语句使用 [`mockImplementation`](/api/mock#mockimplementation): ```ts db.findById.mockImplementation((id) => { @@ -25,17 +24,17 @@ db.findById.mockImplementation((id) => { }) ``` -This works, but it becomes tedious because you have to write the argument-matching logic yourself. This is something that Vitest can handle for you when using the [`vi.when`](/api/vi#vi-when) 5.0.0 API. +这种方法虽然可行,但需要自行编写参数匹配逻辑,过程比较繁琐。使用 [`vi.when`](/api/vi#vi-when) 5.0.0 API 后,Vitest 可以替你处理这部分逻辑。 -## Pattern +## 用法 {#pattern} -`vi.when` takes a spy and lets you define argument-specific behaviors. +`vi.when` 接收一个 spy,让你可以为不同参数定义不同的行为。 -Call `.calledWith(...args)` to declare which arguments to match. This creates a _behavior_. +调用 `.calledWith(...args)` 指定要匹配的参数,这会创建一个 _行为_。 -Then attach an _action_ by calling a `then*` method. The action determines what happens when the behavior matches. +然后调用 `then*` 方法附加一个 _动作_,用于决定匹配该行为后要执行的操作。 -Multiple behaviors can be chained on the same spy: +同一个 spy 可以串联多个行为: ```ts import { test, vi } from 'vitest' @@ -55,20 +54,20 @@ test('returns user data', async () => { }) ``` -The same approach works across all mock outcome types. Here is the full set of actions and their equivalents: +所有类型的模拟结果都可以使用同样的方式处理。下面列出了完整的动作及其对应写法: -| Action | Equivalent to | Equivalent code | -|---|---|---| -| `thenReturn(value)` | `mockReturnValue(value)` | `return value` | -| `thenThrow(error)` | `mockThrow(error)` | `throw error` | +| 动作 | 等价于 | 等效代码 | +| -------------------- | -------------------------- | ------------------------------- | +| `thenReturn(value)` | `mockReturnValue(value)` | `return value` | +| `thenThrow(error)` | `mockThrow(error)` | `throw error` | | `thenResolve(value)` | `mockResolvedValue(value)` | `return Promise.resolve(value)` | -| `thenReject(error)` | `mockRejectedValue(error)` | `return Promise.reject(error)` | +| `thenReject(error)` | `mockRejectedValue(error)` | `return Promise.reject(error)` | -## Stacking actions +## 动作堆叠 {#stacking-actions} -A single behavior can have multiple actions attached to it. When the behavior matches, actions are _consumed_ in **last-in-first-out** order: the most recently registered action runs first. Once that action has been consumed, Vitest falls back to the previous one. Use the `times` option to limit how many calls an action handles before falling through to the next action. An action with no `times` limit runs indefinitely. +单个行为可以附加多个动作。当行为匹配时,动作会按照 **后进先出** 的顺序被 _消耗_:最近注册的动作最先执行。该动作消耗完后,Vitest 会回退到前一个动作。你可以使用 `times` 选项限制某个动作处理调用的次数,达到次数后再回退到下一个动作。不设置 `times` 限制的动作会一直生效。 -Because actions are evaluated in reverse registration order, indefinite actions should be registered first so that later finite actions can temporarily override them. +由于动作会按注册顺序逆序处理,因此应先注册无限期动作,再注册有次数限制的动作,以便后者可以暂时覆盖前者。 ```ts import { test, vi } from 'vitest' @@ -80,9 +79,9 @@ test('retries after an initial failure', async () => { vi.when(fetchInstance) .calledWith('/data/config.json') .thenResolve(new Response('{ debug: true }')) - // ↳ indefinite fallback + // ↳ 无限回退 .thenReject(new Error('network error'), { times: 1 }) - // ↳ applied first and consumed after one call + // ↳ 优先调用,仅生效一次 await expect(readConfig(fetchInstance)).resolves.toEqual({ debug: true }) @@ -90,11 +89,11 @@ test('retries after an initial failure', async () => { }) ``` -For convenience, `then*Once` shorthands are available and equivalent to `{ times: 1 }`: `thenReturnOnce`, `thenResolveOnce`, `thenThrowOnce`, `thenRejectOnce`. +为了简化调用,Vitest 提供了与 `{ times: 1 }` 等价的 `then*Once` 简写形式:`thenReturnOnce`、`thenResolveOnce`、`thenThrowOnce` 和 `thenRejectOnce`。 -## Asymmetric matchers +## 非对称匹配器 {#asymmetric-matchers} -`calledWith` supports [asymmetric matchers](/guide/learn/matchers#asymmetric-matchers). This is useful when you care about the shape or type of an argument rather than its exact value: +`calledWith` 支持 [非对称匹配器](/guide/learn/matchers#asymmetric-matchers)。适用于仅关心参数的结构或类型,而不要求精确的值时: ```ts test('sends email to each recipient', () => { @@ -104,7 +103,7 @@ test('sends email to each recipient', () => { }) ``` -Behaviors, unlike actions, are matched in **first-in-first-out** order. The first behavior whose arguments match the call wins, just like a chain of `if/else` statements. Specific matchers must therefore be registered before broad ones. +与动作不同,行为会按照 **先进先出** 的顺序进行匹配。规则类似于一组 `if/else` 语句,Vitest 会采用第一个参数与实际调用相匹配的行为。因此,应先注册匹配范围较小的行为,再注册匹配范围更大的行为。 ```ts test('sends email to each recipient', () => { @@ -116,10 +115,10 @@ test('sends email to each recipient', () => { }) ``` -::: warning Behavior Merging -When registering a new behavior, Vitest checks existing behaviors in registration order. If the new arguments already match an existing behavior, the new action is merged into that behavior instead of creating a new one. +::: warning 行为合并 +注册新行为时,Vitest 会按注册顺序检查已有行为。如果新参数已经匹配某个已有行为,新的动作会合并到该行为中,而不会创建新行为。 -This is especially important with broad asymmetric matchers: +使用范围较宽的非对称匹配器时尤其需要注意: ```ts vi.when(getRole) @@ -129,7 +128,7 @@ vi.when(getRole) .thenReturnOnce('admin') ``` -Because the second registration is merged into the existing behavior, the `'admin'` action is not scoped to `'admin@example.com'`. Instead, it becomes the next action for the entire `expect.any(String)` behavior. The resulting behavior acts as if it had been written like this: +由于第二次注册会合并到已有行为中,`'admin'` 动作并不会只适用于 `'admin@example.com'`,而会成为整个 `expect.any(String)` 行为的下一个动作。最终效果等同于以下写法: ```ts vi.when(getRole) @@ -138,27 +137,28 @@ vi.when(getRole) .thenReturnOnce('admin') ``` -As a result, the first call with any string returns `'admin'`, while later calls return `'user'`: +因此,第一次传入任意字符串时都会返回 `'admin'`,后续调用则返回 `'user'`: ```ts expect(getRole('user@example.com')).toBe('admin') expect(getRole('user@example.com')).toBe('user') ``` + ::: -## Handling unmatched calls +## 处理未匹配的调用 {#handling-unmatched-calls} -By default, when the spy is called with arguments that match no registered behavior, it falls back to the spy's original implementation. If the spy has no original implementation, it returns `undefined`. +默认情况下,如果 spy 接收到的参数没有匹配任何已注册行为,就会回退到 spy 的原始实现。如果 spy 没有原始实现,则返回 `undefined`。 -There are three ways to handle this differently: +你可以通过以下三种方式改变这种行为: -1. [throwing an error](#onunmatched-throw); -1. [running a custom function](#onunmatched-fn); -1. [using asymmetric matchers as catch-all behaviors](#asymmetric-matcher-as-catch-all). +1. [抛出一个错误](#onunmatched-throw); +2. [运行一个自定义函数](#onunmatched-fn); +3. [使用非对称匹配器作为兜底行为](#asymmetric-matcher-as-catch-all). ### `onUnmatched: 'throw'` -Pass `{ onUnmatched: 'throw' }` to throw whenever the spy is called with unregistered arguments: +传入 `{ onUnmatched: 'throw' }`,即可在 spy 接收到未注册参数时抛出错误: ```ts vi.when(db.findById, { onUnmatched: 'throw' }) @@ -171,11 +171,11 @@ await expect(db.findById(3)).rejects.toThrow( ) ``` -The error message includes the unmatched arguments. The error type and message are fixed and cannot be customized. +错误消息中会包含未匹配的参数。错误类型和消息均为固定值,无法自定义。 ### `onUnmatched: fn` -Pass a function to handle unmatched calls with custom logic, for example when a shared mock needs a different fallback per test. +传入一个函数即可使用自定义逻辑处理未匹配的调用,例如为共享 mock 在不同测试中提供不同的回退行为。 ```ts const db = { findById: vi.fn() } @@ -193,11 +193,11 @@ test('returns a placeholder for unknown ids', async () => { }) ``` -The function is called with the same arguments as the spy and its return value is used directly as the spy's result. If it throws or returns a rejected promise, that error propagates to the caller just as it would from any action. +该函数接收与 spy 相同的参数,其返回值会直接作为 spy 的结果。如果函数抛出错误或返回被 reject 的 Promise,错误会像其他动作产生的错误一样传递给调用方。 -### Asymmetric matcher as catch-all +### 将非对称匹配器用作兜底 {#asymmetric-matcher-as-catch-all} -Registering a broad `calledWith` last acts as a fallback for calls that do not match any earlier, more specific behavior. The fallback behavior can return a specific value, resolve or reject a promise, or throw a typed error. +最后注册一个范围较宽的 `calledWith`,可以为不匹配前面具体行为的调用提供兜底。兜底行为可以返回指定值、resolve 或 reject Promise,也可以抛出带类型的错误。 ```ts vi.when(db.findById) @@ -209,9 +209,9 @@ vi.when(db.findById) .thenReject(new Error('user not found')) ``` -## Asserting that all behaviors were called +## 断言所有行为均已调用 {#asserting-that-all-behaviors-were-called} -To check that all registered behaviors were actually matched and their actions consumed, the object returned by `vi.when` supports the [`toHaveBeenExhausted`](/api/expect#tohavebeenexhausted) assertion: +要检查所有已注册行为是否都已匹配且其动作均已消耗,可以对 `vi.when` 返回的对象使用 [`toHaveBeenExhausted`](/api/expect#tohavebeenexhausted) 断言: ```ts test('loads both users', async () => { @@ -229,7 +229,7 @@ test('loads both users', async () => { }) ``` -In this example, if `loadDashboard` only calls `findById(1)`, the test fails with a message listing the behaviors that were never matched: +在这个示例中,如果 `loadDashboard` 只调用了 `findById(1)`,测试就会失败,并列出从未匹配的行为: ``` AssertionError: expected all behaviors to have been exhausted, but some remain: @@ -238,17 +238,17 @@ AssertionError: expected all behaviors to have been exhausted, but some remain: ✗ thenReturn({ id: 2, name: 'Gracie' }) never called ``` -::: warning Caveat -A `vi.when` chain with no behaviors is never considered exhausted. The same applies to a bare `.calledWith()` with no `then*` action attached. Both will always cause `toHaveBeenExhausted` to fail. +::: warning 注意事项 +不包含任何行为的 `vi.when` 链永远不会被视为已耗尽。同样,单独使用且未附加 `then*` 动作的 `.calledWith()` 也不会被视为已耗尽。这两种情况都会导致 `toHaveBeenExhausted` 断言失败。 -Indefinite actions (no `times` limit) satisfy exhaustion checks after being used at least once. The actions keep responding after that, but the assertion is satisfied. +无限期动作(未设置 `times` 限制)至少使用一次后即可通过耗尽检查。此后动作仍会继续响应调用,但断言会视为通过。 ::: -## Automatic cleanup with `using` +## 使用 `using` 自动清理 {#automatic-cleanup-with-using} -`vi.when` supports the [Explicit Resource Management](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management) protocol. +`vi.when` 支持 [显式资源管理](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management) 协议。 -Declare the chain with `using` to scope behaviors to the current block and restore the spy automatically when execution leaves it. +使用 `using` 声明行为链,可以将行为限制在当前代码块内,并在执行离开代码块时自动恢复 spy。 ```ts const spy = vi.fn(() => 'original') @@ -256,16 +256,16 @@ const spy = vi.fn(() => 'original') test('with mocked behavior', () => { using w = vi.when(spy).calledWith('hello').thenReturn('mocked') expect(spy('hello')).toBe('mocked') -}) // ← restored here +}) // ← 在此处恢复 test('without mocked behavior', () => { expect(spy('hello')).toBe('original') }) ``` -## See also +## 相关链接 {#see-also} - [`vi.when`](/api/vi#vi-when) - [`toHaveBeenExhausted`](/api/expect#tohavebeenexhausted) - [`vi.isWhenChain`](/api/vi#vi-iswhenchain) -- [Auto-Cleanup with `using`](/guide/recipes/explicit-resources) +- [使用 `using` 自动清理](/guide/recipes/explicit-resources) diff --git a/guide/recipes/custom-assertions.md b/guide/recipes/custom-assertions.md index 0754ebdf..68600eb1 100644 --- a/guide/recipes/custom-assertions.md +++ b/guide/recipes/custom-assertions.md @@ -8,7 +8,7 @@ title: 自定义断言工具函数 | 技巧 [`vi.defineHelper`](/api/vi#vi-defineHelper) 4.1.0 会包装一个函数,让 Vitest 在堆栈中移除工具函数的内部实现,并将错误指向调用点。 -## 示例 {#pattern} +## 用法 {#pattern} ```ts import { expect, test, vi } from 'vitest' diff --git a/guide/recipes/db-transaction.md b/guide/recipes/db-transaction.md index 5b29f4a0..5b20f7ba 100644 --- a/guide/recipes/db-transaction.md +++ b/guide/recipes/db-transaction.md @@ -8,7 +8,7 @@ title: 一个测试对应一个数据库事务 | 技巧 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' diff --git a/guide/recipes/disable-isolation.md b/guide/recipes/disable-isolation.md index 03ad3997..c5f8316e 100644 --- a/guide/recipes/disable-isolation.md +++ b/guide/recipes/disable-isolation.md @@ -1,14 +1,14 @@ --- -title: Per-File Isolation Settings | Recipes +title: 按文件配置隔离 | 技巧 --- -# Per-File Isolation Settings +# 按文件配置隔离 {#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. +可以使用 [`projects`](/guide/projects) 为单元测试套件设置 [`isolate: false`](/config/isolate),同时保持集成测试套件的隔离状态。 -## Pattern +## 用法 {#pattern} ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -18,7 +18,7 @@ export default defineConfig({ projects: [ { test: { - // Non-isolated unit tests + // 不隔离的单元测试 name: 'Unit tests', isolate: false, exclude: ['**.integration.test.ts'], @@ -26,7 +26,7 @@ export default defineConfig({ }, { test: { - // Isolated integration tests + // 隔离的集成测试 name: 'Integration tests', include: ['**.integration.test.ts'], }, @@ -36,36 +36,36 @@ export default defineConfig({ }) ``` -## When isolation matters +## 需要隔离的情况 {#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 +- 修改模块级状态(计数器、缓存、顶层 `let` 绑定等) +- 调用 [`vi.stubGlobal`](/api/vi#vi-stubglobal) 或 [`vi.stubEnv`](/api/vi#vi-stubenv) +- 修改原型(`Date.prototype`、`Array.prototype` 等) +- 在 `process` 或其他长期存在的事件发送器上注册监听器 +- 依赖新的模块实例来执行 `vi.mock` 工厂函数 -If any of those apply, isolation is doing real work and should stay on. +如果存在上述任一情况,隔离机制就发挥着实际作用,应当保持启用。 -## Verifying it's safe +## 验证是否安全 {#verifying-its-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 +## 按执行池隔离 {#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. +`isolate` 只对 [`threads`](/config/pool) 和 [`forks`](/config/pool) 执行池生效。无论该选项如何设置,`vmThreads` 和 `vmForks` 执行池始终以隔离模式运行,因为它们牺牲启动速度来换取更强的隔离保障。 -## See also +## 相关链接 {#see-also} - [`isolate`](/config/isolate) -- [Test Projects](/guide/projects) -- [Improving Performance](/guide/improving-performance) -- [Parallel and Sequential Test Files](/guide/recipes/parallel-sequential) +- [测试项目](/guide/projects) +- [性能优化](/guide/improving-performance) +- [并行和串行测试文件](/guide/recipes/parallel-sequential) diff --git a/guide/recipes/explicit-resources.md b/guide/recipes/explicit-resources.md index 67dc35c1..f8bf8cc6 100644 --- a/guide/recipes/explicit-resources.md +++ b/guide/recipes/explicit-resources.md @@ -1,16 +1,16 @@ --- -title: Auto-Cleanup with `using` | Recipes +title: 使用 `using` 自动清理 | 技巧 --- -# Auto-Cleanup with `using` +# 使用 `using` 自动清理 {#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. +测试中创建的 spy 和 mock 需要在测试结束后恢复,否则状态会泄漏到其他测试。常见做法是在测试套件层级使用 `afterEach(() => vi.restoreAllMocks())`,或者在每个测试中内联调用 [`onTestFinished(() => spy.mockRestore())`](/api/hooks#ontestfinished)。 -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. +如果运行时支持 [显式资源管理](https://github.com/tc39/proposal-explicit-resource-management)(Node.js 24 及以上版本,或在现代打包工具中使用 TypeScript 5.2 及以上版本),还可以采用更简洁的方式:使用 `using` 而不是 `const` 声明 spy。退出代码块时,spy 会自动恢复。 -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 +此方式适用于 [`vi.spyOn`](/api/vi#vi-spyon)、[`vi.fn`](/api/vi#vi-fn) 和 [`vi.doMock`](/api/vi#vi-domock)。3.2.0 -## Pattern +## 用法 {#pattern} ```ts import { expect, it, vi } from 'vitest' @@ -25,10 +25,10 @@ it('calls console.log', () => { expect(spy).toHaveBeenCalled() }) -// console.log is restored here without an afterEach +// console.log 会在这里恢复,无须使用 afterEach ``` -The same pattern works with `vi.doMock`, which returns a disposable that queues an unmock when the scope exits: +同样的用法也适用于 `vi.doMock`。它会返回一个可释放对象,并在退出作用域时自动取消模块模拟: ```ts import { expect, it, vi } from 'vitest' @@ -42,19 +42,19 @@ it('uses the mocked module, then the real one', async () => { expect(loadUser('alice').name).toBe('Alice') } - // ./users is unmocked from here on + // 从这里开始,./users 不再处于模拟状态 }) ``` -## Scoped to any block +## 限定于任意代码块内 {#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: +`using` 采用块级作用域,因此可以只在测试的某一部分启用 spy。`afterEach` 和 `onTestFinished` 都无法实现这一点,因为它们只能在测试结束后运行: ```ts import { expect, it, vi } from 'vitest' it('only mocks fetch for the auth call', async () => { - // real fetch here + // 此处使用真实的 fetch await preloadConfig() { @@ -65,21 +65,21 @@ it('only mocks fetch for the auth call', async () => { expect(fetchSpy).toHaveBeenCalledOnce() } - // real fetch is back + // 恢复使用真实的 fetch 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. +如果只有少数调用需要恢复,也可以采用这种方式,避免在全局配置中启用 [`restoreMocks: true`](/config/restoremocks)。 -## Compatibility +## 兼容性 {#compatibility} -`using` requires support for the TC39 Explicit Resource Management proposal: +使用 `using` 需要环境支持 TC39 显式资源管理提案: -- 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. +- TypeScript ≥ 5.2(`target` 需设为 `'es2022'` 或更高版本,并包含默认启用的 `disposable` 库)。 +- Node.js ≥ 24(也可以使用 Node.js 22+ 也可以通过启用 `--harmony` 这类选项)。 -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: +如果当前环境尚不支持,可以使用 [`onTestFinished`](/api/hooks#ontestfinished) 实现效果最接近的整项测试清理。它允许在测试中直接注册清理逻辑,并且无论测试通过还是失败,都会在测试结束后执行: ```ts import { expect, it, onTestFinished, vi } from 'vitest' @@ -93,13 +93,13 @@ it('calls console.log', () => { }) ``` -`onTestFinished` can't tear down a spy mid-test the way `using` can, so the block-scoped pattern above remains specific to ERM. +`onTestFinished` 无法像 `using` 一样在测试执行过程中清理 spy,因此只有显式资源管理能够实现上述块级作用域用法。 -## See also +## 相关链接 {#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) +- [TC39 显式资源管理提案](https://github.com/tc39/proposal-explicit-resource-management) diff --git a/guide/recipes/parallel-sequential.md b/guide/recipes/parallel-sequential.md index f8dd5a47..b6791f55 100644 --- a/guide/recipes/parallel-sequential.md +++ b/guide/recipes/parallel-sequential.md @@ -1,14 +1,14 @@ --- -title: Parallel and Sequential Test Files | Recipes +title: 并行和串行测试文件 | 技巧 --- -# Parallel and Sequential Test Files +# 并行和串行测试文件 {#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. +全局禁用并行会拖慢测试套件中的所有测试。可以将测试套件拆分为两个 [`projects`](/guide/projects),一个并行运行,另一个串行运行。这样只有需要串行执行的文件会受到性能影响。 -## Pattern +## 用法 {#pattern} ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -34,11 +34,11 @@ export default defineConfig({ }) ``` -[`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. +在项目级别设置 [`fileParallelism: false`](/config/fileparallelism) 后,匹配的文件会依次运行,而测试套件中的其他文件仍可并行执行。该设置是 [`maxWorkers: 1`](/config/maxworkers) 的简写,两者作用相同。 -## Run sequential after parallel +## 在并行测试后运行串行测试 {#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: +默认情况下,项目之间会并行运行。因此,串行项目的第一个文件可能与仍持有相同资源的并行文件重叠。使用 [`sequence.groupOrder`](/config/sequence#sequence-grouporder) 3.2.0 可以强制并行批次先运行完毕: ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -66,24 +66,24 @@ export default defineConfig({ }) ``` -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 +## 文件级与测试级作用域 {#file-scope-vs-test-scope} -There are two different "parallel" knobs in Vitest. Don't confuse them: +Vitest 中有两个用于控制“并行”的控制方式,请不要混淆: -| 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`](/config/fileparallelism) | 两个测试 _文件_ 是否在并行 worker 中运行 | +| 文件内部 | `describe.concurrent`/`test.concurrent` | 同一文件中的测试是否并行运行 | -`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. +`fileParallelism: false` 不会让文件内的测试并行执行;默认情况下,文件内的测试会按顺序运行。同样,在 `describe` 或 `test` 上使用 `concurrent` 也不会影响测试文件的调度方式。 -## See also +## 相关链接 {#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) +- [并行执行](/guide/parallelism) +- [测试项目](/guide/projects) +- [按文件配置隔离](/guide/recipes/disable-isolation) diff --git a/guide/recipes/schema-matching.md b/guide/recipes/schema-matching.md index b22ace77..cfbc8376 100644 --- a/guide/recipes/schema-matching.md +++ b/guide/recipes/schema-matching.md @@ -1,14 +1,14 @@ --- -title: Schema-Driven Assertions | Recipes +title: Schema 驱动断言 | 技巧 --- -# Schema-Driven Assertions +# Schema 驱动断言 {#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`. +如果项目已使用 [Zod](https://zod.dev)、[Valibot](https://valibot.dev) 或 [ArkType](https://arktype.io) 验证数据,那么这些 schema 已经定义了有效的数据结构。在测试中复用这些类型,比在 `toEqual` 和 `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. +[`expect.schemaMatching`](/api/expect#expect-schemamatching) 4.0.0 是一个非对称匹配器,接受任意 [Standard Schema v1](https://standardschema.dev) 对象,如果待检查的值通过验证则匹配成功。 -## Pattern +## 用法 {#pattern} ```ts import { expect, test } from 'vitest' @@ -23,9 +23,9 @@ test('email validation', () => { }) ``` -`expect.schemaMatching` is an asymmetric matcher, so it composes inside any equality check the same way `expect.any` or `expect.stringMatching` do: +`expect.schemaMatching` 是一个非对称匹配器,因此可以像 `expect.any` 或 `expect.stringMatching` 一样,在任意相等性检查中组合使用: -- `toEqual` / `toStrictEqual` +- `toEqual`/`toStrictEqual` - `toMatchObject` - `toContainEqual` - `toThrow` @@ -33,7 +33,7 @@ test('email validation', () => { - `toHaveReturnedWith` - `toHaveBeenResolvedWith` -## Works with any Standard Schema library +## 支持所有兼容 Standard Schema 的库 {#works-with-any-standard-schema-library} ```ts import { expect, test } from 'vitest' @@ -59,9 +59,9 @@ expect(user).toEqual({ }) ``` -## Verifying call arguments +## 验证调用参数 {#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: +常见的用法是,在不逐一列出所有字段的情况下,断言调用模拟对象时传入的数据能够通过 schema 验证: ```ts import { expect, test, vi } from 'vitest' @@ -81,9 +81,9 @@ test('persists a valid user', () => { }) ``` -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. +当已有用于验证某个值的 schema,而测试原本需要手动列出它的每个属性时,可以使用 `schemaMatching`。它适用于断言 UUID 或时间戳等生成字段,因为只需验证格式是否正确,不必关心具体的值。 -## See also +## 相关链接 {#see-also} - [`expect.schemaMatching`](/api/expect#expect-schemamatching) - [Standard Schema](https://standardschema.dev) diff --git a/guide/recipes/type-narrowing.md b/guide/recipes/type-narrowing.md index 6b5abc62..20680fb4 100644 --- a/guide/recipes/type-narrowing.md +++ b/guide/recipes/type-narrowing.md @@ -8,7 +8,7 @@ title: 在测试中收窄类型 | 技巧 [`expect.assert`](/api/expect#assert) 4.0.0 会在运行时收窄 TypeScript 类型并抛出异常。同一个调用可以替代上述三种方式。 -## 示例 {#pattern} +## 用法 {#pattern} ```ts import { expect, test } from 'vitest' diff --git a/guide/recipes/watch-templates.md b/guide/recipes/watch-templates.md index eb330c91..f4ab284f 100644 --- a/guide/recipes/watch-templates.md +++ b/guide/recipes/watch-templates.md @@ -9,7 +9,7 @@ title: 监视非直接导入的文件 | 技巧 [`watchTriggerPatterns`](/config/watchtriggerpatterns) 3.2.0 可以让这些依赖关系显式化。你需要声明一个匹配文件路径的正则表达式,以及一 个回调函数,用于在匹配的文件变更时返回需要重新运行的测试。 -## 示例 {#pattern} +## 用法 {#pattern} ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config'