From 903e593665c9520edb5eb37908212571d872c018 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 13 Aug 2026 01:22:18 +0200 Subject: [PATCH] docs: update migration guide (#10930) --- docs/config/coverage.md | 4 +- docs/config/faketimers.md | 2 + docs/guide/migration.md | 116 ++++++++++++++++++-------------------- 3 files changed, 61 insertions(+), 61 deletions(-) diff --git a/docs/config/coverage.md b/docs/config/coverage.md index 409d57fd5..9fc8aa6c3 100644 --- a/docs/config/coverage.md +++ b/docs/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/docs/config/faketimers.md b/docs/config/faketimers.md index eedae1eec..679c967b7 100644 --- a/docs/config/faketimers.md +++ b/docs/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/docs/guide/migration.md b/docs/guide/migration.md index 1d80d7388..46b5d1f26 100644 --- a/docs/guide/migration.md +++ b/docs/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 -- 2.51.2