diff --git a/.vitepress/config.ts b/.vitepress/config.ts
index c88bb295..c621fe9a 100644
--- a/.vitepress/config.ts
+++ b/.vitepress/config.ts
@@ -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',
diff --git a/api/expect.md b/api/expect.md
index c84512e6..dac621f2 100644
--- a/api/expect.md
+++ b/api/expect.md
@@ -1454,6 +1454,38 @@ test('spy function returns bananas on second call', async () => {
expect(sell).toHaveNthResolvedWith(2, { product: 'bananas' })
})
```
+
+## toHaveBeenExhausted 5.0.0 {#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 4.1.0 {#called}
diff --git a/api/vi.md b/api/vi.md
index 0530efb4..b8719a41 100644
--- a/api/vi.md
+++ b/api/vi.md
@@ -800,6 +800,134 @@ globalThis.IntersectionObserver === undefined
// 抛出 ReferenceError,因为变量未定义
IntersectionObserver === undefined
```
+
+### vi.when 5.0.0 {#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 5.0.0 {#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
diff --git a/config/outputfile.md b/config/outputfile.md
index 68ab3fb6..62780b0d 100644
--- a/config/outputfile.md
+++ b/config/outputfile.md
@@ -8,4 +8,4 @@ outline: deep
- **类型:** `string | Record`
- **命令行终端:** `--outputFile=`, `--outputFile.json=./path`
-当指定 `--reporter=json`、`--reporter=html` 或 `--reporter=junit` 时,将测试结果写入文件。通过提供对象而非字符串,你可以在使用多个报告器时定义各自的输出配置。
+当指定 `--reporter=json` 或 `--reporter=junit` 时,将测试结果写入文件。通过提供对象而非字符串,你可以在使用多个报告器时定义各自的输出配置。
diff --git a/guide/index.md b/guide/index.md
index 754bc41b..3e71a121 100644
--- a/guide/index.md
+++ b/guide/index.md
@@ -39,6 +39,9 @@ pnpm add -D vitest
bun add -D vitest
```
+```bash [deno]
+deno add -D vitest
+```
:::
:::tip
diff --git a/guide/learn/mock-functions.md b/guide/learn/mock-functions.md
index 0b04dea8..0f3c1f8e 100644
--- a/guide/learn/mock-functions.md
+++ b/guide/learn/mock-functions.md
@@ -71,6 +71,10 @@ test('mock async return values', async () => {
await expect(fetchUser()).rejects.toThrow('Not found')
})
```
+
+::: 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}
diff --git a/guide/migration.md b/guide/migration.md
index 5ce3bf4b..324eae03 100644
--- a/guide/migration.md
+++ b/guide/migration.md
@@ -43,6 +43,16 @@ test('sort', async ({ bench }) => { // [!code ++]
- **`benchmark.outputJson` config and the `--outputJson` CLI flag** are removed. Use `--reporter=json --outputFile=` 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`。
@@ -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 前提条件
diff --git a/guide/mocking/functions.md b/guide/mocking/functions.md
index 9c406735..d4670fe2 100644
--- a/guide/mocking/functions.md
+++ b/guide/mocking/functions.md
@@ -7,6 +7,10 @@
如果你需要传递自定义函数实现作为参数或创建新的模拟实体,你可以使用 [`vi.fn()`](/api/vi#vi-fn) 来创建一个模拟函数。
`vi.spyOn` 和 `vi.fn` 都共享相同的方法。
+
+::: 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}
diff --git a/guide/recipes/conditional-mocking.md b/guide/recipes/conditional-mocking.md
new file mode 100644
index 00000000..7d8c68dd
--- /dev/null
+++ b/guide/recipes/conditional-mocking.md
@@ -0,0 +1,271 @@
+---
+title: Conditional Mocking with vi.when | Recipes
+---
+
+
+# Conditional Mocking with `vi.when`
+
+::: tip Prerequisites
+This recipe assumes you already have some familiarity with [mocking](/guide/mocking) in Vitest.
+:::
+
+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:
+
+```ts
+db.findById.mockImplementation((id) => {
+ if (id === 1) {
+ return Promise.resolve({ id: 1, name: 'Ella' })
+ }
+
+ if (id === 2) {
+ return Promise.resolve({ id: 2, name: 'Gracie' })
+ }
+
+ return Promise.resolve(undefined)
+})
+```
+
+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.
+
+## Pattern
+
+`vi.when` takes a spy and lets you define argument-specific behaviors.
+
+Call `.calledWith(...args)` to declare which arguments to match. This creates a _behavior_.
+
+Then attach an _action_ by calling a `then*` method. The action determines what happens when the behavior matches.
+
+Multiple behaviors can be chained on the same spy:
+
+```ts
+import { test, vi } from 'vitest'
+import { getUserById } from './user.ts'
+
+test('returns user data', async () => {
+ const db = { findById: vi.fn() }
+
+ vi.when(db.findById)
+ .calledWith(1)
+ .thenResolve({ id: 1, name: 'Ella' })
+ .calledWith(2)
+ .thenResolve({ id: 2, name: 'Gracie' })
+
+ await expect(getUserById(db, 1)).resolves.toEqual({ name: 'Ella' })
+ await expect(getUserById(db, 2)).resolves.toEqual({ name: 'Gracie' })
+})
+```
+
+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` |
+| `thenResolve(value)` | `mockResolvedValue(value)` | `return Promise.resolve(value)` |
+| `thenReject(error)` | `mockRejectedValue(error)` | `return Promise.reject(error)` |
+
+## 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.
+
+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'
+import { readConfig } from './config.ts'
+
+test('retries after an initial failure', async () => {
+ const fetchInstance = vi.fn<() => Promise>()
+
+ 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 })
+
+ expect(fetchInstance).toHaveBeenCalledTimes(2)
+})
+```
+
+For convenience, `then*Once` shorthands are available and equivalent to `{ times: 1 }`: `thenReturnOnce`, `thenResolveOnce`, `thenThrowOnce`, `thenRejectOnce`.
+
+## 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:
+
+```ts
+test('sends email to each recipient', () => {
+ vi.when(sendEmail)
+ .calledWith(expect.stringContaining('@'))
+ .thenReturn({ ok: true, message: 'sent via external relay' })
+})
+```
+
+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.
+
+```ts
+test('sends email to each recipient', () => {
+ vi.when(sendEmail)
+ .calledWith(expect.stringContaining('@internal.example.com'))
+ .thenReturn({ ok: true, message: 'sent via internal relay' })
+ .calledWith(expect.stringContaining('@'))
+ .thenReturn({ ok: true, message: 'sent via external relay' })
+})
+```
+
+::: 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.
+
+This is especially important with broad asymmetric matchers:
+
+```ts
+vi.when(getRole)
+ .calledWith(expect.any(String))
+ .thenReturn('user')
+ .calledWith('admin@example.com')
+ .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:
+
+```ts
+vi.when(getRole)
+ .calledWith(expect.any(String))
+ .thenReturn('user')
+ .thenReturnOnce('admin')
+```
+
+As a result, the first call with any string returns `'admin'`, while later calls return `'user'`:
+
+```ts
+expect(getRole('user@example.com')).toBe('admin')
+expect(getRole('user@example.com')).toBe('user')
+```
+:::
+
+## 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`.
+
+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).
+
+### `onUnmatched: 'throw'`
+
+Pass `{ onUnmatched: 'throw' }` to throw whenever the spy is called with unregistered arguments:
+
+```ts
+vi.when(db.findById, { onUnmatched: 'throw' })
+ .calledWith(1)
+ .thenResolve({ id: 1, name: 'Ella' })
+
+await expect(db.findById(1)).resolves.toMatchObject({ name: 'Ella' })
+await expect(db.findById(3)).rejects.toThrow(
+ 'vi.when: no behavior defined when called with [3]',
+)
+```
+
+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.
+
+```ts
+const db = { findById: vi.fn() }
+
+test('returns a placeholder for unknown ids', async () => {
+ vi.when(
+ db.findById,
+ { onUnmatched: id => Promise.resolve({ id, name: `User ${id}` }) }
+ )
+ .calledWith(1)
+ .thenResolve({ id: 1, name: 'Ella' })
+
+ await expect(db.findById(1)).resolves.toMatchObject({ name: 'Ella' })
+ await expect(db.findById(42)).resolves.toMatchObject({ name: 'User 42' })
+})
+```
+
+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.
+
+### 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.
+
+```ts
+vi.when(db.findById)
+ .calledWith(1)
+ .thenResolve({ id: 1, name: 'Ella' })
+ .calledWith(2)
+ .thenResolve({ id: 2, name: 'Gracie' })
+ .calledWith(expect.any(Number))
+ .thenReject(new Error('user not found'))
+```
+
+## 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:
+
+```ts
+test('loads both users', async () => {
+ const db = { findById: vi.fn() }
+
+ const w = vi.when(db.findById)
+ .calledWith(1)
+ .thenResolveOnce({ id: 1, name: 'Ella' })
+ .calledWith(2)
+ .thenResolveOnce({ id: 2, name: 'Gracie' })
+
+ await loadDashboard(db)
+
+ expect(w).toHaveBeenExhausted()
+})
+```
+
+In this example, if `loadDashboard` only calls `findById(1)`, the test fails with a message listing the behaviors that were never matched:
+
+```
+AssertionError: expected all behaviors to have been exhausted, but some remain:
+
+ calledWith(2)
+ ✗ 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.
+
+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.
+:::
+
+## Automatic cleanup with `using`
+
+`vi.when` supports the [Explicit Resource Management](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management) protocol.
+
+Declare the chain with `using` to scope behaviors to the current block and restore the spy automatically when execution leaves it.
+
+```ts
+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
+
+- [`vi.when`](/api/vi#vi-when)
+- [`toHaveBeenExhausted`](/api/expect#tohavebeenexhausted)
+- [`vi.isWhenChain`](/api/vi#vi-iswhenchain)
+- [Auto-Cleanup with `using`](/guide/recipes/explicit-resources)
diff --git a/guide/reporters.md b/guide/reporters.md
index 01e8de80..ab2a21b6 100644
--- a/guide/reporters.md
+++ b/guide/reporters.md
@@ -57,7 +57,7 @@ export default defineConfig({
## 报告器输出 {#reporter-output}
-默认情况下,Vitest 的报告器会将输出打印到终端。当使用 `json`、`html` 或 `junit` 报告器时,你可以在 Vite 配置文件中或通过 CLI 加入 `outputFile` [配置选项](/config/outputfile),将测试输出写入文件。
+默认情况下,Vitest 的报告器会将输出打印到终端。当使用 `json` 或 `junit` 报告器时,你可以在 Vite 配置文件中或通过 CLI 加入 `outputFile` [配置选项](/config/outputfile),将测试输出写入文件。
:::code-group
@@ -518,8 +518,8 @@ export default defineConfig({
### HTML 报告器 {#html-reporter}
生成 HTML 文件,通过交互式 [GUI](/guide/ui) 查看测试结果。文件生成后,Vitest 将保持本地开发服务器运行,并提供一个链接,以便在浏览器中查看报告。
-
-可使用 [`outputFile`](/config/outputfile) 配置选项指定输出文件。如果没有提供 `outputFile` 选项,则会创建一个新的 HTML 文件。
+
+The report artifact root can be specified using the reporter's `outputDir` option. The report entry is written to `/index.html` and the UI assets files live under `/ui/`. By default `outputDir` is `.vitest`, the shared Vitest artifact directory, so attachments (`.vitest/attachments`) and coverage (`.vitest/coverage`) are reused without being copied.
:::code-group
diff --git a/guide/ui.md b/guide/ui.md
index e20b2054..62281ade 100644
--- a/guide/ui.md
+++ b/guide/ui.md
@@ -18,6 +18,10 @@ vitest --ui
最后,你可以访问 Vitest UI 界面,通过 `http://localhost:51204/__vitest__/`
+::: tip
+Vitest UI access is protected. If the direct URL shows an error, open the URL with a token printed by Vitest in the terminal, for example `http://localhost:51204/__vitest__/?token=...`.
+:::
+
::: warning
UI 是交互式的,需要一个正在运行的 Vite 服务器,因此请确保在 `watch` 模式(默认模式)下运行 Vitest。或者,你可以通过在配置的 `reporters` 选项中指定 `html` 来生成一个与 Vitest UI 完全相同的静态 HTML 报告。
:::
@@ -47,10 +51,10 @@ export default defineConfig({
要预览你的 HTML 报告,可以使用 [vite preview](https://vitejs.dev/guide/cli.html#vite-preview) 命令:
```sh
-npx vite preview --outDir ./html
+npx vite preview --outDir .vitest
```
-
-你可以使用 [`outputFile`](/config/outputfile) 配置选项配置输出。你需要在那里指定 `.html` 路径。例如,`./html/index.html` 是默认值。
+
+You can configure the output location with the HTML reporter's `outputDir` option. It points to the report artifact root, and the report entry is written to `/index.html`. The default value is `.vitest`, the shared Vitest artifact directory.
:::
If you need a portable report that can be opened or shared as one file, see [`singleFile`](/guide/reporters#html-reporter) in the HTML reporter documentation.
@@ -63,7 +67,7 @@ If you need a portable report that can be opened or shared as one file, see [`si
id: upload-report
with:
name: vitest-report
- path: html/
+ path: .vitest/
- name: Viewer link in summary
run: echo "[View HTML report](https://viewer.vitest.dev/?url=${{ steps.upload-report.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY
@@ -76,7 +80,7 @@ When you use `singleFile: true`, you can upload the report as a single file and
```yaml
- uses: actions/upload-artifact@v7
with:
- path: html/index.html
+ path: .vitest/index.html
archive: false
```
:::