From 7863e54f9e431e97f255a3070e1d7ccc091fc2e2 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 5 Aug 2026 13:04:30 +0200 Subject: [PATCH 01/30] feat!: fail the test when an asynchronous assertion is not awaited (#10868) --- api/expect.md | 8 ++------ guide/learn/async.md | 2 +- guide/migration.md | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/api/expect.md b/api/expect.md index d2ceeead..19f1bcb7 100644 --- a/api/expect.md +++ b/api/expect.md @@ -1872,9 +1872,7 @@ test('buyApples returns new stock id', async () => { ``` :::warning -If the assertion is not awaited, then you will have a false-positive test that will pass every time. To make sure that assertions are actually called, you may use [`expect.assertions(number)`](#expect-assertions). - -Since Vitest 3, if a method is not awaited, Vitest will show a warning at the end of the test. In Vitest 4, the test will be marked as "failed" if the assertion is not awaited. +If the assertion is not awaited, the test will be marked as "failed" at the end of the test. ::: ## rejects @@ -1903,9 +1901,7 @@ test('buyApples throws an error when no id provided', async () => { ``` :::warning -If the assertion is not awaited, then you will have a false-positive test that will pass every time. To make sure that assertions were actually called, you can use [`expect.assertions(number)`](#expect-assertions). - -Since Vitest 3, if a method is not awaited, Vitest will show a warning at the end of the test. In Vitest 4, the test will be marked as "failed" if the assertion is not awaited. +If the assertion is not awaited, the test will be marked as "failed" at the end of the test. ::: ## expect.assertions diff --git a/guide/learn/async.md b/guide/learn/async.md index 4997ff87..1a52aa84 100644 --- a/guide/learn/async.md +++ b/guide/learn/async.md @@ -46,7 +46,7 @@ test('rejects with an error', async () => { ``` ::: warning -Don't forget the `await` before `expect`. Vitest will detect unawaited assertions and print a warning at the end of the test, but it's best to always include `await` explicitly. Vitest will also wait for all pending promises in `Promise.all` before starting the next test, but relying on this behavior makes tests harder to understand. +Don't forget the `await` before `expect`. Vitest will detect unawaited assertions and fail the test at the end of it. ::: ## Assertion Counting diff --git a/guide/migration.md b/guide/migration.md index 986703a1..a9d238c7 100644 --- a/guide/migration.md +++ b/guide/migration.md @@ -322,6 +322,21 @@ await expect.poll(async ({ signal }) => { A poll that legitimately needs more time should raise its `timeout`. Otherwise it fails with `expect.poll() function didn't resolve in time.` (or `expect.poll() assertion didn't resolve in time.`). +### Unawaited Asynchronous Assertions Fail the Test + +Asynchronous assertions, like `resolves`, `rejects` and `toMatchFileSnapshot`, now fail the test if they are not awaited. Previously, Vitest auto-awaited them at the end of the test and printed a warning: + +```ts +test('unawaited assertion', async () => { + // v4: prints a warning, the test passes // [!code --] + // v5: the test fails // [!code ++] + expect(promise).resolves.toBe(1) // [!code --] + await expect(promise).resolves.toBe(1) // [!code ++] +}) +``` + +The reported error points to the assertion that was not awaited. + ### Test Titles and Inspected Values Use `pretty-format` Vitest now formats values with [`pretty-format`](https://www.npmjs.com/package/pretty-format) instead of `loupe` when it inspects them, including the values interpolated into [`test.each`](/api/test#test-each) and [`test.for`](/api/test#test-for) titles. The rendering of some values changes, so snapshots or assertions that capture inspected output may need updating. From 2e1be0c3f461caaa133a43b9a19b9c62fe0e6cd9 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 7 Aug 2026 14:11:47 +0200 Subject: [PATCH 02/30] feat(vm): support `require(esm)` in vm pools (#10829) --- config/pool.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pool.md b/config/pool.md index 4639d18f..65b7688a 100644 --- a/config/pool.md +++ b/config/pool.md @@ -31,6 +31,8 @@ Restarting a worker thread is not free: Node.js runs a full garbage collection o The `vmForks` pool recycles workers by letting the child process exit, and the operating system reclaims the memory. If your test suite is large enough to recycle workers, `vmForks` is usually noticeably faster than `vmThreads`, even though its communication with the main process is slower. ::: +On Node.js 24.9 and later, `require()` of an ES module is supported inside vm pools, mirroring [Node's own `require(esm)`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require). Calling `require()` on an ES module whose graph contains top-level `await` throws `ERR_REQUIRE_ASYNC_MODULE` - use `await import()` for those files. + ::: warning Running code in a sandbox has some advantages (faster tests), but also comes with a number of disadvantages. From e512e7c7c7e08a33ed3be284157b3431e5816e67 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 7 Aug 2026 15:21:55 +0200 Subject: [PATCH 03/30] feat: report the duration breakdown as percentages (#10820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ari Perkkiö --- config/experimental.md | 83 ++++++++++++++++++++++++++++++++++ guide/cli-generated.md | 28 ++++++++++++ guide/cli.md | 44 ++++++++++++++++++ guide/improving-performance.md | 52 ++++++++++++++++++++- 4 files changed, 205 insertions(+), 2 deletions(-) diff --git a/config/experimental.md b/config/experimental.md index a3a188ce..82d244ef 100644 --- a/config/experimental.md +++ b/config/experimental.md @@ -413,3 +413,86 @@ const tags = getTags() test('my test', { tags }, () => {}) ``` ::: + +## experimental.diagnostics 5.0.0 {#experimental-diagnostics} + +- **Type:** + +```ts +interface DiagnosticsOptions { + /** + * Hint when `isolate: true` spends a significant amount of time spawning + * a fresh worker (and re-creating the environment) for every test file, + * estimating how much `isolate: false` could save. + * @default true + */ + isolate?: boolean + /** + * Hint when re-creating a DOM environment for every test file dominates + * the run and a `vm` pool would set it up once per worker. + * @default true + */ + environment?: boolean + /** + * Hint when test files repeatedly evaluate the same module graph + * (typical for barrel-file imports) and `isolate: false` would + * evaluate it once per worker. + * @default true + */ + import?: boolean + /** + * Hint when transforming modules dominates the run and + * `fsModuleCache` would persist the results across runs. + * @default true + */ + transform?: boolean +} +``` + +- **Default:** `true` + +Print performance hints after the run when the collected timings show that a configuration change would make the run significantly faster: + +``` +Environment jsdom was created 40 times · 23.80s total, 79% of tracked time + create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files) + learn more: https://vitest.dev/guide/improving-performance#test-environments +``` + +Hints never suggest changing an option that was set explicitly: if the config defines `pool`, other pools are not suggested, and an explicitly configured `isolate` is never suggested to be disabled. Hints are also printed in CI. Set the option to `false` to disable all hints, or disable them individually. + +To measure the impact of a configuration change instead of estimating it, run [`vitest doctor`](/guide/cli#vitest-doctor). + +### experimental.diagnostics.isolate {#experimental-diagnostics-isolate} + +- **Type:** `boolean` +- **Default:** `true` + +Hint when `isolate: true` spends a significant amount of time spawning a fresh worker (and re-creating the environment) for every test file, estimating how much `isolate: false` could save. Reused workers also keep evaluated modules alive, so files stop re-evaluating the module graph they share. Per-module evaluation times are only collected when [`experimental.importDurations`](#experimental-importdurations) is enabled; without it the estimate counts the worker startups alone and is reported as a lower bound ("at least"). + +### experimental.diagnostics.environment {#experimental-diagnostics-environment} + +- **Type:** `boolean` +- **Default:** `true` + +Hint when re-creating a DOM environment for every test file dominates the run and a `vm` pool would set it up once per worker. + +### experimental.diagnostics.import {#experimental-diagnostics-import} + +- **Type:** `boolean` +- **Default:** `true` + +Hint when test files repeatedly evaluate the same module graph and `isolate: false` would evaluate it once per worker. This is typical for barrel-file imports: every test file imports a few symbols through an index file and evaluates the whole graph behind it. The duplication is measured from how often each module was served to the workers, so suites whose test files import mostly disjoint modules stay quiet: reusing workers would not reduce their import work. + +``` +Import 837 modules were evaluated 16740 times · 15.69s total, 64% of tracked time + ~850ms faster with isolate: false — shared modules are evaluated once per worker instead of once per file + learn more: https://vitest.dev/guide/improving-performance#test-isolation +``` + +### experimental.diagnostics.transform {#experimental-diagnostics-transform} + +- **Type:** `boolean` +- **Default:** `true` + +Hint when transforming modules dominates the run. Without a persistent cache every `vitest run` transforms the whole module graph from scratch; [`fsModuleCache`](/config/fsmodulecache) stores the results on disk so repeated runs skip them. The hint estimates the time the next run would save. On CI the hint includes a note that the cache directory must be persisted between runs for the cache to take effect. diff --git a/guide/cli-generated.md b/guide/cli-generated.md index bd55f1f3..e06f45f7 100644 --- a/guide/cli-generated.md +++ b/guide/cli-generated.md @@ -992,3 +992,31 @@ Custom provider for detecting changed files. (default: `git`) - **Config:** [experimental.preParse](/config/experimental#experimental-preparse) Parse test specifications before running them. This will apply `.only` flag and test name pattern across all files without running them. (default: `false`) + +### experimental.diagnostics.isolate + +- **CLI:** `--experimental.diagnostics.isolate` +- **Config:** [experimental.diagnostics.isolate](/config/experimental#experimental-diagnostics-isolate) + +Print a hint estimating how much time `isolate: false` would save when `isolate: true` spends a significant amount of time spawning a worker per test file. (default: `true`) + +### experimental.diagnostics.environment + +- **CLI:** `--experimental.diagnostics.environment` +- **Config:** [experimental.diagnostics.environment](/config/experimental#experimental-diagnostics-environment) + +Print a hint when re-creating a DOM environment for every test file dominates the run and a `vm` pool would set it up once per worker. (default: `true`) + +### experimental.diagnostics.import + +- **CLI:** `--experimental.diagnostics.import` +- **Config:** [experimental.diagnostics.import](/config/experimental#experimental-diagnostics-import) + +Print a hint when test files repeatedly evaluate the same module graph (typical for barrel-file imports) and `isolate: false` would evaluate it once per worker. (default: `true`) + +### experimental.diagnostics.transform + +- **CLI:** `--experimental.diagnostics.transform` +- **Config:** [experimental.diagnostics.transform](/config/experimental#experimental-diagnostics-transform) + +Print a hint when transforming modules dominates the run and `fsModuleCache` would persist the results across runs. (default: `true`) diff --git a/guide/cli.md b/guide/cli.md index dcef0f02..afff5f82 100644 --- a/guide/cli.md +++ b/guide/cli.md @@ -123,6 +123,50 @@ tests/test2.test.ts Since Vitest 4.1, you may pass `--static-parse` to [parse test files](/api/advanced/vitest#parsespecifications) instead of running them to collect tests. Vitest parses test files with limited concurrency, defaulting to `os.availableParallelism()`. You can change it via the `--static-parse-concurrency` option. +### `vitest doctor` + +`vitest doctor` measures how much faster the test suite would run under alternative configurations by running it under each of them. The candidates are picked based on the current config: + +```bash +vitest doctor +``` + +``` +Results (min of 3 runs each) + + baseline (pool: forks · isolate: true) 4.08s + pool: 'threads' 3.64s (-11%) + pool: 'vmThreads' 1.33s (-67%) + isolate: false 1.28s (-69%) + +Recommendation: pool: 'vmThreads' (-67%) + + // vitest.config.ts + import { defineConfig } from 'vitest/config' + + export default defineConfig({ + test: { + pool: 'vmThreads', // measured -67% on this suite + }, + }) +``` + +The `isolate: false` candidate is additionally validated by running the suite twice with a shuffled file order: if any test depends on isolation, the candidate is reported as failed instead of recommended. When several candidates are close to the fastest, doctor prefers the one that keeps per-file isolation. + +Doctor also probes lower [`maxWorkers`](/config/maxworkers) values on top of the winning configuration: every worker funnels its transform requests through the single main-thread Vite server, so past a certain count more workers make the run slower, not faster. Starting from half the current worker count, doctor keeps halving while the suite gets at least 5% faster, and includes the winning value in the recommendation. + +Suites running a DOM environment are measured under both vm pools, `vmThreads` and `vmForks`: they amortize the environment creation cost by keeping one environment per worker while every file still gets a fresh VM context. `vmForks` uses child processes instead of worker threads: each child gets its own heap and garbage collector, so either pool can come out faster depending on the suite, and `vmForks` is the vm option for suites that cannot run in worker threads. + +Projects running `jsdom` are also measured under `environment: 'happy-dom'` when the package is installed. The swap is applied per project; projects on other environments keep them. happy-dom implements the DOM differently than jsdom, so tests that depend on layout or navigation should be verified before adopting the swap. When the [fs module cache](/config/fsmodulecache) is off, doctor measures `fsModuleCache: true` after an untimed priming run that populates the cache, so the reported time is what repeated runs pay. + +Every measurement runs the full suite, including browser projects: `isolate: false` also affects browser mode. Candidates that cannot affect browser projects (`pool`, `environment`, the fs module cache) are picked based on the node-side projects only. + +Failing candidates are reported with an excerpt of their errors. If the suite fails under the current configuration, doctor aborts and shows the errors: it needs a passing baseline to compare against. + +Short suites are measured multiple times and the best time is reported, so the comparison reflects a warm steady state. Doctor runs the full suite several times, so it takes a multiple of a normal run's time. See [Improving Performance](/guide/improving-performance) for the trade-offs behind every candidate. + +Doctor measures and reports the baseline even when there are no candidates to compare. Configurations on a `vm` pool are additionally compared against `pool: 'threads'` with `isolate: false`, which also reuses workers but shares module state between files; a configuration already on one vm pool is still measured under the other. + ## Shell Autocompletions Vitest provides shell autocompletions for commands, options, and option values powered by [`@bomb.sh/tab`](https://github.com/bombshell-dev/tab). diff --git a/guide/improving-performance.md b/guide/improving-performance.md index 58244a92..4cf23185 100644 --- a/guide/improving-performance.md +++ b/guide/improving-performance.md @@ -1,5 +1,28 @@ # Improving Performance +## Profile First + +The `Duration` line of the summary breaks the run down into phases, as percentages of all tracked time: + +``` +Duration 3.76s (environment 79%, import 13%, transform 6%, tests 1%, setup 1%) +``` + +The percentages are relative to the sum of all tracked phases, not to the wall-clock time: phases run in parallel workers, so their sum is usually larger than the run itself. In a multi-project setup the percentages aggregate over all [projects](/guide/projects), so a phase that dominates one project can be diluted by the others; the performance hints below analyze each project separately. + +The phases map to configuration options: + +- `environment` - creating the test environment (for example `jsdom`, `happy-dom`) for test files. See [Test Environments](#test-environments). +- `transform` - waiting for Vite to resolve and transform imported modules. See [Caching Between Reruns](#caching-between-reruns). +- `import` - evaluating test files and their modules, excluding the transform wait tracked above. When files import mostly the same modules (typical for barrel-file imports), isolation re-evaluates that shared graph for every file. See [Test Isolation](#test-isolation). +- `setup` - running [`setupFiles`](/config/setupfiles). +- `worker` - preparing the test runner in each worker. Isolation pays this cost for every test file. See [Test Isolation](#test-isolation). +- `tests` - running the tests themselves. A run dominated by this phase has little to gain from configuration changes. + +When the collected timings show that a configuration change would make the run significantly faster, Vitest also prints a hint after the summary, see [`experimental.diagnostics`](/config/experimental#experimental-diagnostics). Hints never suggest changing an option that was set explicitly. + +[`vitest doctor`](/guide/cli#vitest-doctor) measures the alternative configurations instead of estimating them: it runs the suite under each candidate and reports the comparison, including whether the tests pass with `isolate: false`. + ## Test Isolation By default Vitest runs every test file in an isolated environment based on the [pool](/config/pool): @@ -73,6 +96,31 @@ export default defineConfig({ ``` ::: +## Test Environments + +DOM environments are expensive to create: `jsdom` costs roughly 200-500ms per import and `happy-dom` roughly 90-200ms, plus the time to construct the window. With an isolating pool (the default), that cost is paid for every test file, because every file gets a fresh worker. On DOM-heavy suites this is often the largest cost of the run; it appears as the `environment` share of the `Duration` breakdown. + +Three configurations reduce this cost: + +| configuration | environment created | isolation | trade-off | +|---|---|---|---| +| `pool: 'forks'`/`'threads'` + `isolate: true` (default) | once per file | fresh process/thread and environment per file | safest, slowest | +| `pool: 'vmThreads'` | once per worker | fresh VM context and `window` per file | test code runs in a VM realm: cross-realm `instanceof` edge cases with externalized packages, and memory is not reclaimed as reliably (see [`vmMemoryLimit`](/config/vmmemorylimit)) | +| `isolate: false` | once per worker | none - files in the same worker share the environment and module state | tests must not depend on a clean `window` or module state; run `vitest doctor` to check | + +```ts [vitest.config.js] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'jsdom', + pool: 'vmThreads', // environment per worker, fresh window per file + }, +}) +``` + +Prefer `isolate: false` with `threads` if the tests tolerate shared state: it is the fastest option and keeps memory behavior simple. Use `vmThreads` when every file needs a fresh `window` and the per-file environment cost dominates the run. `happy-dom` is cheaper to create than `jsdom` in every setup. + ## Limiting Directory Search You can limit the working directory when Vitest searches for files using [`test.dir`](/config/dir) option. This should make the search faster if you have unrelated folders and files in the root directory. @@ -85,10 +133,10 @@ This improvement is most noticeable when rerunning a small number of tests that ```shell # the first run -Duration 8.75s (transform 4.02s, setup 629ms, import 5.52s, tests 2.52s, environment 0ms, prepare 3ms) +Duration 8.75s (import 43%, transform 32%, tests 20%, setup 5%) # the second run -Duration 5.90s (transform 842ms, setup 543ms, import 2.35s, tests 2.94s, environment 0ms, prepare 3ms) +Duration 5.90s (tests 44%, import 35%, transform 13%, setup 8%) ``` ## Node Compile Cache From 3e063df19d84a17b5b1607c79f06beeb6beca45b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:52:46 +0200 Subject: [PATCH 04/30] chore(deps): update dependency @antfu/ni to v30 (#10900) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9d3d5262..d7ea8e33 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "vue": "catalog:" }, "devDependencies": { - "@antfu/ni": "^28.3.0", + "@antfu/ni": "^30.5.0", "@iconify-json/carbon": "catalog:", "@iconify-json/logos": "catalog:", "@iconify/vue": "catalog:", From 5ae4afd2b0a48f57f8c56d4304eedb0bf2accf7b Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 10 Aug 2026 10:35:16 +0200 Subject: [PATCH 05/30] docs: add TestMu AI to sponsors (#10904) --- .vitepress/sponsors.ts | 5 +++++ public/testmuai.svg | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 public/testmuai.svg diff --git a/.vitepress/sponsors.ts b/.vitepress/sponsors.ts index 6a906b54..f096c171 100644 --- a/.vitepress/sponsors.ts +++ b/.vitepress/sponsors.ts @@ -67,6 +67,11 @@ export const sponsors: SponsorTier[] = [ url: 'https://aerius.se/', img: '/aerius.png', }, + { + name: 'TestMu AI', + url: 'https://www.testmuai.com/?utm_medium=sponsor&utm_source=vitest-dev', + img: '/testmuai.svg', + }, ], }, ] diff --git a/public/testmuai.svg b/public/testmuai.svg new file mode 100644 index 00000000..6fe08857 --- /dev/null +++ b/public/testmuai.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + From c890a478cbb5efe6b1aa7cb5bfeb7dfa793a8dbb Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 10 Aug 2026 15:20:10 +0200 Subject: [PATCH 06/30] feat!: share the Vite server between inline projects (#10848) --- .vitepress/config.ts | 4 ++ api/advanced/test-project.md | 10 ++++- config/sharedviteserver.md | 74 ++++++++++++++++++++++++++++++++++++ guide/cli-generated.md | 7 ++++ guide/migration.md | 26 +++++++++++++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 config/sharedviteserver.md diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 3076617a..dcb9f30b 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -542,6 +542,10 @@ export default ({ mode }: { mode: string }) => { text: 'projects', link: '/config/projects', }, + { + text: 'sharedViteServer', + link: '/config/sharedviteserver', + }, { text: 'isolate', link: '/config/isolate', diff --git a/api/advanced/test-project.md b/api/advanced/test-project.md index 4f204c87..091d82e1 100644 --- a/api/advanced/test-project.md +++ b/api/advanced/test-project.md @@ -98,7 +98,15 @@ It is based on the root of the project and its name. Note that the root path is ## vite -This is project's [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver). All projects have their own Vite servers. +This is project's [`ViteDevServer`](https://vite.dev/guide/api-javascript#vitedevserver). Note that the server is not necessarily exclusive to this project: other projects can reuse it when the [`sharedViteServer`](/config/sharedviteserver) option applies, and browser instances of the same cluster share a single browser server. + +## sharedViteServer + +```ts +const sharedViteServer: boolean +``` + +`true` when the project reuses the Vite server of the config that declared it instead of resolving its own (see the [`sharedViteServer`](/config/sharedviteserver) option). The project that owns the server reports `false` even when other projects reuse it. To detect any two projects sharing a server (including browser instances), compare their [`vite`](#vite) references. ## browser diff --git a/config/sharedviteserver.md b/config/sharedviteserver.md new file mode 100644 index 00000000..ecc82891 --- /dev/null +++ b/config/sharedviteserver.md @@ -0,0 +1,74 @@ +--- +title: sharedViteServer | Config +outline: deep +--- + +# sharedViteServer + +- **Type:** `boolean` +- **Default:** `true` +- **CLI:** `--sharedViteServer=false` + +Inline [projects](/guide/projects) that don't modify the Vite config reuse the Vite server of the config that declares them. Instead of resolving a new Vite config and creating a new server for every project, such projects share the declaring config's server and its transform cache, so shared source files are transformed once instead of once per project and tests run faster. The performance improvement varies depending on the number of inline projects and how many source files they have in common. + +This option _only_ applies to inline projects. Projects referenced as config files or directories always resolve their own Vite config and create their own server. + +A project still gets its own Vite server when it defines Vite-level options that change the server (`plugins`, `resolve`, and so on), when its `extends` doesn't point to the declaring config, or when it defines test options that affect the Vite config: + +- [`alias`](/config/alias) +- [`browser`](/config/browser/enabled) +- [`css`](/config/css) +- [`deps.moduleDirectories`](/config/deps#deps-moduledirectories) +- [`deps.optimizer`](/config/deps#deps-optimizer) +- `mode` +- [`root`](/config/root) + +Options like `env`, `setupFiles`, `server.deps`, or `environment` don't prevent sharing: every project keeps its own module resolution rules, module runner, and module instances on top of the shared server. + +The same applies to Vite-level values that don't change the server: an empty `plugins` list (`plugins: isCI ? [ciPlugin()] : []`) and `define`. + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + projects: [ + // these projects share the root Vite server + { test: { name: 'unit', include: ['**/*.unit.test.ts'] } }, + { test: { name: 'integration', include: ['**/*.integration.test.ts'] } }, + // `define` doesn't create a new server, so this project also shares it + { define: { __DEV__: 'true' }, test: { name: 'dev' } }, + // this project resolves its own Vite config because of `alias` + { test: { name: 'aliased', alias: { lib: './src/lib' } } }, + ], + }, +}) +``` + +::: tip +If every project repeats the same `plugins` entry, move it to the declaring config. The projects inherit it from the shared server and keep sharing: + +```ts [vitest.config.ts] +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + // hoisted: instantiated once on the shared server + plugins: [react()], + test: { + projects: [ + { test: { name: 'unit', include: ['**/*.unit.test.ts'] } }, + { test: { name: 'integration', include: ['**/*.integration.test.ts'] } }, + ], + }, +}) +``` +::: + +To see the decision for every project, including why a project resolves its own server, run Vitest with `DEBUG=vitest:projects`. API consumers can check whether a project reuses the declaring config's server via [`project.sharedViteServer`](/api/advanced/test-project#sharedviteserver). + +The option applies to every level: inline projects of a [nested projects container](/guide/projects#nested-projects) share the container's server the same way. + +::: warning +When projects share a server, the declaring config file is executed once instead of once per project. Plugins are instantiated once, and their `config` hooks cannot observe per-project test options. If a plugin needs to behave differently per project, disable this option or don't share the server for that project (for example, set `extends: false` or define the project in its own config file). +::: diff --git a/guide/cli-generated.md b/guide/cli-generated.md index e06f45f7..b5836f69 100644 --- a/guide/cli-generated.md +++ b/guide/cli-generated.md @@ -930,6 +930,13 @@ Run only tests with the specified tags. You can use logical operators `&&` (and) Should Vitest throw an error if test has a tag that is not defined in the config. (default: `true`) +### sharedViteServer + +- **CLI:** `--sharedViteServer` +- **Config:** [sharedViteServer](/config/sharedviteserver) + +Let inline projects that don't modify the Vite config reuse the Vite server of the config that declares them. (default: `true`) + ### experimental.importDurations.print - **CLI:** `--experimental.importDurations.print ` diff --git a/guide/migration.md b/guide/migration.md index a9d238c7..82951fe7 100644 --- a/guide/migration.md +++ b/guide/migration.md @@ -157,6 +157,32 @@ Since the inherited `projects` paths resolve relative to the referenced config, Inline configurations continue to ignore the `projects` field at runtime, but it is now also excluded from their `ProjectConfig` type. +### Inline Projects Share the Vite Server by Default + +Inline projects that don't modify the Vite config now reuse the Vite server of the config that declares them instead of resolving a new Vite config and creating a new server per project. This is controlled by the new [`sharedViteServer`](/config/sharedviteserver) option, which is enabled by default. + +Sharing the server means files are transformed once instead of once per project, so tests should now run faster: a config with several inline projects over the same codebase benefits the most, while a config with a single project won't see a difference. + +Note that this _only_ applies to inline projects. Projects referenced as config files or directories always resolve their own Vite config and create their own server, exactly as before. + +An inline project also still gets its own Vite server when it defines Vite-level options that change the server (`plugins`, `resolve`, and so on), a non-default `extends`, or test options that affect the Vite config: `alias`, `browser`, `css`, `deps.moduleDirectories`, `deps.optimizer`, `mode`, or `root`. Every project keeps its own module resolution rules, module runner, and module instances, so options like `env`, `setupFiles`, `server.deps`, or `environment` still resolve per project. + +The observable change: when the server is shared, the declaring config file is executed once instead of once per project, so its plugins are instantiated once and their `config` hooks no longer run for every project. If a plugin relies on being re-instantiated per project, disable the sharing: + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + sharedViteServer: false, // [!code ++] + projects: [ + { test: { name: 'unit' } }, + { test: { name: 'integration' } }, + ], + }, +}) +``` + ### Hoisted Mocking Calls Must Be at the Top Level [`vi.mock`](/api/vi#vi-mock), [`vi.unmock`](/api/vi#vi-unmock), and [`vi.hoisted`](/api/vi#vi-hoisted) are hoisted to the top of the file and run before any surrounding code. Calling them inside a function, block, or `describe`/`test` callback previously only logged a warning. Vitest 5.0 now throws, because the call does not execute where it is written: From c6f5073ceb1c5d4253061ad9ab677a1a09b4a49a Mon Sep 17 00:00:00 2001 From: shoaibyazdani <46214812+shoaibyazdani@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:21:36 +0500 Subject: [PATCH 07/30] docs(browser): clarify special keys supported by userEvent.keyboard (closes #7118) (#10903) --- api/browser/interactivity.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/api/browser/interactivity.md b/api/browser/interactivity.md index 8b76e24e..69ddff4f 100644 --- a/api/browser/interactivity.md +++ b/api/browser/interactivity.md @@ -281,7 +281,14 @@ function keyboard(text: string): Promise The `userEvent.keyboard` allows you to trigger keyboard strokes. If any input has a focus, it will type characters into that input. Otherwise, it will trigger keyboard events on the currently focused element (`document.body` if there are no focused elements). -This API supports [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard). +This API supports [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard). Common special keys that can be referenced inside the braces include: + +- **Modifiers:** `{Shift}`, `{Control}`, `{Alt}`, `{Meta}` +- **Navigation:** `{ArrowUp}`, `{ArrowDown}`, `{ArrowLeft}`, `{ArrowRight}`, `{Home}`, `{End}`, `{PageUp}`, `{PageDown}` +- **Editing:** `{Backspace}`, `{Delete}`, `{Insert}`, `{Tab}`, `{Enter}`, `{Escape}` +- **Function keys:** `{F1}` through `{F12}` + +Note: The exact set of supported keys may vary depending on the underlying browser provider (Playwright vs WebdriverIO). If a key press doesn't trigger the expected behavior, consult your provider's documentation or file an issue. ```ts import { userEvent } from 'vitest/browser' From 7b607b010315a28f97c1b07e64e1643f5ad88cc1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:45:00 +0200 Subject: [PATCH 08/30] fix(deps): update all non-major dependencies (#10898) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Vladimir Sheremet --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d7ea8e33..5e7e018f 100644 --- a/package.json +++ b/package.json @@ -22,19 +22,19 @@ "@iconify-json/carbon": "catalog:", "@iconify-json/logos": "catalog:", "@iconify/vue": "catalog:", - "@shikijs/transformers": "^4.4.1", - "@shikijs/vitepress-twoslash": "^4.4.1", + "@shikijs/transformers": "^4.4.3", + "@shikijs/vitepress-twoslash": "^4.4.3", "@unocss/reset": "catalog:", "@vite-pwa/assets-generator": "^1.0.2", "@vite-pwa/vitepress": "^1.1.0", "@vitejs/plugin-vue": "catalog:", - "@voidzero-dev/vitepress-theme": "^4.8.4", + "@voidzero-dev/vitepress-theme": "^5.0.5", "https-localhost": "^4.7.1", "tinyglobby": "catalog:", "unocss": "catalog:", "vite": "^6.3.5", "vite-plugin-pwa": "^1.3.0", - "vitepress": "2.0.0-alpha.16", + "vitepress": "2.0.0-alpha.18", "vitepress-plugin-group-icons": "^1.7.6", "vitepress-plugin-llms": "^1.13.4", "vitepress-plugin-tabs": "^0.9.1", From 726132d9b681431853849eabe40a52cf93ad2315 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 11 Aug 2026 09:43:48 +0200 Subject: [PATCH 09/30] fix(spy)!: preserve class mock prototype methods on instances (#10910) --- api/mock.md | 4 +++ api/vi.md | 9 ++++- guide/migration.md | 26 ++++++++++++++ guide/mocking/classes.md | 77 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/api/mock.md b/api/mock.md index 8407a702..964a33fa 100644 --- a/api/mock.md +++ b/api/mock.md @@ -166,6 +166,8 @@ mockFn.mock.calls[0][0] === 0 // true mockFn.mock.calls[1][0] === 1 // true ``` +If the implementation is a class, the mock's `prototype` is re-pointed to the implementation's prototype, so constructed instances see its prototype methods and pass `instanceof` checks against it. See [Mocking Classes](/guide/mocking/classes) for details. + ## mockImplementationOnce ```ts @@ -284,6 +286,8 @@ Does what [`mockClear`](#mockClear) does and resets the mock implementation. Thi Note that resetting a mock from `vi.fn()` will set the implementation to an empty function that returns `undefined`. Resetting a mock from `vi.fn(impl)` will reset the implementation to `impl`. +The mock's `prototype` chain follows along: it reverts to the original class for `vi.fn(impl)` and `vi.spyOn()`, and to a plain object for `vi.fn()`, so instances constructed after the reset no longer pass `instanceof` checks against a previously set class implementation. + This is useful when you want to reset a mock to its original state. ```ts diff --git a/api/vi.md b/api/vi.md index 91325198..5e773eb2 100644 --- a/api/vi.md +++ b/api/vi.md @@ -473,13 +473,18 @@ You can also pass down a class to `vi.fn`: ```ts const Cart = vi.fn(class { - get = () => 0 + get() { + return 0 + } }) const cart = new Cart() expect(Cart).toHaveBeenCalled() +expect(cart.get()).toBe(0) ``` +Instances keep the prototype chain of the implementation class, so its prototype methods are available on instances, and `instanceof` checks against the implementation class pass. See [Mocking Classes](/guide/mocking/classes) for details. + ### vi.mockObject 3.2.0 ```ts @@ -614,6 +619,8 @@ const spy = vi.spyOn(cart, 'Apples') If you provide an arrow function, you will get [` is not a constructor` error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Not_a_constructor) when the mock is called. +With a class implementation, instances keep the prototype chain of that class: prototype methods like `getApples` are available on instances, and `instanceof` checks against the implementation class pass. See [Mocking Classes](/guide/mocking/classes) for details. + ::: tip In environments that support [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management), you can use `using` instead of `const` to automatically call `mockRestore` on any mocked function when the containing block is exited. This is especially useful for spied methods: diff --git a/guide/migration.md b/guide/migration.md index 82951fe7..1d80d738 100644 --- a/guide/migration.md +++ b/guide/migration.md @@ -217,6 +217,32 @@ In browser mode, mock metadata is serialized between Vitest and the test iframe. Automocks are now restored as automocks. If a browser test relied on the original implementation running through an automocked module, its exports now return `undefined` by default. Pass [`{ spy: true }`](/api/vi#vi-mock) to keep calling the real implementation while still tracking calls, or provide a factory with the behavior you need. +### Class Mocks Keep Prototype Methods + +Instances created from a class mock previously inherited from the mock's own empty `prototype`. Methods defined with the regular class syntax were `undefined` on instances, even inside the constructor, and `instanceof` checks against the implementation class failed. This affected [`vi.fn(Dog)`](/api/vi#vi-fn), `vi.spyOn(obj, 'Dog')` with or without a mock implementation, and [`.mockImplementation(class ...)`](/api/mock#mockimplementation). + +The mock's `prototype` is now chained to the implementation's prototype as soon as the implementation is set, and kept in sync when it changes, so instances behave like instances of the implementation class: + +```ts +class Dog { + speak() { + return 'bark!' + } +} + +const MockedDog = vi.fn(Dog) +const dog = new MockedDog() + +typeof dog.speak // was 'undefined', now 'function' +dog instanceof Dog // was false, now true +dog instanceof MockedDog // true, as before + +// the chain is visible on the mock itself +Object.getPrototypeOf(MockedDog.prototype) // was Object.prototype, now Dog.prototype +``` + +Overriding methods on the mock's `prototype` still works and shadows the implementation. [`mockReset`](/api/mock#mockreset) reverts the chain together with the implementation: back to the original class for `vi.fn(Dog)` and `vi.spyOn()`, and to a plain object for `vi.fn()`. See [Mocking Classes](/guide/mocking/classes) for details. + ### Benchmarking API Rewrite The benchmarking API has been rewritten. `bench` is no longer a top-level import from `vitest`; it is a [test-context fixture](/guide/test-context#bench) accessed from inside a regular `test()`. See the [Benchmarking guide](/guide/benchmarking) for the new API. diff --git a/guide/mocking/classes.md b/guide/mocking/classes.md index 09f8b8cd..a3a69002 100644 --- a/guide/mocking/classes.md +++ b/guide/mocking/classes.md @@ -27,7 +27,12 @@ class Dog { } ``` -We can re-create this class with `vi.fn` (or `vi.spyOn().mockImplementation()`): +Notice that this class defines its members in two different ways, and the difference matters for mocking: + +- `greet` is a class field. The assignment runs during construction, so every instance gets its own copy of the function as its own property. +- `speak`, `isHungry`, and `feed` are prototype methods. They are created once and stored on `Dog.prototype`, an object that all instances share. An instance doesn't have a `speak` property of its own: when you call `dog.speak()`, JavaScript doesn't find `speak` on the instance and continues looking on `Dog.prototype`. Every instance finds the same function there, so `dog.speak === Dog.prototype.speak` is `true`. + +We can re-create this class with `vi.fn` (or `vi.spyOn().mockImplementation()`). By defining every method as a class field, each instance gets its own separate mock, which allows checking calls on a single instance: ```ts const Dog = vi.fn(class { @@ -93,6 +98,7 @@ import { feed } from '../src/feed.js' const Dog = vi.fn(class { feed = vi.fn() + isHungry = vi.fn(() => false) }) test('can feed dogs', () => { @@ -124,7 +130,72 @@ expect(Max.speak).not.toHaveBeenCalled() expect(Max.greet).not.toHaveBeenCalled() ``` -We can reassign the return value for a specific instance: +You don't have to redefine every method as a class field. Instances keep the prototype chain of the class you pass to `vi.fn`, so prototype methods stay available on instances, both during and after construction, and instances pass `instanceof` checks against that class: + +```ts +class OriginalDog { + constructor(name) { + this.name = name + } + + speak() { + return 'bark!' + } +} + +const MockedDog = vi.fn(OriginalDog) +const dog = new MockedDog('Cooper') + +dog.speak() // bark! +dog instanceof MockedDog // true +dog instanceof OriginalDog // true +``` + +Note that nothing is mocked in this example. Unlike the `speak = vi.fn()` field in the `Dog` example above, the instance doesn't receive its own mock function. `dog.speak` is found through the prototype chain and refers to the original class method (`dog.speak === MockedDog.prototype.speak`), so call assertions throw: + +```ts +expect(dog.speak).toHaveBeenCalled() +// TypeError: [Function speak] is not a spy or a call to a spy! +``` + +Since every instance finds `speak` on the prototype, you can mock it for all of them at once by assigning a mock there: + +```ts +MockedDog.prototype.speak = vi.fn(() => 'woof!') + +const cooper = new MockedDog('Cooper') +const max = new MockedDog('Max') + +cooper.speak() // woof! +max.speak() // woof! + +// calls from both instances are recorded by the same mock +expect(MockedDog.prototype.speak).toHaveBeenCalledTimes(2) +// `mock.contexts` keeps the instance of every call +expect(vi.mocked(MockedDog.prototype.speak).mock.contexts).toEqual([cooper, max]) +``` + +Assigning on `MockedDog.prototype` instead of `OriginalDog.prototype` keeps the original class untouched: the lookup order is `instance` → `MockedDog.prototype` → `OriginalDog.prototype`, so the assigned function shadows the original method. Because instances look the method up on every call rather than keeping a copy, the mock is visible to all of them, even those created before the assignment. The trade-off is that they also share a single call history, unlike class fields, which give every instance its own mock. + +::: warning +The mock's `prototype` always follows the current implementation: it is re-pointed when you set a new implementation, when a queued `mockImplementationOnce` class is constructed, and when the mock is reset. If a single mock uses different class implementations, instances created by earlier implementations lose access to their prototype methods once a newer implementation takes over. Own properties assigned in the constructor or via class fields are not affected. +::: + +If you want to mock the method of one instance only, use [`vi.spyOn`](/api/vi#vi-spyon). It defines the mock directly on that instance, shadowing the prototype method just for it: + +```ts +const cooper = new MockedDog('Cooper') +const max = new MockedDog('Max') + +vi.spyOn(cooper, 'speak').mockReturnValue('meow!') + +cooper.speak() // meow! +max.speak() // bark!, still the original method + +expect(cooper.speak).toHaveBeenCalledTimes(1) +``` + +When methods are defined as class fields, like in the mocked `Dog` class at the top of this page, every instance already has its own mock, so you can reassign the return value for a specific instance directly: ```ts const dog = new Dog('Cooper') @@ -138,7 +209,7 @@ vi.mocked(dog.speak).mockReturnValue('woof woof') dog.speak() // woof woof ``` -To mock the property, we can use the `vi.spyOn(dog, 'name', 'get')` method. This makes it possible to use spy assertions on the mocked property: +To mock a non-function property, like `name`, we can use the `vi.spyOn(dog, 'name', 'get')` method. This makes it possible to use spy assertions on the mocked property: ```ts const dog = new Dog('Cooper') From fd768f03fadea85eec9226d0311255ce024a4e5f Mon Sep 17 00:00:00 2001 From: NoiseFan Date: Tue, 11 Aug 2026 19:26:34 +0800 Subject: [PATCH 10/30] docs: normalize ordered list number (#10895) --- guide/browser/visual-regression-testing.md | 4 ++-- guide/recipes/conditional-mocking.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/guide/browser/visual-regression-testing.md b/guide/browser/visual-regression-testing.md index 2156e42c..9e96e45f 100644 --- a/guide/browser/visual-regression-testing.md +++ b/guide/browser/visual-regression-testing.md @@ -177,8 +177,8 @@ Note that **screenshots for deleted or renamed tests aren't removed automaticall When a visual test fails, Vitest provides three images to help debug: 1. **Reference screenshot**: the expected baseline image -1. **Actual screenshot**: what was captured during the test -1. **Diff image**: highlights the differences; only generated when the screenshots have the same dimensions (behavior may vary with custom matchers) +2. **Actual screenshot**: what was captured during the test +3. **Diff image**: highlights the differences; only generated when the screenshots have the same dimensions (behavior may vary with custom matchers) You'll see something like this in the CLI output: diff --git a/guide/recipes/conditional-mocking.md b/guide/recipes/conditional-mocking.md index 0fb8e190..42f51415 100644 --- a/guide/recipes/conditional-mocking.md +++ b/guide/recipes/conditional-mocking.md @@ -152,8 +152,8 @@ By default, when the spy is called with arguments that match no registered behav 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). +2. [running a custom function](#onunmatched-fn); +3. [using asymmetric matchers as catch-all behaviors](#asymmetric-matcher-as-catch-all). ### `onUnmatched: 'throw'` From fe2fbfe8e432f2ee9310c10275ec9631b6bd1b13 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 12 Aug 2026 19:20:45 +0900 Subject: [PATCH 11/30] fix(snapshot): support no-unsafe-eval CSP by evaluating snapshot files on server (#10665) --- config/api.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/api.md b/config/api.md index 4d2fd461..173b93c9 100644 --- a/config/api.md +++ b/config/api.md @@ -31,5 +31,4 @@ If the host is set to anything other than `localhost` or `127.0.0.1`, Vitest wil - **Type:** `boolean` - **Default:** `true` if not exposed to the network, `false` otherwise -Allows running any test file via the UI. This applies to the interactive elements (and the server code behind them) in the [UI](/guide/ui) that can run the code. This option also gates privileged browser APIs that can execute code indirectly, such as raw Chrome DevTools Protocol access through [`cdp()`](/api/browser/context#cdp). - +Allows running any test file via the UI. This applies to the interactive elements (and the server code behind them) in the [UI](/guide/ui) that can run the code. In Browser Mode, this option also gates indirect code execution, including evaluating external `.snap` files on the server and accessing raw Chrome DevTools Protocol through [`cdp()`](/api/browser/context#cdp). From fedaf1b9af8d255c78c6eb87e438a932a933fb98 Mon Sep 17 00:00:00 2001 From: "jonbray.eth" Date: Wed, 12 Aug 2026 12:33:02 -0400 Subject: [PATCH 12/30] feat(reporter): add custom GitHub Actions summary title (#10891) Co-authored-by: Raul Macarie --- guide/reporters.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/guide/reporters.md b/guide/reporters.md index a57593ab..11f61bc6 100644 --- a/guide/reporters.md +++ b/guide/reporters.md @@ -741,6 +741,22 @@ export default defineConfig({ }) ``` +The job summary title defaults to `Vitest Test Report`. You can use `jobSummary.title` to distinguish multiple Vitest invocations that append to the same job summary. + +```ts +export default defineConfig({ + test: { + reporters: [ + ['github-actions', { + jobSummary: { + title: 'My Test Report', + }, + }], + ], + }, +}) +``` + The flaky tests section of the summary includes permalink URLs that link test names directly to the relevant source lines on GitHub. These links are generated automatically using environment variables that GitHub Actions provides (`$GITHUB_REPOSITORY`, `$GITHUB_SHA`, and `$GITHUB_WORKSPACE`), so no configuration is needed in most cases. If you need to override these values — for example, when running in a container or a custom environment — you can customize them via the `fileLinks` option: From d3697b61c1e943ef0444f109046433a810e3e2f8 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 13 Aug 2026 01:22:18 +0200 Subject: [PATCH 13/30] docs: update migration guide (#10930) --- config/coverage.md | 4 +- config/faketimers.md | 2 + guide/migration.md | 116 +++++++++++++++++++++---------------------- 3 files changed, 61 insertions(+), 61 deletions(-) diff --git a/config/coverage.md b/config/coverage.md index 409d57fd..9fc8aa6c 100644 --- a/config/coverage.md +++ b/config/coverage.md @@ -45,6 +45,8 @@ List of files included in coverage as glob patterns. By default only files cover It is recommended to pass file extensions in the pattern. +Patterns are matched against each file's path relative to the project root. A pattern with no glob wildcard is treated as a directory and matches everything inside it, so `include: ['src']` is equivalent to `include: ['src/**']`. + See [Including and excluding files from coverage report](/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples. ## coverage.exclude @@ -54,7 +56,7 @@ See [Including and excluding files from coverage report](/guide/coverage.html#in - **Available for providers:** `'v8' | 'istanbul'` - **CLI:** `--coverage.exclude=`, `--coverage.exclude= --coverage.exclude=` -List of files excluded from coverage as glob patterns. +List of files excluded from coverage as glob patterns. Patterns are matched the same way as [`coverage.include`](#coverage-include). See [Including and excluding files from coverage report](/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples. diff --git a/config/faketimers.md b/config/faketimers.md index eedae1ee..679c967b 100644 --- a/config/faketimers.md +++ b/config/faketimers.md @@ -23,6 +23,8 @@ Installs fake timers with the specified Unix epoch. An array with names of global methods and APIs to fake. For example, to only mock `setTimeout()` and `nextTick()`, specify this property as `['setTimeout', 'nextTick']`. +`Temporal` is only faked when it is available on the global object: natively (Node.js >= 26 by default, behind `--harmony-temporal` on older versions, and supporting browsers) or through a globally installed polyfill such as `import 'temporal-polyfill/global'`. + Mocking `nextTick` is not supported when running Vitest inside `node:child_process` by using `--pool=forks`. NodeJS uses `process.nextTick` internally in `node:child_process` and hangs when it is mocked. Mocking `nextTick` is supported when running Vitest with `--pool=threads`. ## fakeTimers.toNotFake diff --git a/guide/migration.md b/guide/migration.md index 1d80d738..46b5d1f2 100644 --- a/guide/migration.md +++ b/guide/migration.md @@ -9,17 +9,13 @@ outline: deep ## Migrating to Vitest 5.0 {#vitest-5} -::: warning Work in progress -Vitest 5.0 is currently in beta. This section tracks breaking changes as they are merged and may change before the stable release. -::: - ::: warning Prerequisites Vitest 5.0 requires Vite >= 6.4.0 and Node.js >= 22.12.0. Before proceeding with any other migration steps, ensure your environment meets these requirements. Running Vitest 5.0 on older versions of Vite or Node.js is not supported and may result in unexpected errors. ::: ### `clearMocks` is Enabled by Default -[`clearMocks`](/config/#clearmocks) now defaults to `true`. Vitest calls [`vi.clearAllMocks()`](/api/vi#vi-clearallmocks) before every test, resetting the `mock.calls`, `mock.instances`, `mock.contexts` and `mock.results` of every mock. Mock implementations are left intact, so this only affects the recorded history. +[`clearMocks`](/config/clearmocks) now defaults to `true`: Vitest calls [`vi.clearAllMocks()`](/api/vi#vi-clearallmocks) before every test, clearing the recorded history of every mock while leaving implementations intact. In practice this means a mock no longer carries calls from one test into the next: @@ -60,7 +56,7 @@ export default defineConfig({ [`testNamePattern`](/config/testnamepattern) (the `-t` CLI flag) now matches against the test's full name with the suite chain and test name joined by `' > '`, the same string shown in the reporter output. Previously the segments were joined with a single space, mirroring Jest. -This only affects patterns that span the boundary between a suite and a test (or between nested suites). Patterns that match within a single name segment, and patterns that use `.`/`.*` between segments, are unaffected. +Only patterns that span the boundary between two segments are affected: ```ts describe('math', () => { @@ -100,15 +96,9 @@ export default defineConfig({ }) ``` -A few options are excluded because they are always scoped to a single project or to the whole test run: - -- `name` and `projects` are never inherited. -- `globalSetup` is not inherited from the root config: the root-level `globalSetup` already runs once per test run, so inheriting it would run the same files again for every project. It is still inherited when extending a non-root config file. -- The project's own `tags` replace the inherited array instead of being merged with it. - Projects referenced as config files or directories are not affected; they still don't inherit any options from the root config. -Keep in mind that arrays are merged, not overridden. For example, if the root config defines `setupFiles`, the project's own `setupFiles` are appended to the inherited ones. If you need the previous behavior, set `extends: false` in the project configuration: +Keep in mind that arrays are merged, not overridden: if the root config defines `setupFiles`, the project's own `setupFiles` are appended to the inherited ones. See [the projects guide](/guide/projects#configuration) for the merge rules and the few options that are never inherited. If you need the previous behavior, set `extends: false` in the project configuration: ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -131,9 +121,9 @@ export default defineConfig({ ### Referenced Config Files Can Define Their Own Projects -A config file referenced in [`test.projects`](/guide/projects) that declares `projects` itself is now treated like the root config: it doesn't run tests on its own, it only provides the [nested projects](/guide/projects#nested-projects) it declares. Their names are prefixed with the name of the declaring config, e.g. `app (unit)`. +A config file referenced in [`test.projects`](/guide/projects) that declares `projects` itself now provides the [nested projects](/guide/projects#nested-projects) it declares (named `app (unit)`, `app (e2e)`, and so on) instead of running tests as a single project. -In Vitest 4 the `projects` field of a referenced config was silently ignored and the config ran as a single project. Check that your project configs don't carry a `projects` field unknowingly. The most common way to do that is merging a config that defines it: +In Vitest 4 the `projects` field of a referenced config was silently ignored. Check that your project configs don't carry a `projects` field unknowingly. The most common way to do that is merging a config that defines it: ```ts [packages/app/vitest.config.ts] import { defineProject, mergeConfig } from 'vitest/config' @@ -159,14 +149,10 @@ Inline configurations continue to ignore the `projects` field at runtime, but it ### Inline Projects Share the Vite Server by Default -Inline projects that don't modify the Vite config now reuse the Vite server of the config that declares them instead of resolving a new Vite config and creating a new server per project. This is controlled by the new [`sharedViteServer`](/config/sharedviteserver) option, which is enabled by default. - -Sharing the server means files are transformed once instead of once per project, so tests should now run faster: a config with several inline projects over the same codebase benefits the most, while a config with a single project won't see a difference. +Inline projects that don't modify the Vite config now reuse the Vite server of the config that declares them instead of resolving a new Vite config and creating a new server per project, so shared files are transformed once and tests run faster. This is controlled by the new [`sharedViteServer`](/config/sharedviteserver) option, which is enabled by default; its documentation lists the exact options that still give a project its own server. Note that this _only_ applies to inline projects. Projects referenced as config files or directories always resolve their own Vite config and create their own server, exactly as before. -An inline project also still gets its own Vite server when it defines Vite-level options that change the server (`plugins`, `resolve`, and so on), a non-default `extends`, or test options that affect the Vite config: `alias`, `browser`, `css`, `deps.moduleDirectories`, `deps.optimizer`, `mode`, or `root`. Every project keeps its own module resolution rules, module runner, and module instances, so options like `env`, `setupFiles`, `server.deps`, or `environment` still resolve per project. - The observable change: when the server is shared, the declaring config file is executed once instead of once per project, so its plugins are instantiated once and their `config` hooks no longer run for every project. If a plugin relies on being re-instantiated per project, disable the sharing: ```ts [vitest.config.ts] @@ -213,15 +199,13 @@ The dynamic variants [`vi.doMock`](/api/vi#vi-domock) and [`vi.doUnmock`](/api/v ### Automocked Modules Stay Automocked in the Browser -In browser mode, mock metadata is serialized between Vitest and the test iframe. An automocked module (a [`vi.mock`](/api/vi#vi-mock) call with no factory) was incorrectly restored as a spy on the other side, so its exports kept calling the real implementation instead of the auto-generated stubs. - -Automocks are now restored as automocks. If a browser test relied on the original implementation running through an automocked module, its exports now return `undefined` by default. Pass [`{ spy: true }`](/api/vi#vi-mock) to keep calling the real implementation while still tracking calls, or provide a factory with the behavior you need. +In browser mode, the exports of an automocked module (a [`vi.mock`](/api/vi#vi-mock) call with no factory) incorrectly kept calling the real implementation instead of the auto-generated stubs. If a browser test relied on that, its exports now return `undefined` by default. Pass [`{ spy: true }`](/api/vi#vi-mock) to keep calling the real implementation while still tracking calls, or provide a factory with the behavior you need. ### Class Mocks Keep Prototype Methods Instances created from a class mock previously inherited from the mock's own empty `prototype`. Methods defined with the regular class syntax were `undefined` on instances, even inside the constructor, and `instanceof` checks against the implementation class failed. This affected [`vi.fn(Dog)`](/api/vi#vi-fn), `vi.spyOn(obj, 'Dog')` with or without a mock implementation, and [`.mockImplementation(class ...)`](/api/mock#mockimplementation). -The mock's `prototype` is now chained to the implementation's prototype as soon as the implementation is set, and kept in sync when it changes, so instances behave like instances of the implementation class: +The mock's `prototype` is now chained to the implementation's prototype, so instances behave like instances of the implementation class: ```ts class Dog { @@ -241,7 +225,7 @@ dog instanceof MockedDog // true, as before Object.getPrototypeOf(MockedDog.prototype) // was Object.prototype, now Dog.prototype ``` -Overriding methods on the mock's `prototype` still works and shadows the implementation. [`mockReset`](/api/mock#mockreset) reverts the chain together with the implementation: back to the original class for `vi.fn(Dog)` and `vi.spyOn()`, and to a plain object for `vi.fn()`. See [Mocking Classes](/guide/mocking/classes) for details. +Overriding methods on the mock's `prototype` still works and shadows the implementation, and [`mockReset`](/api/mock#mockreset) reverts the chain together with the implementation. See [Mocking Classes](/guide/mocking/classes) for details. ### Benchmarking API Rewrite @@ -282,9 +266,9 @@ vitest --ui # UI started at http://localhost:51204/__vitest__/?token=... ``` -### Fake Timers Now Mock `Temporal` +### Fake Timers and `setSystemTime` Now Mock `Temporal` -Vitest now mocks the [`Temporal`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal) API alongside `Date` when fake timers are enabled, following the [`@sinonjs/fake-timers` v15.4 update](https://github.com/sinonjs/fake-timers/blob/main/CHANGELOG.md#1540--2026-05-05). This only takes effect when `Temporal` is available on the global object — either natively (Node.js >= 26 by default, behind `--harmony-temporal` on older versions, and supporting browsers) or through a globally installed polyfill such as `import 'temporal-polyfill/global'`. +Vitest now mocks the [`Temporal`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal) API alongside `Date`, following the [`@sinonjs/fake-timers` v15.4 update](https://github.com/sinonjs/fake-timers/blob/main/CHANGELOG.md#1540--2026-05-05). This only takes effect when `Temporal` is available on the global object, natively or through a globally installed polyfill such as `import 'temporal-polyfill/global'`. Previously `Temporal.Now` kept returning the real wall-clock time even when [`vi.useFakeTimers()`](/api/vi#vi-usefaketimers) was active. Now it follows the mocked clock: @@ -294,19 +278,17 @@ vi.useFakeTimers({ now: 0 }) Temporal.Now.instant().epochMilliseconds // 0 (was the real time in v4) ``` -`Temporal` is part of the default set of faked APIs, so it is controlled by [`fakeTimers.toFake`](/config/#faketimers-tofake) and [`fakeTimers.toNotFake`](/config/#faketimers-tonotfake). To keep `Temporal` native, add it to `toNotFake`: +The same applies to [`vi.setSystemTime()`](/api/vi#vi-setsystemtime), which previously mocked only `Date` when used without fake timers: ```ts -vi.useFakeTimers({ toNotFake: ['Temporal'] }) +vi.setSystemTime(0) +Temporal.Now.instant().epochMilliseconds // 0 (was the real time in v4) ``` -### `setSystemTime` Now Mocks Temporal - -Previously `vi.setSystemTime` mocked only `Date` without fake timers, but now it also mocks methods of `Temporal.Now`. +`Temporal` is part of the default set of faked APIs, so it is controlled by [`fakeTimers.toFake`](/config/#faketimers-tofake) and [`fakeTimers.toNotFake`](/config/#faketimers-tonotfake). To keep `Temporal` native, add it to `toNotFake`: ```ts -vi.setSystemTime(0) -Temporal.Now.instant().epochMilliseconds // 0 (was the real time in v4) +vi.useFakeTimers({ toNotFake: ['Temporal'] }) ``` ### `toThrow("")` Matches Any Error Message @@ -328,28 +310,7 @@ expect(() => { throw new Error('boom') }).not.toThrow(/^$/) Assertion interfaces now use two type parameters: `R` is the matcher return type and `T` is the received value type. Synchronous assertions use `void`, while assertions accessed through `.resolves`, `.rejects`, [`expect.poll`](/api/expect#poll), or [`expect.element`](/api/browser/assertions) use `Promise`. -If you declare custom matchers, augment the `Matchers` interface. It adds the matcher to instance assertions, asymmetric matchers, and the type accepted by `expect.extend`: - -```ts [vitest.d.ts] -import 'vitest' - -interface CustomMatchers { - toBeFoo: () => R - toEqualTyped: (expected: T) => R -} - -declare module 'vitest' { - interface Matchers extends CustomMatchers {} -} -``` - -This makes custom matcher return types reflect how the matcher is used: - -```ts -const syncResult = expect('value').toEqualTyped('other') // void -const asyncResult = expect(Promise.resolve('value')).resolves.toEqualTyped('other') // Promise -await asyncResult -``` +If you declare custom matchers, augment the `Matchers` interface as shown in [Extending Matchers](/guide/extending-matchers). It adds the matcher to instance assertions, asymmetric matchers, and the type accepted by `expect.extend`, and the matcher's return type reflects how it is used: `void` when called synchronously, `Promise` through `.resolves` or `.rejects`. Code that refers to assertion types directly must also provide the return type first: @@ -513,9 +474,7 @@ export default defineConfig({ ### Coverage `include` and `exclude` Match More Precisely -`coverage.include` and `coverage.exclude` were matched against absolute paths with picomatch's `contains` option, which matched many more files than intended. For example, a pattern could match a file because a parent directory in its absolute path happened to contain the same segment. Patterns are now matched against each file's path relative to the project root, without `contains`. - -A pattern with no glob wildcard is treated as a directory and expanded to match everything inside it: +[`coverage.include`](/config/coverage#coverage-include) and `coverage.exclude` were matched against absolute paths with picomatch's `contains` option, which matched many more files than intended. Patterns are now matched against each file's path relative to the project root, without `contains`, and a pattern with no glob wildcard is treated as a directory that matches everything inside it: ```ts [vitest.config.ts] export default defineConfig({ @@ -559,17 +518,38 @@ 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. +### `browser.api` Is Replaced by the Top-Level `api` + +Browser mode now runs on a single Vite server configured by the top-level [`api`](/config/api) option; the default port in browser mode is still `63315`. The `browser.api` option is deprecated and no longer has any effect, so move its value to `api`: + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + api: { port: 4444 }, // [!code ++] + browser: { + enabled: true, + api: { port: 4444 }, // [!code --] + }, + }, +}) +``` + +The already deprecated `browser.isolate` option now also prints a warning at startup; its value is still applied to the top-level [`isolate`](/config/isolate) option that replaces it. + ### 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/` +- **Failure screenshots** ([`screenshotFailures`](/config/browser/screenshotfailures)): `__screenshots__/` → `.vitest/attachments/failure-screenshots/`, so they are no longer mixed with the reference screenshots of `toMatchScreenshot` - **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) - **JSON reporter** ([`json`](/guide/reporters#json-reporter)): stdout → `.vitest/json/output.json` - **JUnit reporter** ([`junit`](/guide/reporters#junit-reporter)): stdout → `.vitest/junit/output.xml` -The `json` and `junit` reporters now write to a file by default instead of printing to stdout. If you previously relied on the report being printed to stdout (for example `vitest --reporter=json > out.json` or `vitest --reporter=json | jq`), either read the generated artifact file instead (for example `jq . .vitest/json/output.json`), or opt back into stdout with the reporter's `stdout` option (`reporters: [['json', { stdout: true }]]`). An explicit `outputFile` is still respected and unchanged. +The `json` and `junit` reporters now write to a file by default instead of printing to stdout. If you piped the report (for example `vitest --reporter=json | jq`), read the artifact file instead, or opt back into stdout with the reporter's [`stdout` option](/guide/reporters#reporter-output) (`reporters: [['json', { stdout: true }]]`). An explicit `outputFile` is still respected and unchanged. ### `toMatchScreenshot` Now Uses a Dedicated Screenshot Directory Config @@ -615,6 +595,20 @@ class MyReporter implements Reporter { Node.js and browser tests run in separate pools and do not share these ids, so the same value can appear in both. +### `resolveConfig` Returns the Resolved Vite Config + +The [`resolveConfig`](/guide/advanced/#resolveconfig) helper from `vitest/node` no longer returns a `{ vitestConfig, viteConfig }` pair. It resolves the config without creating a Vite server and returns the resolved Vite config; the fully resolved Vitest config is available on its `test` property: + +```ts +import { resolveConfig } from 'vitest/node' + +const { viteConfig, vitestConfig } = await resolveConfig(options) // [!code --] +const viteConfig = await resolveConfig(options) // [!code ++] +const vitestConfig = viteConfig.test // [!code ++] +``` + +The Vitest 4 limitations are gone: the returned config now includes fully resolved `projects`, and `viteConfig.test` no longer holds partially resolved options. + ### Package Migration The following packages are deprecated as of this release. They will no longer receive feature updates, but security fixes will continue to be backported: @@ -622,6 +616,8 @@ The following packages are deprecated as of this release. They will no longer re - [`@vitest/runner`](https://npmx.dev/package/@vitest/runner) - [`@vitest/ws-client`](https://npmx.dev/package/@vitest/ws-client) +`vitest` also no longer depends on [`@vitest/expect`](https://npmx.dev/package/@vitest/expect): the assertion code is bundled into `vitest` itself. The package is still published and usable on its own, but it no longer shares state with Vitest's `expect`. Interact with Vitest's assertions through the `vitest` entry point (`expect`, `expect.extend`, `chai`) instead. + The [`@vitest/browser-webdriverio`](https://npmx.dev/package/@vitest/browser-webdriverio) provider has been moved to the [vitest-community](https://github.com/vitest-community/vitest-webdriverio) organization. Going forward, WebdriverIO support is community-maintained and addressed on a per-issue basis. If you use it, update your dependency to the new package and report any issues in the new repository. ### Removed Deprecated Entrypoints From 79532d4b7dee5f3736b019f052b410d8e9f17f89 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 14 Aug 2026 14:09:30 +0200 Subject: [PATCH 14/30] refactor(projects): cleanup comments, add more `debug` statements (#10953) --- guide/projects.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/guide/projects.md b/guide/projects.md index 2aff34bf..b3b96b2e 100644 --- a/guide/projects.md +++ b/guide/projects.md @@ -391,3 +391,22 @@ export default defineProject({ ``` Note that only config files can define nested projects. The `projects` option inside an inline configuration is not supported. + +## Debugging Project Resolution + +If projects are not resolved the way you expect, run Vitest with the `DEBUG=vitest:projects` environment variable: + +```bash +DEBUG=vitest:projects vitest +``` + +Vitest will log how every project was resolved: which files a glob pattern matched, how browser instances and benchmark projects were expanded, why a project was dropped by the `--project` filter, and whether a project creates its own Vite server or [shares one](/config/sharedviteserver) with another project: + +``` +vitest:projects resolving 3 project definitions declared by /vitest.config.ts +vitest:projects projects glob "packages/*" matched 2 paths +vitest:projects inline project "unit" shares the Vite server of /vitest.config.ts +vitest:projects project "e2e" is dropped by the --project filter: unit +vitest:projects resolved projects: "unit", "pkg-a", "pkg-b" +vitest:projects creating a Vite server for project "pkg-a" +``` From 4511691bc85b6e6075dd5eeb542ceb121d66bf40 Mon Sep 17 00:00:00 2001 From: Arjun Date: Fri, 14 Aug 2026 05:11:36 -0700 Subject: [PATCH 15/30] fix(browser): disallow filter-only options in getBy* locators (fix #10295) (#10933) --- api/browser/locators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/browser/locators.md b/api/browser/locators.md index 363a062c..357b357d 100644 --- a/api/browser/locators.md +++ b/api/browser/locators.md @@ -515,7 +515,7 @@ page.getByRole('button') ## filter ```ts -function filter(options: LocatorOptions): Locator +function filter(options: LocatorFilterOptions): Locator ``` This methods narrows down the locator according to the options, such as filtering by text. It can be chained to apply multiple filters. From 2ded62f93cb366267a7e24cd78e2a0382167f06b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:51:33 +0200 Subject: [PATCH 16/30] fix(deps): update all non-major dependencies (#10966) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5e7e018f..0069e165 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,9 @@ "unocss": "catalog:", "vite": "^6.3.5", "vite-plugin-pwa": "^1.3.0", - "vitepress": "2.0.0-alpha.18", + "vitepress": "2.0.0-alpha.19", "vitepress-plugin-group-icons": "^1.7.6", - "vitepress-plugin-llms": "^1.13.4", + "vitepress-plugin-llms": "^1.13.5", "vitepress-plugin-tabs": "^0.9.1", "workbox-window": "^7.4.1" } From 5d198e27f2aa7c9307351753bc609395859d50bd Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 17 Aug 2026 11:03:11 +0200 Subject: [PATCH 17/30] chore: update agents file (#10952) --- .vitepress/config.ts | 1 + AGENTS.md | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 AGENTS.md diff --git a/.vitepress/config.ts b/.vitepress/config.ts index dcb9f30b..47e7d9f6 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -35,6 +35,7 @@ export default ({ mode }: { mode: string }) => { srcExclude: [ '**/guide/examples/*', '**/guide/cli-generated.md', + 'AGENTS.md', ], locales: { root: { diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..fc4d8310 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,26 @@ +# Vitest Docs Guide + +Guidance for working on the VitePress site in `docs/`. The root `AGENTS.md` applies here too. + +## Building + +- Local dev server: `pnpm docs` (from the repo root) +- Build with `pnpm docs:build` from the repo root; it sets the required env vars and regenerates the CLI table first +- The build type-checks code samples against the built workspace packages, so run `pnpm build` at the repo root first + +## What fails the build + +- Code blocks tagged `ts twoslash` are type-checked during `pnpm docs:build` only; the dev server skips twoslash and will not catch broken samples. A sample that intentionally fails needs `// @errors: `; use `// ---cut---` and `// @filename:` to hide setup lines. Changing public types in `packages/` can break twoslash samples. +- Dead internal links fail the build. Every CLI option gets a generated link to `/config/