Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,10 @@ export default ({ mode }: { mode: string }) => {
text: 'Auto-Cleanup with `using`',
link: '/guide/recipes/explicit-resources',
},
{
text: 'Conditional Mocking with `vi.when`',
link: '/guide/recipes/conditional-mocking',
},
{
text: 'Per-File Isolation Settings',
link: '/guide/recipes/disable-isolation',
Expand Down
32 changes: 32 additions & 0 deletions api/expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -1454,6 +1454,38 @@ test('spy function returns bananas on second call', async () => {
expect(sell).toHaveNthResolvedWith(2, { product: 'bananas' })
})
```
<!-- TODO: translation -->
## toHaveBeenExhausted <Version>5.0.0</Version> {#tohavebeenexhausted}

- **Type:** `() => void`

This assertion checks that every behavior registered on a [`vi.when`](/api/vi#vi-when) chain has been consumed. A behavior is considered exhausted when it has been called the number of times specified by its `times` option, or at least once for behaviors that apply indefinitely.

Requires a `When` chain returned by `vi.when` to be passed to `expect`.

```ts
import { expect, test, vi } from 'vitest'

test('all behaviors were consumed', () => {
const spy = vi.fn()
const w = vi.when(spy)
.calledWith(1)
.thenReturnOnce('once')
.calledWith(2)
.thenReturn('always')

expect(w).not.toHaveBeenExhausted()

spy(1) // consumes the `thenReturnOnce` behavior
spy(2) // satisfies the `thenReturn` behavior (called at least once)

expect(w).toHaveBeenExhausted()
})
```

::: warning
A `When` chain with no registered behaviors is never considered exhausted. `toHaveBeenExhausted` only passes when at least one `calledWith` with an associated action (`then*`) has been registered and every registered behavior has been fully consumed.
:::

## called <Version>4.1.0</Version> {#called}

Expand Down
128 changes: 128 additions & 0 deletions api/vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,134 @@ globalThis.IntersectionObserver === undefined
// 抛出 ReferenceError,因为变量未定义
IntersectionObserver === undefined
```
<!-- TODO: translation -->
### vi.when <Version>5.0.0</Version> {#vi-when}

```ts
interface WhenOptions {
onUnmatched?: 'throw' | 'passthrough' | ((...args: unknown[]) => unknown)
}

interface BehaviorOptions {
times?: number
}

function when(spy: Mock, options?: WhenOptions): When
```

Defines per-argument behaviors on a spy, replacing its implementation for the duration of the `when` chain.

Call `.calledWith(...args)` on the returned object to specify which call arguments to match, then chain one or more `then*` methods to declare what the spy should return, throw, or resolve when invoked with those arguments. Arguments are matched with deep equality and support asymmetric matchers such as `expect.any()`.

```ts
const spy = vi.fn()

vi.when(spy)
.calledWith(1)
.thenReturn('one')
.calledWith(2)
.thenReturn('two')

expect(spy(1)).toBe('one')
expect(spy(2)).toBe('two')
```

Available `then*` methods:

| Method | Description |
|--------|-------------|
| `thenReturn(value, options?)` | Returns `value`. |
| `thenReturnOnce(value)` | Returns `value` once, then falls back. |
| `thenThrow(error, options?)` | Throws `error`. |
| `thenThrowOnce(error)` | Throws `error` once, then falls back. |
| `thenResolve(value, options?)` | Returns a resolved `Promise` with `value`. |
| `thenResolveOnce(value)` | Resolves once, then falls back. |
| `thenReject(error, options?)` | Returns a rejected `Promise` with `error`. |
| `thenRejectOnce(error)` | Rejects once, then falls back. |

The optional `times` option limits how many times a behavior applies before being exhausted. Behaviors registered for the same arguments are consumed last-in-first-out: the most recently registered behavior is tried first, and once exhausted, earlier ones act as fallbacks.

```ts
const spy = vi.fn<(key: string) => string>()

vi.when(spy)
.calledWith('theme')
.thenReturn('light') // fallback, applies indefinitely
.thenReturn('dark', { times: 2 }) // applied first for the next 2 calls

expect(spy('theme')).toBe('dark')
expect(spy('theme')).toBe('dark')
expect(spy('theme')).toBe('light') // falls back
```

When called with arguments that match no registered behavior, the spy falls through to its original implementation by default. Use the `onUnmatched` option to change this:

- `'passthrough'` (**default**): delegates to the spy's original implementation
- `'throw'`: throws an error listing the unmatched arguments
- a function: called with the unmatched arguments; its return value is used

```ts
const spy = vi.fn<(id: number) => string>()

vi.when(spy, { onUnmatched: 'throw' })
.calledWith(1)
.thenReturn('Alice')

expect(spy(1)).toBe('Alice')
expect(() => spy(99)).toThrow() // no behavior defined for 99
```

The `When` object returned by `vi.when` supports the [`toHaveBeenExhausted` assertion](/api/expect#tohavebeenexhausted), which passes once every registered behavior has been consumed.

```ts
const spy = vi.fn()
const w = vi.when(spy)
.calledWith(1)
.thenReturnOnce('once')
.calledWith(2)
.thenReturn('always')

expect(w).not.toHaveBeenExhausted()

spy(1) // consumes the `thenReturnOnce` behavior
spy(2) // satisfies `thenReturn` (called at least once)

expect(w).toHaveBeenExhausted()
```

::: tip
In environments that support [Explicit Resource Management](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management), you can use `using` instead of `const` to automatically restore the spy's original implementation when the containing block exits:

```ts
const spy = vi.fn(() => 'original')

{
using w = vi.when(spy)
.calledWith('hello')
.thenReturn('mocked')

expect(spy('hello')).toBe('mocked')
} // ← spy's original implementation is restored here

expect(spy('hello')).toBe('original')
```
:::

### vi.isWhenChain <Version>5.0.0</Version> {#vi-iswhenchain}

```ts
function isWhenChain(input: object): input is When
```

Returns `true` if the given value is a `When` chain created by [`vi.when`](#vi-when). If you are using TypeScript, it will also narrow down its type.

```ts
const spy = vi.fn()
const w = vi.when(spy).calledWith(1).thenReturn(0)

expect(vi.isWhenChain(w)).toBe(true)
expect(vi.isWhenChain(spy)).toBe(false)
```

## Fake Timers

Expand Down
2 changes: 1 addition & 1 deletion config/outputfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ outline: deep
- **类型:** `string | Record<string, string>`
- **命令行终端:** `--outputFile=<path>`, `--outputFile.json=./path`

当指定 `--reporter=json`、`--reporter=html` 或 `--reporter=junit` 时,将测试结果写入文件。通过提供对象而非字符串,你可以在使用多个报告器时定义各自的输出配置。
当指定 `--reporter=json` 或 `--reporter=junit` 时,将测试结果写入文件。通过提供对象而非字符串,你可以在使用多个报告器时定义各自的输出配置。
3 changes: 3 additions & 0 deletions guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ pnpm add -D vitest
bun add -D vitest
```

```bash [deno]
deno add -D vitest
```
:::

:::tip
Expand Down
4 changes: 4 additions & 0 deletions guide/learn/mock-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ test('mock async return values', async () => {
await expect(fetchUser()).rejects.toThrow('Not found')
})
```
<!-- TODO: translation -->
::: tip
`mockReturnValue` always returns the same value regardless of the arguments the mock receives. If you need argument-specific return values, [`vi.when`](/api/vi#vi-when) lets you attach different behaviors for different argument combinations without writing your own `if/else` logic. See the [Conditional Mocking](/guide/recipes/conditional-mocking) recipe for details.
:::

## 模拟实现 {#mock-implementation}

Expand Down
18 changes: 18 additions & 0 deletions guide/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ test('sort', async ({ bench }) => { // [!code ++]
- **`benchmark.outputJson` config and the `--outputJson` CLI flag** are removed. Use `--reporter=json --outputFile=<path>` to capture benchmark results; the JSON reporter now includes a `benchmarks` field on each test case.
- **`Vitest` instance `mode` property** is now always `'test'`. The previous `'benchmark'` value is no longer used; benchmarks run inside a dedicated project of the same `Vitest` instance.

### Vitest UI Requires an Authenticated URL

Vitest UI now requires token authentication for the HTML page and API access. The `/__vitest__/` URL will show an error until the browser is authenticated. To authenticate, open the URL with a token printed by Vitest, as shown below. Once authenticated, the direct `/__vitest__/` URL will work correctly.

```bash
vitest --ui
# UI started at http://localhost:51204/__vitest__/?token=...
```

### Removed `test.sequential`, `describe.sequential`, and `sequential` Options
### 移除 `test.sequential`, `describe.sequential`, 和 `sequential` 选项 {##removed-test-sequential-describe-sequential-and-sequential-options}

Vitest 5.0 移除了已弃用的 `test.sequential`、`describe.sequential` 和 `sequential` 选项。当你需要让某个测试或测试套件不再沿用继承来的并发设置,或退出全局配置的并发时,请使用 `concurrent: false`。
Expand Down Expand Up @@ -165,6 +175,14 @@ Vitest no longer serves the browser orchestrator UI from a bare `/__vitest_test_

If you manually opened the browser preview by copying the Vite server URL or visiting `/__vitest_test__/` directly, use the URL opened or printed by Vitest instead.

### Generated Reports and Artifacts Use the `.vitest` Directory

Vitest now uses a single `.vitest` directory at the project root as the shared artifact root, so one `.vitest` entry in `.gitignore` is enough. Defaults that moved this major:

- **Attachments** ([`attachmentsDir`](/config/attachmentsdir)): `.vitest-attachements/` → `.vitest/attachments/`
- **Blob reporter** and `--merge-reports`: `.vitest-reports/blob-*.json` → `.vitest/blob/blob-*.json`
- **HTML reporter** ([`html`](/guide/reporters#html-reporter)): `html/index.html` → `.vitest/index.html`, and its option changed from `outputFile` (a file) to `outputDir` (a directory)

## 迁移至 Vitest 4.0 {#vitest-4}

::: warning 前提条件
Expand Down
4 changes: 4 additions & 0 deletions guide/mocking/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
如果你需要传递自定义函数实现作为参数或创建新的模拟实体,你可以使用 [`vi.fn()`](/api/vi#vi-fn) 来创建一个模拟函数。

`vi.spyOn` 和 `vi.fn` 都共享相同的方法。
<!-- TODO: translation -->
::: tip
If you need a mock to return different values depending on the arguments it receives, [`vi.when()`](/api/vi#vi-when) lets you define argument-specific behaviors without writing your own `if/else` logic. See the [Conditional Mocking](/guide/recipes/conditional-mocking) recipe for details.
:::

## 示例 {#example}

Expand Down
Loading
Loading