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 ``` :::