---
url: /config.md
---
# Configuring Vitest
If you are using Vite and have a `vite.config` file, Vitest will read it to match with the plugins and setup as your Vite app. If you want to have a different configuration for testing or your main app doesn't rely on Vite specifically, you could either:
* Create `vitest.config.ts`, which will have the higher priority and will **override** the configuration from `vite.config.ts` (Vitest supports all conventional JS and TS extensions, but doesn't support `json`) - it means all options in your `vite.config` will be **ignored**
* Pass `--config` option to CLI, e.g. `vitest --config ./path/to/vitest.config.ts`
* Use `process.env.VITEST` or `mode` property on `defineConfig` (will be set to `test` if not overridden with `--mode`) to conditionally apply different configuration in `vite.config.ts`. Note that like any other environment variable, `VITEST` is also exposed on `import.meta.env` in your tests
When an explicit `--config` option is not provided, Vitest looks for `vitest.config.{ts,mts,cts,js,mjs,cjs}` first and `vite.config.{ts,mts,cts,js,mjs,cjs}` second in the project [`root`](/config/root). If no config file is found, Vitest will run without one.
To configure `vitest` itself, add `test` property in your Vite config. You'll also need to add a reference to Vitest types using a [triple slash command](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html#-reference-types-) at the top of your config file, if you are importing `defineConfig` from `vite` itself.
If you are not using `vite`, add `defineConfig` imported from `vitest/config` to your config file:
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// ... Specify options here.
},
})
```
If you have a `vite` config already, you can add `/// ` to include the `test` types:
```js [vite.config.js]
///
import { defineConfig } from 'vite'
export default defineConfig({
test: {
// ... Specify options here.
},
})
```
You can retrieve Vitest's default options to expand them if needed:
```js [vitest.config.js]
import { configDefaults, defineConfig } from 'vitest/config'
export default defineConfig({
test: {
exclude: [...configDefaults.exclude, 'packages/template/*'],
},
})
```
When using a separate `vitest.config.js`, you can also extend Vite's options from another config file if needed:
```js [vitest.config.js]
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(viteConfig, defineConfig({
test: {
exclude: ['packages/template/*'],
},
}))
```
If your Vite config is defined as a function, you can define the config like this:
```js [vitest.config.js]
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default defineConfig(configEnv => mergeConfig(
viteConfig(configEnv),
defineConfig({
test: {
exclude: ['packages/template/*'],
},
})
))
```
Since Vitest uses Vite config, you can also use any configuration option from [Vite](https://vitejs.dev/config/). For example, `define` to define global variables, or `resolve.alias` to define aliases - these options should be defined on the top level, *not* within a `test` property.
## Automatic Dependency Installation
Vitest will prompt you to install certain dependencies if they are not already installed. You can disable this behavior by setting the `VITEST_SKIP_INSTALL_CHECKS=1` environment variable.
## Config Options
Configuration options that are not supported inside a [project](/guide/projects) config have icon next to them. This means they can only be set in the root Vitest config.
---
---
url: /config/include.md
---
# include
* **Type:** `string[]`
* **Default:** `['**/*.{test,spec}.?(c|m)[jt]s?(x)']`
* **CLI:** `vitest [...include]`, `vitest **/*.test.js`
A list of [glob patterns](https://superchupu.dev/tinyglobby/comparison) that match your test files. These patterns are resolved relative to the [`root`](/config/root) ([`process.cwd()`](https://nodejs.org/api/process.html#processcwd) by default).
Vitest uses the [`tinyglobby`](https://npmx.dev/package/tinyglobby) package to resolve the globs.
::: tip NOTE
When using coverage, Vitest automatically adds test files `include` patterns to coverage's default `exclude` patterns. See [`coverage.exclude`](/config/coverage#exclude).
:::
## Example
```js
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: [
'./test',
'./**/*.{test,spec}.ts(x)?',
],
},
})
```
Vitest provides reasonable defaults, so normally you wouldn't override them. A good example of defining `include` is for [test projects](/guide/projects):
```js{8,12} [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
test: {
name: 'unit',
include: ['./test/unit/*.test.js'],
},
},
{
test: {
name: 'e2e',
include: ['./test/e2e/*.test.js'],
},
},
],
},
})
```
::: warning
This option will override Vitest defaults. If you just want to extend them, use `configDefaults` from `vitest/config`:
```js{6}
import { configDefaults, defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: [
...configDefaults.include,
'./test',
'./**/*.{test,spec}.ts(x)?',
],
},
})
```
:::
---
---
url: /config/exclude.md
---
# exclude
* **Type:** `string[]`
* **Default:** `['**/node_modules/**', '**/.git/**']`
* **CLI:** `vitest --exclude "**/excluded-file" --exclude "*/other-files/*.js"`
A list of [glob patterns](https://superchupu.dev/tinyglobby/comparison) that should be excluded from your test files. These patterns are resolved relative to the [`root`](/config/root) ([`process.cwd()`](https://nodejs.org/api/process.html#processcwd) by default).
Vitest uses the [`tinyglobby`](https://npmx.dev/package/tinyglobby) package to resolve the globs.
::: warning
This option does not affect coverage. If you need to remove certain files from the coverage report, use [`coverage.exclude`](/config/coverage#exclude).
This is the only option that doesn't override your configuration if you provide it with a CLI flag. All glob patterns added via `--exclude` flag will be added to the config's `exclude`.
:::
## Example
```js
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
exclude: [
'**/node_modules/**',
'**/dist/**',
'./temp/**',
],
},
})
```
::: tip
Although the CLI `exclude` option is additive, manually setting `exclude` in your config will replace the default value. To extend the default `exclude` patterns, use `configDefaults` from `vitest/config`:
```js{6}
import { configDefaults, defineConfig } from 'vitest/config'
export default defineConfig({
test: {
exclude: [
...configDefaults.exclude,
'packages/template/*',
'./temp/**',
],
},
})
```
:::
---
---
url: /config/include-source.md
---
# includeSource
* **Type:** `string[]`
* **Default:** `[]`
A list of [glob patterns](https://superchupu.dev/tinyglobby/comparison) that match your [in-source test files](/guide/in-source). These patterns are resolved relative to the [`root`](/config/root) ([`process.cwd()`](https://nodejs.org/api/process.html#processcwd) by default).
When defined, Vitest will run all matched files that have `import.meta.vitest` inside.
::: warning
Vitest performs a simple text-based inclusion check on source files. If a file contains `import.meta.vitest`, even in a comment, it will be matched as an in-source test file.
:::
Vitest uses the [`tinyglobby`](https://npmx.dev/package/tinyglobby) package to resolve the globs.
## Example
```js
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
includeSource: ['src/**/*.{js,ts}'],
},
})
```
Then you can write tests inside your source files:
```ts [src/index.ts]
export function add(...args: number[]) {
return args.reduce((a, b) => a + b, 0)
}
// #region in-source test suites
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('add', () => {
expect(add()).toBe(0)
expect(add(1)).toBe(1)
expect(add(1, 2, 3)).toBe(6)
})
}
// #endregion
```
For your production build, you need to replace the `import.meta.vitest` with `undefined`, letting the bundler do the dead code elimination.
::: code-group
```js [vite.config.ts]
import { defineConfig } from 'vite'
export default defineConfig({
define: { // [!code ++]
'import.meta.vitest': 'undefined', // [!code ++]
}, // [!code ++]
})
```
```js [rolldown.config.js]
import { defineConfig } from 'rolldown/config'
export default defineConfig({
transform: {
define: { // [!code ++]
'import.meta.vitest': 'undefined', // [!code ++]
}, // [!code ++]
},
})
```
```js [rollup.config.js]
import replace from '@rollup/plugin-replace' // [!code ++]
export default {
plugins: [
replace({ // [!code ++]
'import.meta.vitest': 'undefined', // [!code ++]
}) // [!code ++]
],
// other options
}
```
```js [build.config.js]
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
replace: { // [!code ++]
'import.meta.vitest': 'undefined', // [!code ++]
}, // [!code ++]
// other options
})
```
```js [webpack.config.js]
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.DefinePlugin({ // [!code ++]
'import.meta.vitest': 'undefined', // [!code ++]
})// [!code ++]
],
}
```
:::
::: tip
To get TypeScript support for `import.meta.vitest`, add `vitest/importMeta` to your `tsconfig.json`:
```json [tsconfig.json]
{
"compilerOptions": {
"types": ["vitest/importMeta"]
}
}
```
:::
---
---
url: /config/name.md
---
# name
* **Type:**
```ts
interface UserConfig {
name?: string | { label: string; color?: LabelColor }
}
```
Assign a custom name to the test project or Vitest process. The name will be visible in the CLI and UI, and available in the Node.js API via [`project.name`](/api/advanced/test-project#name).
The color used by the CLI and UI can be changed by providing an object with a `color` property.
## Colors
The displayed colors depend on your terminal’s color scheme. In the UI, colors match their CSS equivalents.
* black
* red
* green
* yellow
* blue
* magenta
* cyan
* white
## Example
::: code-group
```js [string]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
name: 'unit',
},
})
```
```js [object]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
name: {
label: 'unit',
color: 'blue',
},
},
})
```
:::
This property is mostly useful if you have several projects as it helps distinguish them in your terminal:
```js{7,11} [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
name: 'unit',
include: ['./test/*.unit.test.js'],
},
{
name: 'e2e',
include: ['./test/*.e2e.test.js'],
},
],
},
})
```
::: tip
Vitest automatically assigns a name when none is provided. Resolution order:
* If the project is specified by a config file or directory, Vitest uses the package.json's `name` field.
* If there is no `package.json`, Vitest falls back to the project folder's basename.
* If the project is defined inline in the `projects` array (an object), Vitest assigns a numeric name equal to that project's array index (0-based).
:::
::: warning
Note that projects cannot have the same name. Vitest will throw an error during the config resolution.
:::
You can also assign different names to different browser [instances](/config/browser/instances):
```js{10,11} [vitest.config.js]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium', name: 'Chrome' },
{ browser: 'firefox', name: 'Firefox' },
],
},
},
})
```
::: tip
Browser instances inherit their parent project's name with the browser name appended in parentheses. For example, a project named `browser` with a chromium instance will be shown as `browser (chromium)`.
If the parent project has no name, or instances are defined at the root level (not inside a named project), the instance name defaults to the browser value (e.g. `chromium`). To override this behavior, set an explicit `name` on the instance.
:::
---
---
url: /config/server.md
---
# server
Before Vitest 4, this option was used to define the configuration for the `vite-node` server.
At the moment, this option allows you to configure the inlining and externalization mechanisms, along with the module runner debugging configuration.
::: warning
These options should be used only as the last resort to improve performance by externalizing auto-inlined dependencies or to fix issues by inlining invalid external dependencies.
Normally, Vitest should do this automatically.
:::
## server.deps
### server.deps.external
* **Type:** `(string | RegExp)[]`
* **Default:** files inside [`moduleDirectories`](/config/deps#moduledirectories)
Specifies modules that should not be transformed by Vite and should instead be processed directly by the engine. These modules are imported via native dynamic `import` and bypass both transformation and resolution phases.
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
server: {
deps: {
external: ['react'],
},
},
},
})
```
External modules and their dependencies are not present in the module graph and will not trigger test restarts when they change.
Typically, packages under `node_modules` are externalized.
::: tip
If a string is provided, it is first normalized by prefixing the `/node_modules/` or other [`moduleDirectories`](/config/deps#moduledirectories) segments (for example, `'react'` becomes `/node_modules/react/`), and the resulting string is then matched against the full file path. For example, package `@company/some-name` located inside `packages/some-name` should be specified as `some-name`, and `packages` should be included in `deps.moduleDirectories`.
If a `RegExp` is provided, it is matched against the full file path.
:::
### server.deps.inline
* **Type:** `(string | RegExp)[] | true`
* **Default:** everything that is not externalized
Specifies modules that should be transformed and resolved by Vite. These modules are run by Vite's [module runner](https://vite.dev/guide/api-environment-runtimes#modulerunner).
Typically, your source files are inlined.
::: tip
If a string is provided, it is first normalized by prefixing the `/node_modules/` or other [`moduleDirectories`](/config/deps#moduledirectories) segments (for example, `'react'` becomes `/node_modules/react/`), and the resulting string is then matched against the full file path. For example, package `@company/some-name` located inside `packages/some-name` should be specified as `some-name`, and `packages` should be included in `deps.moduleDirectories`.
If a `RegExp` is provided, it is matched against the full file path.
:::
### server.deps.fallbackCJS
* **Type:** `boolean`
* **Default:** `false`
When enabled, Vitest will try to guess a CommonJS build for an ESM entry by checking a few common CJS/UMD file name and folder patterns (like `.mjs`, `.umd.js`, `.cjs.js`, `umd/`, `cjs/`, `lib/`).
This is a best-effort heuristic to work around confusing or incorrect ESM/CJS packaging and may not work for all dependencies.
---
---
url: /config/deps.md
---
# deps
* **Type:** `{ optimizer?, ... }`
Handling for dependencies resolution.
## deps.optimizer {#deps-optimizer}
* **Type:** `{ ssr?, client? }`
* **See also:** [Dep Optimization Options](https://vitejs.dev/config/dep-optimization-options.html)
Enable dependency optimization. If you have a lot of tests, this might improve their performance.
When Vitest encounters the external library listed in `include`, it will be bundled into a single file using esbuild and imported as a whole module. This is good for several reasons:
* Importing packages with a lot of imports is expensive. By bundling them into one file we can save a lot of time
* Importing UI libraries is expensive because they are not meant to run inside Node.js
* Your `alias` configuration is now respected inside bundled packages
* Code in your tests is running closer to how it's running in the browser
Be aware that only packages in `deps.optimizer?.[mode].include` option are bundled (some plugins populate this automatically, like Svelte). You can read more about available options in [Vite](https://vitejs.dev/config/dep-optimization-options.html) docs (Vitest doesn't support `disable` and `noDiscovery` options). By default, Vitest uses `optimizer.client` for `jsdom` and `happy-dom` environments, and `optimizer.ssr` for `node` and `edge` environments.
This options also inherits your `optimizeDeps` configuration (for web Vitest will extend `optimizeDeps`, for ssr - `ssr.optimizeDeps`). If you redefine `include`/`exclude` option in `deps.optimizer` it will extend your `optimizeDeps` when running tests. Vitest automatically removes the same options from `include`, if they are listed in `exclude`.
::: tip
You will not be able to edit your `node_modules` code for debugging, since the code is actually located in your `cacheDir` or `test.cache.dir` directory. If you want to debug with `console.log` statements, edit it directly or force rebundling with `deps.optimizer?.[mode].force` option.
:::
### deps.optimizer.{mode}.enabled
* **Type:** `boolean`
* **Default:** `false`
Enable dependency optimization.
## deps.client {#deps-client}
* **Type:** `{ transformAssets?, ... }`
Options that are applied to external files when the environment is set to `client`. By default, `jsdom` and `happy-dom` use `client` environment, while `node` and `edge` environments use `ssr`, so these options will have no affect on files inside those environments.
Usually, files inside `node_modules` are externalized, but these options also affect files in [`server.deps.external`](/config/server#server-deps-external).
### deps.client.transformAssets
* **Type:** `boolean`
* **Default:** `true`
Should Vitest process assets (.png, .svg, .jpg, etc) files and resolve them like Vite does in the browser.
This module will have a default export equal to the path to the asset, if no query is specified.
::: warning
At the moment, this option only works with [`vmThreads`](/config/pool#vmthreads) and [`vmForks`](/config/pool#vmforks) pools.
:::
### deps.client.transformCss
* **Type:** `boolean`
* **Default:** `true`
Should Vitest process CSS (.css, .scss, .sass, etc) files and resolve them like Vite does in the browser.
If CSS files are disabled with [`css`](/config/css) options, this option will just silence `ERR_UNKNOWN_FILE_EXTENSION` errors.
::: warning
At the moment, this option only works with [`vmThreads`](/config/pool#vmthreads) and [`vmForks`](/config/pool#vmforks) pools.
:::
### deps.client.transformGlobPattern
* **Type:** `RegExp | RegExp[]`
* **Default:** `[]`
Regexp pattern to match external files that should be transformed.
By default, files inside `node_modules` are externalized and not transformed, unless it's CSS or an asset, and corresponding option is not disabled.
::: warning
At the moment, this option only works with [`vmThreads`](/config/pool#vmthreads) and [`vmForks`](/config/pool#vmforks) pools.
:::
## deps.interopDefault
* **Type:** `boolean`
* **Default:** `true`
Interpret CJS module's default as named exports. Some dependencies only bundle CJS modules and don't use named exports that Node.js can statically analyze when a package is imported using `import` syntax instead of `require`. When importing such dependencies in Node environment using named exports, you will see this error:
```
import { read } from 'fs-jetpack';
^^^^
SyntaxError: Named export 'read' not found. The requested module 'fs-jetpack' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export.
```
Vitest doesn't do static analysis, and cannot fail before your running code, so you will most likely see this error when running tests, if this feature is disabled:
```
TypeError: createAsyncThunk is not a function
TypeError: default is not a function
```
By default, Vitest assumes you are using a bundler to bypass this and will not fail, but you can disable this behaviour manually, if your code is not processed.
## deps.moduleDirectories
* **Type:** `string[]`
* **Default:** `['node_modules']`
A list of directories that should be treated as module directories. This config option affects the behavior of [`vi.mock`](/api/vi#vi-mock): when no factory is provided and the path of what you are mocking matches one of the `moduleDirectories` values, Vitest will try to resolve the mock by looking for a `__mocks__` folder in the [root](/config/root) of the project.
This option will also affect if a file should be treated as a module when externalizing dependencies. By default, Vitest imports external modules with native Node.js bypassing Vite transformation step.
Setting this option will *override* the default, if you wish to still search `node_modules` for packages include it along with any other options:
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
deps: {
moduleDirectories: ['node_modules', path.resolve('../../packages')],
}
},
})
```
---
---
url: /config/runner.md
---
# runner
* **Type:** `VitestRunnerConstructor`
Path to a custom test runner. This is an advanced feature and should be used with custom library runners. You can read more about it in [the documentation](/api/advanced/runner).
---
---
url: /config/benchmark.md
---
# benchmark {#benchmark}
* **Type:** `{ include?, exclude?, ... }`
Options used when running `vitest bench`.
## benchmark.enabled
* **Type:** `boolean`
* **Default:** `false`
Enables the benchmark project. When set, Vitest creates a dedicated benchmark project alongside your regular test project, runs files matching [`benchmark.include`](#benchmark-include) in it, and exposes the [`bench` fixture](/guide/test-context#bench) to those files. Running `vitest bench` enables this automatically.
## benchmark.include
* **Type:** `string[]`
* **Default:** `['**/*.{bench,benchmark}.?(c|m)[jt]s?(x)']`
Include globs for benchmark test files
## benchmark.exclude
* **Type:** `string[]`
* **Default:** `['node_modules', 'dist', '.idea', '.git', '.cache']`
Exclude globs for benchmark test files
## benchmark.includeSource
* **Type:** `string[]`
* **Default:** `[]`
Include globs for in-source benchmark test files. This option is similar to [`includeSource`](/config/include-source).
When defined, Vitest will run all matched files with `import.meta.vitest` inside.
## benchmark.retainSamples
* **Type:** `boolean`
* **Default:** `false`
Include the `samples` array of per-iteration timings on every benchmark result. Disabled by default to reduce memory usage; enable when a custom reporter or API consumer needs the raw samples.
## benchmark.provider
* **Type:** `string`
* **Default:** `undefined` (uses the built-in provider)
The benchmark provider that executes registered benchmarks and returns their results. Set this to a module path whose default export implements `BenchmarkProvider`. Relative paths are resolved from the project root.
See the [Custom Benchmark Provider](/guide/advanced/benchmark-provider) guide for setup instructions and the provider API.
## benchmark.suppressExportGetterWarnings
* **Type:** `boolean`
* **Default:** `false`
Suppress the warning printed when a benchmark accesses module export getters too many times. Vitest tracks getter access during benchmark runs because Vite's module runner wraps every export in a getter, and excessive access can dominate the measurement (see [Module Runner Overhead](/guide/benchmarking#module-runner-overhead)). Enable this when you've intentionally accepted the overhead, or when the warning is noisy for benchmarks where the getter cost is negligible.
---
---
url: /config/alias.md
---
# alias
* **Type:** `Record | Array<{ find: string | RegExp, replacement: string, customResolver?: ResolverFunction | ResolverObject }>`
Define custom aliases when running inside tests. They will be merged with aliases from `resolve.alias`.
::: warning
Vitest uses Vite SSR primitives to run tests which has [certain pitfalls](https://vitejs.dev/guide/ssr.html#ssr-externals).
1. Aliases affect only modules imported directly with an `import` keyword by an [inlined](/config/server#server-deps-inline) module (all source code is inlined by default).
2. Vitest does not support aliasing `require` calls.
3. If you are aliasing an external dependency (e.g., `react` -> `preact`), you may want to alias the actual `node_modules` packages instead to make it work for externalized dependencies. Both [Yarn](https://classic.yarnpkg.com/en/docs/cli/add/#toc-yarn-add-alias) and [pnpm](https://pnpm.io/aliases/) support aliasing via the `npm:` prefix.
:::
---
---
url: /config/globals.md
---
# globals
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--globals`, `--no-globals`, `--globals=false`
By default, `vitest` does not provide global APIs for explicitness. If you prefer to use the APIs globally like Jest, you can pass the `--globals` option to CLI or add `globals: true` in the config.
```js
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
},
})
```
::: tip
Note that some libraries, e.g., `@testing-library/react`, rely on globals being present to perform auto cleanup.
:::
To get TypeScript working with the global APIs, add `vitest/globals` to the `types` field in your `tsconfig.json`:
```json [tsconfig.json]
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
```
If you have redefined your [`typeRoots`](https://www.typescriptlang.org/tsconfig/#typeRoots) to include additional types in your compilation, you will need to add back the `node_modules` to make `vitest/globals` discoverable:
```json [tsconfig.json]
{
"compilerOptions": {
"typeRoots": ["./types", "./node_modules/@types", "./node_modules"],
"types": ["vitest/globals"]
}
}
```
---
---
url: /config/injectcjsglobals.md
---
# injectCjsGlobals
* **Type:** `boolean`
* **Default:** `true`
* **CLI:** `--no-inject-cjs-globals`, `--injectCjsGlobals=false`
Inject CommonJS module variables (`module`, `exports`, `require`, `__filename`, `__dirname`) into every module processed by Vitest.
By default, every file that Vitest transforms has access to these variables even if it is written using ESM syntax. This doesn't reflect how modules work in the wild: browsers do not support CommonJS variables, and Node.js doesn't expose them in ES modules.
To make the module environment stricter and closer to the target runtime, you can disable this behaviour:
```js
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
injectCjsGlobals: false,
},
})
```
When this option is disabled, only modules that are detected to be CommonJS receive these variables. CommonJS modules always keep them because they are part of the module scope, without them the module cannot be evaluated at all. The module type is detected the same way Node.js does it:
1. The file extension: `.cjs` and `.cts` files are always CommonJS, `.mjs` and `.mts` files are always ES modules.
2. The `type` field in the nearest `package.json`: `"module"` means ES module, `"commonjs"` means CommonJS. Same as in Node.js, the lookup stops at the first `package.json` and never crosses a `node_modules` boundary, so dependencies don't inherit the `type` of your project.
3. The presence of ESM syntax in the file: if the file has no static `import`/`export` declarations and doesn't reference `import.meta`, it is treated as CommonJS. Syntax inside comments and strings doesn't affect the detection. Dynamic imports are allowed in CommonJS modules, so they don't count as ESM syntax; type-only TypeScript imports are erased during the transform, so they don't count either.
The syntax detection is always enabled: Vitest doesn't respect Node.js CLI flags that modify the module type resolution, like `--no-experimental-detect-module`, `--input-type` (it only applies to the string input in Node.js), or the `--experimental-default-type` flag removed in Node.js 23.
Referencing a CommonJS variable in an ES module throws a `ReferenceError`, just like outside of Vitest:
```
ReferenceError: __dirname is not defined
"__dirname" is a CommonJS variable that is not available in ES modules, and "injectCjsGlobals" is disabled. If this module is meant to be an ES module, use "import.meta.dirname" instead of "__dirname". If it is meant to be a CommonJS module, use the ".cjs" file extension, set "type": "commonjs" in the nearest package.json, or externalize it with "server.deps.external".
```
::: warning
This option doesn't affect externalized modules which are always executed by the native runtime. Node.js provides CommonJS variables to externalized CommonJS modules on its own.
Note that inlined CommonJS modules are not processed by Vite plugins even when this option is enabled: `require` calls always leave the module runner, so features like mocking do not apply to them.
:::
---
---
url: /config/environment.md
---
# environment
* **Type:** `'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' | string`
* **Default:** `'node'`
* **CLI:** `--environment=`
The environment that will be used for testing. The default environment in Vitest
is a Node.js environment. If you are building a web application, you can use
browser-like environment through either [`jsdom`](https://github.com/jsdom/jsdom)
or [`happy-dom`](https://github.com/capricorn86/happy-dom) instead.
If you are building edge functions, you can use [`edge-runtime`](https://edge-runtime.vercel.app/packages/vm) environment
::: tip
You can also use [Browser Mode](/guide/browser/) to run integration or unit tests in the browser without mocking the environment.
:::
To define custom options for your environment, use [`environmentOptions`](/config/environmentoptions).
By adding a `@vitest-environment` docblock or comment at the top of the file,
you can specify another environment to be used for all tests in that file:
Docblock style:
```js
/**
* @vitest-environment jsdom
*/
test('use jsdom in this test file', () => {
const element = document.createElement('div')
expect(element).not.toBeNull()
})
```
Comment style:
```js
// @vitest-environment happy-dom
test('use happy-dom in this test file', () => {
const element = document.createElement('div')
expect(element).not.toBeNull()
})
```
For compatibility with Jest, there is also a `@jest-environment`:
```js
/**
* @jest-environment jsdom
*/
test('use jsdom in this test file', () => {
const element = document.createElement('div')
expect(element).not.toBeNull()
})
```
You can also define a custom environment. When non-builtin environment is used, Vitest will try to load the file if it's relative or absolute, or a package `vitest-environment-${name}`, if the name is a bare specifier.
The custom environment file should export an object with the shape of `Environment`:
```ts [environment.js]
import type { Environment } from 'vitest'
export default {
name: 'custom',
viteEnvironment: 'ssr',
setup() {
// custom setup
return {
teardown() {
// called after all tests with this env have been run
}
}
}
}
```
::: tip
The `viteEnvironment` field corresponds to the environment defined by the [Vite Environment API](https://vite.dev/guide/api-environment#environment-api). By default, Vite exposes `client` (for the browser) and `ssr` (for the server) environments.
:::
Vitest also exposes `builtinEnvironments` through `vitest/environments` entry, in case you just want to extend it. You can read more about extending environments in [our guide](/guide/environment).
::: tip
jsdom environment exposes `jsdom` global variable equal to the current [JSDOM](https://github.com/jsdom/jsdom) instance. If you want TypeScript to recognize it, you can add `vitest/jsdom` to your `tsconfig.json` when you use this environment:
```json [tsconfig.json]
{
"compilerOptions": {
"types": ["vitest/jsdom"]
}
}
```
:::
---
---
url: /config/environmentoptions.md
---
# environmentOptions
* **Type:** `Record<'jsdom' | 'happyDOM' | string, unknown>`
* **Default:** `{}`
These options are passed to the setup method of the current [environment](/config/environment). By default, you can configure options only for `jsdom` and `happyDOM` when you use them as your test environment.
## Example
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environmentOptions: {
jsdom: {
url: 'http://localhost:3000',
},
happyDOM: {
width: 300,
height: 400,
},
},
},
})
```
::: warning
Options are scoped to their environment. For example, put jsdom options under the `jsdom` key and happy-dom options under the `happyDOM` key. This lets you mix multiple environments within the same project.
:::
---
---
url: /config/watch.md
---
# watch {#watch}
* **Type:** `boolean`
* **Default:** `!process.env.CI && process.stdin.isTTY`
* **CLI:** `-w`, `--watch`, `--watch=false`
Enable watch mode
In interactive environments, this is the default, unless `--run` is specified explicitly.
In CI, or when run from a non-interactive shell, "watch" mode is not the default, but can be enabled explicitly with this flag.
---
---
url: /config/watchtriggerpatterns.md
---
# watchTriggerPatterns 3.2.0
* **Type:** `WatcherTriggerPattern[]`
Vitest reruns tests based on the module graph which is populated by static and dynamic `import` statements. However, if you are reading from the file system or fetching from a proxy, then Vitest cannot detect those dependencies.
To correctly rerun those tests, you can define a regex pattern and a function that returns a list of test files to run.
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
watchTriggerPatterns: [
{
pattern: /src\/(mailers|templates)\/(.*)\.(ts|html|txt)$/,
testsToRun: (id, match) => {
// relative to the root value
return `./api/tests/mailers/${match[2]}.test.ts`
},
},
],
},
})
```
::: warning
Returned files should be either absolute or relative to the root. Note that this is a global option, and it cannot be used inside of [project](/guide/projects) configs.
:::
---
---
url: /config/root.md
---
# root
* **Type:** `string`
* **CLI:** `-r `, `--root=`
Project root
---
---
url: /config/dir.md
---
# dir
* **Type:** `string`
* **CLI:** `--dir=`
* **Default:** same as `root`
Base directory to scan for the test files. You can specify this option to speed up test discovery if your root covers the whole project
---
---
url: /config/reporters.md
---
# reporters
* **Type:**
```ts
interface UserConfig {
reporters?: ConfigReporter | Array
}
type ConfigReporter = string | Reporter | [string, object?]
```
* **Default:** [`'default'`](/guide/reporters#default-reporter). See [Default Reporters](/guide/reporters#default-reporters) for environment-specific behavior.
* **CLI:**
* `--reporter=tap` for a single reporter
* `--reporter=verbose --reporter=github-actions` for multiple reporters
This option defines a single reporter or a list of reporters available to Vitest during the test run.
Alongside built-in reporters, you can also pass down a custom implementation of a [`Reporter` interface](/api/advanced/reporters), or a path to a module that exports it as a default export (e.g. `'./path/to/reporter.ts'`, `'@scope/reporter'`).
You can configure a reporter by providing a tuple: `[string, object]`, where the string is a reporter name, and the object is the reporter's options.
::: warning
Note that the [coverage](/guide/coverage) feature uses a different [`coverage.reporter`](/config/coverage#reporter) option instead of this one.
:::
## Built-in Reporters
* [`default`](/guide/reporters#default-reporter)
* [`verbose`](/guide/reporters#verbose-reporter)
* [`tree`](/guide/reporters#tree-reporter)
* [`dot`](/guide/reporters#dot-reporter)
* [`junit`](/guide/reporters#junit-reporter)
* [`json`](/guide/reporters#json-reporter)
* [`html`](/guide/reporters#html-reporter)
* [`tap`](/guide/reporters#tap-reporter)
* [`tap-flat`](/guide/reporters#tap-flat-reporter)
* [`hanging-process`](/guide/reporters#hanging-process-reporter)
* [`github-actions`](/guide/reporters#github-actions-reporter)
* [`minimal`](/guide/reporters#minimal-reporter) (aliased as `agent`)
* [`blob`](/guide/reporters#blob-reporter)
## Example
::: code-group
```js [vitest.config.js]
import { configDefaults, defineConfig } from 'vitest/config'
export default defineConfig({
test: {
reporters: [
...configDefaults.reporters,
// conditional reporter
...(process.env.CI ? ['html'] : []),
// custom reporter from npm package
// options are passed down as a tuple
[
'vitest-sonar-reporter',
{ outputFile: 'sonar-report.xml' }
],
]
}
})
```
```bash [CLI]
vitest --reporter=github-actions --reporter=junit
```
:::
---
---
url: /config/outputfile.md
---
# outputFile {#outputfile}
* **Type:** `string | Record`
* **CLI:** `--outputFile=`, `--outputFile.json=./path`
Write test results to a file when the `--reporter=json` or `--reporter=junit` option is also specified.
By providing an object instead of a string you can define individual outputs when using multiple reporters.
---
---
url: /config/pool.md
---
# pool
* **Type:** `'threads' | 'forks' | 'vmThreads' | 'vmForks'`
* **Default:** `'forks'`
* **CLI:** `--pool=threads`
Pool used to run tests in.
## threads
Enable multi-threading. When using threads you are unable to use process related APIs such as `process.chdir()`. Some libraries written in native languages, such as `Prisma`, `bcrypt` and `canvas`, have problems when running in multiple threads and run into segfaults. In these cases it is advised to use `forks` pool instead.
## forks
Similar as `threads` pool but uses `child_process` instead of `worker_threads`. Communication between tests and main process is not as fast as with `threads` pool. Process related APIs such as `process.chdir()` are available in `forks` pool.
## vmThreads
Run tests using [VM context](https://nodejs.org/api/vm.html) (inside a sandboxed environment) in a `threads` pool.
This makes tests run faster, but the VM module is unstable when running [ESM code](https://github.com/nodejs/node/issues/37648). Your tests will [leak memory](https://github.com/nodejs/node/issues/33439) - to battle that, workers are restarted when they exceed [`vmMemoryLimit`](/config/vmmemorylimit).
::: warning Worker recycling is expensive in `vmThreads`
Restarting a worker thread is not free: Node.js runs a full garbage collection over everything the worker accumulated before the thread can exit, and that work runs on a small pool of background threads shared by every worker in the process. When a large test suite hits [`vmMemoryLimit`](/config/vmmemorylimit) repeatedly, these teardowns pile up and also slow down the workers that are still running tests.
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.
* The globals within native modules, such as (`fs`, `path`, etc), differ from the globals present in your test environment. As a result, any error thrown by these native modules will reference a different Error constructor compared to the one used in your code:
```ts
try {
fs.writeFileSync('/does-not-exist')
}
catch (err) {
console.log(err instanceof Error) // false
}
```
* Importing ES modules caches them indefinitely which introduces memory leaks if you have a lot of contexts (test files). There is no API in Node.js that clears that cache.
* Accessing globals [takes longer](https://github.com/nodejs/node/issues/31658) in a sandbox environment.
Please, be aware of these issues when using this option. Vitest team cannot fix any of the issues on our side.
:::
## vmForks
Similar as `vmThreads` pool but uses `child_process` instead of `worker_threads`. Communication between tests and the main process is not as fast as with `vmThreads` pool. Process related APIs such as `process.chdir()` are available in `vmForks` pool. Please be aware that this pool has the same pitfalls listed in `vmThreads`.
Unlike `vmThreads`, recycling a worker that exceeded [`vmMemoryLimit`](/config/vmmemorylimit) only requires the child process to exit, so it is much cheaper. On large test suites that recycle workers regularly, prefer `vmForks` over `vmThreads`.
---
---
url: /config/execargv.md
---
# execArgv
* **Type:** `string[]`
* **Default:** `[]`
Pass additional arguments to `node` in the runner worker. See [Command-line API | Node.js](https://nodejs.org/docs/latest/api/cli.html) for more information.
:::warning
Be careful when using, it as some options may crash worker, e.g. `--prof`, `--title`. See https://github.com/nodejs/node/issues/41103.
:::
---
---
url: /config/vmmemorylimit.md
---
# vmMemoryLimit
* **Type:** `string | number`
* **Default:** `1 / maxWorkers`
This option affects only `vmForks` and `vmThreads` pools.
Specifies the memory limit for workers before they are recycled.
By default, the total system memory is split evenly between workers. By increasing [`maxWorkers`](/config/maxworkers), workers have less memory available, so they're recycled more often.
This value heavily depends on your environment, so it's better to specify it manually instead of relying on the default.
Recycling exists because VM contexts [leak memory](https://github.com/nodejs/node/issues/33439): a worker's memory usage grows with every test file it runs, so a worker cannot live forever. The limit is a trade-off:
* A low limit recycles workers frequently. In the `vmThreads` pool this is expensive, because destroying a worker thread runs a full garbage collection over the worker's memory and competes with running tests for the process' shared background threads. The `vmForks` pool recycles workers by letting the child process exit, which makes frequent recycling much cheaper there.
* A high limit lets workers accumulate memory. When the combined memory usage of all workers approaches what the machine can hold, every pool slows down.
::: tip
The implementation is based on Jest's [`workerIdleMemoryLimit`](https://jestjs.io/docs/configuration#workeridlememorylimit-numberstring).
The limit can be specified in a number of different ways and whatever the result is `Math.floor` is used to turn it into an integer value:
* `<= 1` - The value is assumed to be a percentage of system memory. So 0.5 sets the memory limit of the worker to half of the total system memory
* `\> 1` - Assumed to be a fixed byte value. Because of the previous rule if you wanted a value of 1 byte (I don't know why) you could use 1.1.
* With units
* `50%` - As above, a percentage of total system memory
* `100KB`, `65MB`, etc - With units to denote a fixed memory limit.
* `K` / `KB` - Kilobytes (x1000)
* `KiB` - Kibibytes (x1024)
* `M` / `MB` - Megabytes
* `MiB` - Mebibytes
* `G` / `GB` - Gigabytes
* `GiB` - Gibibytes
:::
::: warning
Percentage based memory limit [does not work on Linux CircleCI](https://github.com/jestjs/jest/issues/11956#issuecomment-1212925677) workers due to incorrect system memory being reported.
:::
---
---
url: /config/fileparallelism.md
---
# fileParallelism
* **Type:** `boolean`
* **Default:** `true`
* **CLI:** `--no-file-parallelism`, `--fileParallelism=false`
Should all test files run in parallel. Setting this to `false` will override `maxWorkers` option to `1`.
::: tip
This option doesn't affect tests running in the same file. If you want to run those in parallel, use `concurrent` option on [describe](/api/describe#describe-concurrent) or via [a config](/config/sequence#sequence-concurrent).
:::
---
---
url: /config/maxworkers.md
---
# maxWorkers
* **Type:** `number | string`
* **Default:**
* if [`watch`](/config/watch) is disabled, uses all available parallelism
* if [`watch`](/config/watch) is enabled, uses half of all available parallelism
Defines the maximum concurrency for test workers. Accepts either a number or a percentage string.
* Number: spawns up to the specified number of workers.
* Percentage string (e.g., "50%"): computes the worker count as the given percentage of the machine’s available parallelism.
## Example
### Number
::: code-group
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
maxWorkers: 4,
},
})
```
```bash [CLI]
vitest --maxWorkers=4
```
:::
### Percent
::: code-group
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
maxWorkers: '50%',
},
})
```
```bash [CLI]
vitest --maxWorkers=50%
```
:::
Vitest uses [`os.availableParallelism`](https://nodejs.org/api/os.html#osavailableparallelism) to know the maximum amount of parallelism available.
---
---
url: /config/testtimeout.md
---
# testTimeout
* **Type:** `number`
* **Default:** `5_000` in Node.js, `15_000` if `browser.enabled` is `true`
* **CLI:** `--test-timeout=5000`, `--testTimeout=5000`
Default timeout of a test in milliseconds. Use `0` to disable timeout completely.
---
---
url: /config/hooktimeout.md
---
# hookTimeout
* **Type:** `number`
* **Default:** `10_000` in Node.js, `30_000` if `browser.enabled` is `true`
* **CLI:** `--hook-timeout=10000`, `--hookTimeout=10000`
Default timeout of a hook in milliseconds. Use `0` to disable timeout completely.
---
---
url: /config/teardowntimeout.md
---
# teardownTimeout {#teardowntimeout}
* **Type:** `number`
* **Default:** `10000`
* **CLI:** `--teardown-timeout=5000`, `--teardownTimeout=5000`
Default timeout to wait for close when Vitest shuts down, in milliseconds
---
---
url: /config/silent.md
---
# silent {#silent}
* **Type:** `boolean | 'passed-only'`
* **Default:** `false`
* **CLI:** `--silent`, `--silent=false`
Silent console output from tests.
Use `'passed-only'` to see logs from failing tests only. Logs from failing tests are printed after a test has finished.
---
---
url: /config/setupfiles.md
---
# setupFiles
* **Type:** `string | string[]`
Paths to setup files resolved relative to the [`root`](/config/root). They will run before each *test file* in the same process. By default, all test files run in parallel, but you can configure it with [`sequence.setupFiles`](/config/sequence#sequence-setupfiles) option.
Vitest will ignore any exports from these files.
:::warning
Note that setup files are executed in the same process as tests, unlike [`globalSetup`](/config/globalsetup) that runs once in the main thread before any test worker is created.
:::
:::info
Editing a setup file will automatically trigger a rerun of all tests.
:::
If you have a heavy process running in the background, you can use `process.env.VITEST_POOL_ID` (integer-like string) inside to distinguish between workers and spread the workload.
:::warning
If [isolation](/config/isolate) is disabled, imported modules are cached, but the setup file itself is executed again before each test file, meaning that you are accessing the same global object before each test file. Make sure you are not doing the same thing more than necessary.
For example, you may rely on a global variable:
```ts
import { config } from '@some-testing-lib'
if (!globalThis.setupInitialized) {
config.plugins = [myCoolPlugin]
computeHeavyThing()
globalThis.setupInitialized = true
}
// hooks reset before each test file
afterEach(() => {
cleanup()
})
globalThis.resetBeforeEachTest = true
```
:::
---
---
url: /config/provide.md
---
# provide
* **Type:** `Partial`
Define values that can be accessed inside your tests using `inject` method.
:::code-group
```ts [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
provide: {
API_KEY: '123',
},
},
})
```
```ts [api.test.js]
import { expect, inject, test } from 'vitest'
test('api key is defined', () => {
expect(inject('API_KEY')).toBe('123')
})
```
:::
::: warning
Properties have to be strings and values need to be [serializable](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types) because this object will be transferred between different processes.
:::
::: tip
If you are using TypeScript, you will need to augment `ProvidedContext` type for type safe access:
```ts [vitest.shims.d.ts]
declare module 'vitest' {
export interface ProvidedContext {
API_KEY: string
}
}
// mark this file as a module so augmentation works correctly
export {}
```
:::
---
---
url: /config/globalsetup.md
---
# globalSetup
* **Type:** `string | string[]`
Path to global setup files relative to project [root](/config/root).
A global setup file can either export named functions `setup` and `teardown` or a `default` function that returns a teardown function:
::: code-group
```js [exports]
export function setup(project) {
console.log('setup')
}
export function teardown() {
console.log('teardown')
}
```
```js [default]
export default function setup(project) {
console.log('setup')
return function teardown() {
console.log('teardown')
}
}
```
:::
Note that the `setup` method and a `default` function receive a [test project](/api/advanced/test-project) as the first argument. The global setup is called before the test workers are created and only if there is at least one test queued, and teardown is called after all test files have finished running. In [watch mode](/config/watch), the teardown is called before the process is exited instead. If you need to reconfigure your setup before the test rerun, you can use [`onTestsRerun`](#handling-test-reruns) hook instead.
Multiple global setup files are possible. `setup` and `teardown` are executed sequentially with teardown in reverse order.
::: danger
Beware that the global setup is running in a different global scope before test workers are even created, so your tests don't have access to global variables defined here. However, you can pass down serializable data to tests via [`provide`](/config/provide) method and read them in your tests via `inject` imported from `vitest`:
:::code-group
```ts [example.test.ts]
import { inject } from 'vitest'
inject('wsPort') === 3000
```
```ts [globalSetup.ts]
import type { TestProject } from 'vitest/node'
export default function setup(project: TestProject) {
project.provide('wsPort', 3000)
}
declare module 'vitest' {
export interface ProvidedContext {
wsPort: number
}
}
```
If you need to execute code in the same process as tests, use [`setupFiles`](/config/setupfiles) instead, but note that it runs before every test file.
:::
## Handling Test Reruns
You can define a custom callback function to be called when Vitest reruns tests. The test runner will wait for it to complete before executing tests. Note that you cannot destruct the `project` like `{ onTestsRerun }` because it relies on the context.
```ts [globalSetup.ts]
import type { TestProject } from 'vitest/node'
export default function setup(project: TestProject) {
project.onTestsRerun(async () => {
await restartDb()
})
}
```
---
---
url: /config/forcereruntriggers.md
---
# forceRerunTriggers
* **Type:** `string[]`
* **Default:** `['**/package.json', '**/vitest.config.*', '**/vite.config.*']`
Glob pattern of file paths that will trigger the whole suite rerun. When paired with the `--changed` argument will run the whole test suite if the trigger is found in the git diff.
Useful if you are testing calling CLI commands, because Vite cannot construct a module graph:
```ts
test('execute a script', async () => {
// Vitest cannot rerun this test, if content of `dist/index.js` changes
await execa('node', ['dist/index.js'])
})
```
::: tip
Make sure that your files are not excluded by [`server.watch.ignored`](https://vitejs.dev/config/server-options.html#server-watch).
:::
---
---
url: /config/coverage.md
---
# coverage {#coverage}
You can use [`v8`](/guide/coverage.html#v8-provider), [`istanbul`](/guide/coverage.html#istanbul-provider) or [a custom coverage solution](/guide/coverage#custom-coverage-provider) for coverage collection.
You can provide coverage options to CLI with dot notation:
```sh
npx vitest --coverage.enabled --coverage.provider=istanbul
```
::: warning
If you are using coverage options with dot notation, don't forget to specify `--coverage.enabled`. Do not provide a single `--coverage` option in that case.
:::
## coverage.provider
* **Type:** `'v8' | 'istanbul' | 'custom'`
* **Default:** `'v8'`
* **CLI:** `--coverage.provider=`
Use `provider` to select the tool for coverage collection.
## coverage.enabled
* **Type:** `boolean`
* **Default:** `false`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.enabled`, `--coverage.enabled=false`
Enables coverage collection. Can be overridden using `--coverage` CLI option.
## coverage.include
* **Type:** `string[]`
* **Default:** Files that were imported during test run
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.include=`, `--coverage.include= --coverage.include=`
List of files included in coverage as glob patterns. By default only files covered by tests are included.
It is recommended to pass file extensions in the pattern.
See [Including and excluding files from coverage report](/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples.
## coverage.exclude
* **Type:** `string[]`
* **Default:** : `[]`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.exclude=`, `--coverage.exclude= --coverage.exclude=`
List of files excluded from coverage as glob patterns.
See [Including and excluding files from coverage report](/guide/coverage.html#including-and-excluding-files-from-coverage-report) for examples.
## coverage.clean
* **Type:** `boolean`
* **Default:** `true`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.clean`, `--coverage.clean=false`
Clean coverage results before running tests.
## coverage.cleanOnRerun
* **Type:** `boolean`
* **Default:** `true`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.cleanOnRerun`, `--coverage.cleanOnRerun=false`
Clean coverage report on watch rerun. Set to `false` to preserve coverage results from previous run in watch mode.
## coverage.reportsDirectory
* **Type:** `string`
* **Default:** `'./coverage'`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.reportsDirectory=`
::: warning
Vitest will delete this directory before running tests if `coverage.clean` is enabled (default value).
:::
Directory to write coverage report to.
## coverage.reporter
* **Type:** `string | string[] | [string, {}][]`
* **Default:** `['text', 'html', 'clover', 'json']`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.reporter=`, `--coverage.reporter= --coverage.reporter=`
Coverage reporters to use. See [istanbul documentation](https://istanbul.js.org/docs/advanced/alternative-reporters/) for detailed list of all reporters. See [`@types/istanbul-reports`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/276d95e4304b3670eaf6e8e5a7ea9e265a14e338/types/istanbul-reports/index.d.ts) for details about reporter specific options.
The reporter has three different types:
* A single reporter: `{ reporter: 'html' }`
* Multiple reporters without options: `{ reporter: ['html', 'json'] }`
* A single or multiple reporters with reporter options:
```ts
{
reporter: [
['lcov', { 'projectRoot': './src' }],
['json', { 'file': 'coverage.json' }],
['text']
]
}
```
You can also pass custom coverage reporters. See [Guide - Custom Coverage Reporter](/guide/coverage#custom-coverage-reporter) for more information.
```ts
{
reporter: [
// Specify reporter using name of the NPM package
'@vitest/custom-coverage-reporter',
['@vitest/custom-coverage-reporter', { someOption: true }],
// Specify reporter using local path
'/absolute/path/to/custom-reporter.cjs',
['/absolute/path/to/custom-reporter.cjs', { someOption: true }],
]
}
```
You can check your coverage report in Vitest UI: check [Vitest UI Coverage](/guide/coverage#vitest-ui) for more details.
::: tip AI coding agents
When Vitest detects it is running inside an AI coding agent, it automatically adds the `text-summary` reporter and sets `skipFull: true` on the `text` reporter to reduce output and minimize token usage.
:::
## coverage.reportOnFailure {#coverage-reportonfailure}
* **Type:** `boolean`
* **Default:** `false`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.reportOnFailure`, `--coverage.reportOnFailure=false`
Generate coverage report even when tests fail.
## coverage.allowExternal
* **Type:** `boolean`
* **Default:** `false`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.allowExternal`, `--coverage.allowExternal=false`
Collect coverage of files outside the [project `root`](/config/root).
## coverage.excludeAfterRemap
* **Type:** `boolean`
* **Default:** `false`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.excludeAfterRemap`, `--coverage.excludeAfterRemap=false`
Apply exclusions again after coverage has been remapped to original sources.
This is useful when your source files are transpiled and may contain source maps of non-source files.
Use this option when you are seeing files that show up in report even if they match your `coverage.exclude` patterns.
## coverage.skipFull
* **Type:** `boolean`
* **Default:** `false`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.skipFull`, `--coverage.skipFull=false`
Do not show files with 100% statement, branch, and function coverage.
## coverage.thresholds
Options for coverage thresholds.
If a threshold is set to a positive number, it will be interpreted as the minimum percentage of coverage required. For example, setting the lines threshold to `90` means that 90% of lines must be covered.
If a threshold is set to a negative number, it will be treated as the maximum number of uncovered items allowed. For example, setting the lines threshold to `-10` means that no more than 10 lines may be uncovered.
```ts
{
coverage: {
thresholds: {
// Requires 90% function coverage
functions: 90,
// Require that no more than 10 lines are uncovered
lines: -10,
}
}
}
```
### coverage.thresholds.lines
* **Type:** `number`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.thresholds.lines=`
Global threshold for lines.
### coverage.thresholds.functions
* **Type:** `number`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.thresholds.functions=`
Global threshold for functions.
### coverage.thresholds.branches
* **Type:** `number`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.thresholds.branches=`
Global threshold for branches.
### coverage.thresholds.statements
* **Type:** `number`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.thresholds.statements=`
Global threshold for statements.
### coverage.thresholds.perFile
* **Type:** `boolean | { 100?: boolean, lines?: number, functions?: number, branches?: number, statements?: number }`
* **Default:** `false`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.thresholds.perFile`, `--coverage.thresholds.perFile=false`
When `true`, each file is checked against the top-level thresholds instead of the project-wide aggregate. When set to an object, both are checked: the aggregate against the top-level thresholds, and every file against these per-file minimums.
```ts
{
coverage: {
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
perFile: {
lines: 50,
functions: 50,
branches: 50,
statements: 50,
},
}
}
}
```
`{ 100: true }` is also accepted inside the object as a shortcut for setting all four metrics to `100`:
```ts
{
coverage: {
thresholds: {
lines: 80,
perFile: {
100: true,
},
}
}
}
```
`perFile` can also be set on an individual [glob-pattern threshold](/config/coverage#coverage-thresholds-glob-pattern). Glob patterns do **not** inherit the top-level `perFile`; set it on each glob explicitly.
```ts
{
coverage: {
thresholds: {
perFile: true,
lines: 80,
'src/utils/**': {
lines: 90,
perFile: true,
},
}
}
}
```
### coverage.thresholds.autoUpdate
* **Type:** `boolean | function`
* **Default:** `false`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.thresholds.autoUpdate=`
Update all threshold values `lines`, `functions`, `branches` and `statements` to configuration file when current coverage is better than the configured thresholds.
This option helps to maintain thresholds when coverage is improved.
You can also pass a function for formatting the updated threshold values. The function receives the new threshold as the first argument and the previous threshold as the second:
```ts
{
coverage: {
thresholds: {
// Log the change and update without decimals
autoUpdate: (newThreshold, previousThreshold) => {
console.log(`Updated threshold from ${previousThreshold} to ${newThreshold}`)
return Math.floor(newThreshold)
},
// 95.85 -> 95
functions: 95,
}
}
}
```
### coverage.thresholds.100
* **Type:** `boolean`
* **Default:** `false`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.thresholds.100`, `--coverage.thresholds.100=false`
Sets global thresholds to 100.
Shortcut for `--coverage.thresholds.lines 100 --coverage.thresholds.functions 100 --coverage.thresholds.branches 100 --coverage.thresholds.statements 100`.
### coverage.thresholds\[glob-pattern]
* **Type:** `{ statements?: number, functions?: number, branches?: number, lines?: number, perFile?: boolean | object }`
* **Default:** `undefined`
* **Available for providers:** `'v8' | 'istanbul'`
Sets thresholds for files matching the glob pattern.
Each glob pattern can set its own `perFile` (`boolean | object`), checked exactly like the top-level `perFile` but scoped to the matched files. Glob patterns do not inherit the top-level `perFile` — set it per glob.
::: tip NOTE
Vitest counts all files, including those covered by glob-patterns, into the global coverage thresholds.
This is different from Jest behavior.
:::
```ts
{
coverage: {
thresholds: {
// Thresholds for all files
functions: 95,
branches: 70,
// Thresholds for matching glob pattern
'src/utils/**.ts': {
statements: 95,
functions: 90,
branches: 85,
lines: 80,
// each matching file must individually hit the thresholds above
perFile: true,
},
// Files matching this pattern will only have lines thresholds set.
// Global thresholds are not inherited.
'**/math.ts': {
lines: 100,
}
}
}
}
```
### coverage.thresholds\[glob-pattern].100
* **Type:** `boolean`
* **Default:** `false`
* **Available for providers:** `'v8' | 'istanbul'`
Sets thresholds to 100 for files matching the glob pattern.
```ts
{
coverage: {
thresholds: {
// Thresholds for all files
functions: 95,
branches: 70,
// Thresholds for matching glob pattern
'src/utils/**.ts': { 100: true },
'**/math.ts': { 100: true }
}
}
}
```
## coverage.ignoreClassMethods
* **Type:** `string[]`
* **Default:** `[]`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.ignoreClassMethods=`
Set to array of class method names to ignore for coverage.
See [istanbul documentation](https://github.com/istanbuljs/nyc#ignoring-methods) for more information.
## coverage.watermarks
* **Type:**
```ts
{
statements?: [number, number],
functions?: [number, number],
branches?: [number, number],
lines?: [number, number]
}
```
* **Default:**
```ts
{
statements: [50, 80],
functions: [50, 80],
branches: [50, 80],
lines: [50, 80]
}
```
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.watermarks.statements=50,80`, `--coverage.watermarks.branches=50,80`
Watermarks for statements, lines, branches and functions. See [istanbul documentation](https://github.com/istanbuljs/nyc#high-and-low-watermarks) for more information.
## coverage.processingConcurrency
* **Type:** `boolean`
* **Default:** `Math.min(20, os.availableParallelism?.() ?? os.cpus().length)`
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.processingConcurrency=`
Concurrency limit used when processing the coverage results.
## coverage.instrumenter 4.1.5 {#coverage-instrumenter}
* **Type:** `(options: InstrumenterOptions) => CoverageInstrumenter`
* **Available for providers:** `'istanbul'`
Factory for a custom instrumenter to use in place of the default `istanbul-lib-instrument`. Vitest calls the factory once during initialization and reuses the returned instrumenter for every file. The rest of the Istanbul pipeline (collection, merging, reporting) is unchanged.
The factory receives an `InstrumenterOptions` object with Vitest's runtime coverage settings, and must return an object implementing the `CoverageInstrumenter` interface. Both types are exported from `vitest/node`.
```ts
interface InstrumenterOptions {
coverageVariable: string
coverageGlobalScope: string
coverageGlobalScopeFunc: boolean
ignoreClassMethods: string[]
}
interface CoverageInstrumenter {
instrumentSync: (code: string, filename: string, inputSourceMap?: any) => string
lastSourceMap: () => any
lastFileCoverage: () => any
}
```
```ts
import { defineConfig } from 'vitest/config'
import { createInstrumenter } from '@vitest/some-custom-instrumenter'
export default defineConfig({
test: {
coverage: {
provider: 'istanbul',
instrumenter: options => createInstrumenter(options),
}
}
})
```
## coverage.customProviderModule
* **Type:** `string`
* **Available for providers:** `'custom'`
* **CLI:** `--coverage.customProviderModule=`
Specifies the module name or path for the custom coverage provider module. See [Guide - Custom Coverage Provider](/guide/coverage#custom-coverage-provider) for more information.
## coverage.htmlDir
* **Type:** `string`
* **Default:** Automatically inferred from `html`, `html-spa`, or `lcov` coverage reporters
* **CLI:** `--coverage.htmlDir=`
Directory of HTML coverage output to be served in [Vitest UI](/guide/ui) and [HTML reporter](/guide/reporters.html#html-reporter).
This is automatically configured when using builtin coverage reporters that produce HTML output (`html`, `html-spa`, and `lcov`). Use this option to override with a custom coverage reporting location when using custom coverage reporters.
Note that setting this option does not change where coverage HTML report is generated. Configure the `coverage.reporter` option to change the directory instead.
## coverage.changed
* **Type:** `boolean | string`
* **Default:** `false` (inherits from `test.changed`)
* **Available for providers:** `'v8' | 'istanbul'`
* **CLI:** `--coverage.changed`, `--coverage.changed=`
Collect coverage only for files changed since a specified commit or branch. When set to `true`, it uses staged and unstaged changes.
## coverage.autoAttachSubprocess 5.0.0 {#coverage-autoattachsubprocess}
* **Type:** `boolean`
* **Default:** `false`
* **Available for providers:** `'v8'`
* **CLI:** `--coverage.autoAttachSubprocess`
Track coverage of the `node:child_process` and `node:worker_threads` spawned during test run.
Note that this option has some performance overhead as its using [`NODE_V8_COVERAGE`](https://nodejs.org/api/cli.html#node-v8-coveragedir) internally. This triggers Node to write lots of unnecessary files on file system.
---
---
url: /config/testnamepattern.md
---
# testNamePattern {#testnamepattern}
* **Type:** `string | RegExp`
* **CLI:** `-t `, `--testNamePattern=`, `--test-name-pattern=`
Run tests with full names matching the pattern.
If you add `OnlyRunThis` to this property, tests not containing the word `OnlyRunThis` in the test name will be skipped.
```js
import { expect, test } from 'vitest'
// run
test('OnlyRunThis', () => {
expect(true).toBe(true)
})
// skipped
test('doNotRun', () => {
expect(true).toBe(true)
})
```
The pattern is matched against the test's full name: the enclosing suite names and the test name joined with `' > '` (the same string shown in the reporter output). For example, the test below has the full name `math > adds`, so it is matched by `-t 'math > adds'` or `-t adds`:
```js
import { describe, expect, test } from 'vitest'
describe('math', () => {
test('adds', () => {
expect(1 + 1).toBe(2)
})
})
```
::: warning
Before Vitest 5, the segments were joined with a single space (`math adds`) to mirror Jest. See the [migration guide](/guide/migration#vitest-5) for details.
:::
---
---
url: /config/ui.md
---
# ui
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--ui`, `--ui=false`
Enable [Vitest UI](/guide/ui).
::: warning
This features requires a [`@vitest/ui`](https://npmx.dev/package/@vitest/ui) package to be installed. If you do not have it already, Vitest will install it when you run the test command for the first time.
:::
::: danger SECURITY ADVICE
Make sure that your UI server is not exposed to the network. Since Vitest 4.1 setting [`api.host`](/config/api) to anything other than `localhost` will disable the buttons to save the code or run any tests for security reasons, effectively making UI a readonly reporter.
:::
---
---
url: /config/open.md
---
# open
* **Type:** `boolean`
* **Default:** `!process.env.CI`
* **CLI:** `--open`, `--open=false`
Open Vitest UI automatically if it's [enabled](/config/ui).
---
---
url: /config/api.md
---
# api
* **Type:** `boolean | number | object`
* **Default:** `false`
* **CLI:** `--api`, `--api.port`, `--api.host`, `--api.strictPort`
Listen to port and serve API for [the UI](/guide/ui) or [browser server](/guide/browser/). When set to `true`, the default port is `51204` or `63315` if running in Browser Mode.
## api.allowWrite 4.1.0 {#api-allowwrite}
* **Type:** `boolean`
* **Default:** `true` if not exposed to the network, `false` otherwise
Vitest server can save test files or snapshot files via the API. This allows anyone who can connect to the API the ability to run any arbitrary code on your machine.
In Browser Mode Vitest saves [annotation attachments](/guide/test-annotations), [artifacts](/api/advanced/artifacts) and [snapshots](/guide/snapshot) by receiving a WebSocket connection from the browser. This allows anyone who can connect to the API write any arbitrary code on your machine within the root of your project (configured by [`fs.allow`](https://vite.dev/config/server-options#server-fs-allow)). This option also gates privileged browser APIs that can write files indirectly, such as raw Chrome DevTools Protocol access through [`cdp()`](/api/browser/context#cdp).
::: danger SECURITY ADVICE
Vitest does not expose the API to the internet by default and only listens on `localhost`. However if `host` is manually exposed to the network, anyone who connects to it can run arbitrary code on your machine, unless `api.allowWrite` and `api.allowExec` are set to `false`.
If the host is set to anything other than `localhost` or `127.0.0.1`, Vitest will set `api.allowWrite` and `api.allowExec` to `false` by default. This means that any write operations (like changing the code in the UI) will not work. However, if you understand the security implications, you can override them.
:::
## api.allowExec 4.1.0 {#api-allowexec}
* **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).
---
---
url: /config/clearmocks.md
---
# clearMocks
* **Type:** `boolean`
* **Default:** `true`
Should Vitest automatically call [`vi.clearAllMocks()`](/api/vi#vi-clearallmocks) before each test.
This will clear mock history without affecting mock implementations.
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
clearMocks: false,
},
})
```
::: warning
Be aware that this option may cause problems with async [concurrent tests](/api/test#test-concurrent). If enabled, the completion of one test will clear the mock history for all mocks, including those currently being used by other tests in progress.
:::
---
---
url: /config/mockreset.md
---
# mockReset
* **Type:** `boolean`
* **Default:** `false`
Should Vitest automatically call [`vi.resetAllMocks()`](/api/vi#vi-resetallmocks) before each test.
This will clear mock history and reset each implementation.
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
mockReset: true,
},
})
```
::: warning
Be aware that this option may cause problems with async [concurrent tests](/api/test#test-concurrent). If enabled, the completion of one test will clear the mock history and implementation for all mocks, including those currently being used by other tests in progress.
:::
---
---
url: /config/restoremocks.md
---
# restoreMocks
* **Type:** `boolean`
* **Default:** `false`
Should Vitest automatically call [`vi.restoreAllMocks()`](/api/vi#vi-restoreallmocks) before each test.
This restores all original implementations on spies created manually with [`vi.spyOn`](/api/vi#vi-spyon).
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
restoreMocks: true,
},
})
```
::: warning
Be aware that this option may cause problems with async [concurrent tests](/api/test#test-concurrent). If enabled, the completion of one test will restore the implementation for all spies, including those currently being used by other tests in progress.
:::
---
---
url: /config/unstubenvs.md
---
# unstubEnvs
* **Type:** `boolean`
* **Default:** `false`
Should Vitest automatically call [`vi.unstubAllEnvs()`](/api/vi#vi-unstuballenvs) before each test.
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
unstubEnvs: true,
},
})
```
::: warning
Be aware that this option may cause problems with async [concurrent tests](/api/test#test-concurrent). If enabled, the completion of one test will restore all the values changed with [`vi.stubEnv`](/api/vi#vi-stubenv), including those currently being used by other tests in progress.
:::
---
---
url: /config/unstubglobals.md
---
# unstubGlobals
* **Type:** `boolean`
* **Default:** `false`
Should Vitest automatically call [`vi.unstubAllGlobals()`](/api/vi#vi-unstuballglobals) before each test.
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
unstubGlobals: true,
},
})
```
::: warning
Be aware that this option may cause problems with async [concurrent tests](/api/test#test-concurrent). If enabled, the completion of one test will restore all global values that were changed with [`vi.stubGlobal`](/api/vi#vi-stubglobal), including those currently being used by other tests in progress.
:::
---
---
url: /config/snapshotformat.md
---
# snapshotFormat
* **Type:** `Omit & { compareKeys?: null | undefined }`
Format options for snapshot testing. These options configure the snapshot-specific formatting layer built on top of [`@vitest/pretty-format`](https://npmx.dev/package/@vitest/pretty-format).
For the full option surface of `PrettyFormatOptions`, see [`@vitest/pretty-format`](https://npmx.dev/package/@vitest/pretty-format). This page focuses on the Vitest snapshot-specific defaults and constraints.
Vitest snapshots already apply these defaults before your `snapshotFormat` overrides:
* `printBasicPrototype: false`
* `escapeString: false`
* `escapeRegex: true`
* `printFunctionName: false`
Vitest also supports formatter options such as `printShadowRoot` and `maxOutputLength` in `snapshotFormat`.
`printShadowRoot` controls whether shadow-root contents are included in DOM snapshots.
`maxOutputLength` is an approximate per-depth output budget, not a hard cap on the final rendered string.
By default, snapshot keys are sorted using the formatter's default behavior. Set `compareKeys` to `null` to disable key sorting. Custom compare functions are not supported in `snapshotFormat`.
::: tip
Beware that `plugins` on this object will be ignored.
If you need to extend snapshot serialization via pretty-format plugins, use [`expect.addSnapshotSerializer`](/api/expect#expect-addsnapshotserializer) or [`snapshotSerializers`](/config/snapshotserializers) instead.
:::
---
---
url: /config/snapshotserializers.md
---
# snapshotSerializers
* **Type:** `string[]`
* **Default:** `[]`
A list of paths to snapshot serializer modules for snapshot testing, useful if you want add custom snapshot serializers. See [Custom Serializer](/guide/snapshot#custom-serializer) for more information.
---
---
url: /config/resolvesnapshotpath.md
---
# resolveSnapshotPath
* **Type:** `(testPath: string, snapExtension: string, context: { config: SerializedConfig }) => string`
* **Default:** stores snapshot files in `__snapshots__` directory
Overrides default snapshot path. For example, to store snapshots next to test files:
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
resolveSnapshotPath: (testPath, snapExtension) => testPath + snapExtension,
},
})
```
You can also use the `context` parameter to access the project's serialized config. This is useful when you have multiple [projects](/guide/projects) and want to store snapshots in different locations based on the project name:
```ts
import { basename, dirname, join } from 'node:path'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
resolveSnapshotPath(testPath, snapExtension, context) {
return join(
dirname(testPath),
'__snapshots__',
context.config.name ?? 'default',
basename(testPath) + snapExtension,
)
},
},
})
```
---
---
url: /config/allowonly.md
---
# allowOnly
* **Type:** `boolean`
* **Default:** `!process.env.CI`
* **CLI:** `--allowOnly`, `--allowOnly=false`
By default, Vitest does not permit tests marked with the [`only`](/api/test#test-only) flag in Continuous Integration (CI) environments. Conversely, in local development environments, Vitest allows these tests to run.
::: info
Vitest uses [`std-env`](https://npmx.dev/package/std-env) package to detect the environment.
:::
You can customize this behavior by explicitly setting the `allowOnly` option to either `true` or `false`.
::: code-group
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
allowOnly: true,
},
})
```
```bash [CLI]
vitest --allowOnly
```
:::
When enabled, Vitest will not fail the test suite if tests marked with [`only`](/api/test#test-only) are detected, including in CI environments.
When disabled, Vitest will fail the test suite if tests marked with [`only`](/api/test#test-only) are detected, including in local development environments.
---
---
url: /config/passwithnotests.md
---
# passWithNoTests
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--passWithNoTests`, `--passWithNoTests=false`
Vitest will not fail, if no tests will be found.
---
---
url: /config/logheapusage.md
---
# logHeapUsage
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--logHeapUsage`, `--logHeapUsage=false`
Show heap usage after each test. Useful for debugging memory leaks.
---
---
url: /config/css.md
---
# css
* **Type:** `boolean | { include?, exclude?, modules? }`
Configure if CSS should be processed. When excluded, CSS files will be replaced with empty strings to bypass the subsequent processing. CSS Modules will return a proxy to not affect runtime.
::: warning
This option is not applied to [browser tests](/guide/browser/).
:::
## css.include
* **Type:** `RegExp | RegExp[]`
* **Default:** `[]`
RegExp pattern for files that should return actual CSS and will be processed by Vite pipeline.
:::tip
To process all CSS files, use `/.+/`.
:::
## css.exclude
* **Type:** `RegExp | RegExp[]`
* **Default:** `[]`
RegExp pattern for files that will return an empty CSS file.
## css.modules
* **Type:** `{ classNameStrategy? }`
* **Default:** `{}`
### css.modules.classNameStrategy
* **Type:** `'stable' | 'scoped' | 'non-scoped'`
* **Default:** `'stable'`
If you decide to process CSS files, you can configure if class names inside CSS modules should be scoped. You can choose one of the options:
* `stable`: class names will be generated as `_${name}_${hashedFilename}`, which means that generated class will stay the same, if CSS content is changed, but will change, if the name of the file is modified, or file is moved to another folder. This setting is useful, if you use snapshot feature.
* `scoped`: class names will be generated as usual, respecting `css.modules.generateScopedName` method, if you have one and CSS processing is enabled. By default, filename will be generated as `_${name}_${hash}`, where hash includes filename and content of the file.
* `non-scoped`: class names will not be hashed.
::: warning
By default, Vitest exports a proxy, bypassing CSS Modules processing. If you rely on CSS properties on your classes, you have to enable CSS processing using `include` option.
:::
---
---
url: /config/maxconcurrency.md
---
# maxConcurrency
* **Type:** `number`
* **Default:** `5`
* **CLI:** `--max-concurrency=10`, `--maxConcurrency=10`
The maximum number of tests and hooks that can run at the same time when using `test.concurrent` or `describe.concurrent`.
The hook execution order within a single group is also controlled by [`sequence.hooks`](/config/sequence#sequence-hooks). With `sequence.hooks: 'parallel'`, the execution is bounded by the same limit of [`maxConcurrency`](/config/maxconcurrency).
---
---
url: /config/cache.md
---
# cache
* **Type:** `false`
* **CLI:** `--no-cache`, `--cache=false`
Use this option if you want to disable the cache feature. At the moment Vitest stores cache for test results to run the longer and failed tests first.
The cache directory is controlled by the Vite's [`cacheDir`](https://vitejs.dev/config/shared-options.html#cachedir) option:
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
cacheDir: 'custom-folder/.vitest'
})
```
You can limit the directory only for Vitest by using `process.env.VITEST`:
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
cacheDir: process.env.VITEST ? 'custom-folder/.vitest' : undefined
})
```
---
---
url: /config/fsmodulecache.md
---
# fsModuleCache 5.0.0
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--fsModuleCache`, `--fsModuleCache=false`
In watch mode, Vitest caches all transformed files in memory, which makes reruns fast. However, this cache is discarded once the test run finishes. Enabling this option allows Vitest to persist the transformed modules on the file system, so they can be reused across reruns and separate Vitest processes.
A single cache directory is shared by every project in the workspace. By default it lives in `node_modules` at the workspace root (so it is naturally invalidated when dependencies are reinstalled); use [`fsModuleCachePath`](/config/fsmodulecachepath) to change its location. You can delete the cache by running [`vitest --clearCache`](/guide/cli#clearcache).
::: warning BROWSER SUPPORT
At the moment, this option does not affect [the browser](/guide/browser/).
:::
You can debug if your modules are cached by running vitest with a `DEBUG=vitest:cache:fs` environment variable:
```shell
DEBUG=vitest:cache:fs vitest --fsModuleCache
```
::: tip
The location of the cache is a single, workspace-wide directory. See [`fsModuleCachePath`](/config/fsmodulecachepath) to move it.
:::
## Known Issues
Vitest creates a persistent file hash based on file content, its id, Vite's environment configuration and coverage status. Vitest tries to use as much information as it has about the configuration, but it is still incomplete. At the moment, it is not possible to track your plugin options because there is no standard interface for it.
If you have a plugin that relies on things outside the file content or the public configuration (like reading another file or a folder), it's possible that the cache will get stale. To work around that, you can define a [cache key generator](/api/advanced/plugin#definecachekeygenerator) to specify a dynamic option or to opt out of caching for that module:
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [
{
name: 'vitest-cache',
configureVitest({ defineCacheKeyGenerator }) {
defineCacheKeyGenerator(({ id, sourceCode }) => {
// never cache this id
if (id.includes('do-not-cache')) {
return false
}
// cache this file based on the value of a dynamic variable
if (sourceCode.includes('myDynamicVar')) {
return process.env.DYNAMIC_VAR_VALUE
}
})
}
}
],
test: {
fsModuleCache: true,
},
})
```
If you are a plugin author, consider defining a [cache key generator](/api/advanced/plugin#definecachekeygenerator) in your plugin if it can be registered with different options that affect the transform result.
On the other hand, if your plugin should not affect the cache key, you can opt out by setting `api.vitest.ignoreFsModuleCache` to `true`:
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [
{
name: 'vitest-cache',
api: {
vitest: {
ignoreFsModuleCache: true,
},
},
},
],
test: {
fsModuleCache: true,
},
})
```
Note that you can still define the cache key generator even if the plugin opts out of module caching.
---
---
url: /config/fsmodulecachepath.md
---
# fsModuleCachePath 5.0.0
* **Type:** `string`
* **Default:** `'node_modules/.vitest-cache'` (resolved from the workspace root)
* **CLI:** `--fsModuleCachePath=`
Directory where the [`fsModuleCache`](/config/fsmodulecache) is stored.
This can be set per project; projects that don't override it fall back to the root's cache directory. The lockfile metadata used to invalidate the cache is always shared across the whole workspace.
By default Vitest stores the cache inside `node_modules` at the workspace root. The root is based on your package manager's lockfile (for example, `.package-lock.json`, `.yarn-state.yml`, `.pnpm/lock.yaml` and so on). Keeping it inside `node_modules` means the cache is naturally invalidated whenever dependencies are reinstalled.
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
fsModuleCache: true,
fsModuleCachePath: 'node_modules/.vitest-cache',
},
})
```
---
---
url: /config/sequence.md
---
# sequence
* **Type:** `{ sequencer?, shuffle?, seed?, hooks?, setupFiles?, groupOrder }`
Options for how tests should be sorted.
You can provide sequence options to CLI with dot notation:
```sh
npx vitest --sequence.shuffle --sequence.seed=1000
```
## sequence.sequencer
* **Type:** `TestSequencerConstructor`
* **Default:** `BaseSequencer`
A custom class that defines methods for sharding and sorting. You can extend `BaseSequencer` from `vitest/node`, if you only need to redefine one of the `sort` and `shard` methods, but both should exist.
Sharding is happening before sorting, and only if `--shard` option is provided.
If [`sequence.groupOrder`](#sequence-grouporder) is specified, the sequencer will be called once for each group and pool.
## sequence.groupOrder
* **Type:** `number`
* **Default:** `0`
Controls the order in which this project runs its tests when using multiple [projects](/guide/projects).
* Projects with the same group order number will run together, and groups are run from lowest to highest.
* If you don't set this option, all projects run in parallel.
* If several projects use the same group order, they will run at the same time.
This setting only affects the order in which projects run, not the order of tests within a project.
To control test isolation or the order of tests inside a project, use the [`isolate`](/config/isolate) and [`sequence.sequencer`](/config/sequence#sequence-sequencer) options.
::: details Example
Consider this example:
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
{
test: {
name: 'slow',
sequence: {
groupOrder: 0,
},
},
},
{
test: {
name: 'fast',
sequence: {
groupOrder: 0,
},
},
},
{
test: {
name: 'flaky',
sequence: {
groupOrder: 1,
},
},
},
],
},
})
```
Tests in these projects will run in this order:
```
0. slow |
|> running together
0. fast |
1. flaky |> runs after slow and fast alone
```
:::
## sequence.shuffle
* **Type:** `boolean | { files?, tests? }`
* **Default:** `false`
* **CLI:** `--sequence.shuffle`, `--sequence.shuffle=false`
If you want files and tests to run randomly, you can enable it with this option, or CLI argument [`--sequence.shuffle`](/guide/cli).
Vitest usually uses cache to sort tests, so long-running tests start earlier, which makes tests run faster. If your files and tests run in random order, you will lose this performance improvement, but it may be useful to track tests that accidentally depend on another test run previously.
### sequence.shuffle.files {#sequence-shuffle-files}
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--sequence.shuffle.files`, `--sequence.shuffle.files=false`
Whether to randomize files, be aware that long running tests will not start earlier if you enable this option.
Because file ordering is shared across [projects](/guide/projects), this option is resolved from the root config only. A project can still randomize its own tests with [`sequence.shuffle.tests`](#sequence-shuffle-tests).
### sequence.shuffle.tests {#sequence-shuffle-tests}
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--sequence.shuffle.tests`, `--sequence.shuffle.tests=false`
Whether to randomize tests.
## sequence.concurrent {#sequence-concurrent}
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--sequence.concurrent`, `--sequence.concurrent=false`
If you want tests to run in parallel, you can enable it with this option, or CLI argument [`--sequence.concurrent`](/guide/cli).
::: warning
When you run tests with `sequence.concurrent` and `expect.requireAssertions` set to `true`, you should use [local expect](/guide/test-context.html#expect) instead of the global one. Otherwise, this may cause false negatives in [some situations (#8469)](https://github.com/vitest-dev/vitest/issues/8469).
:::
## sequence.seed
* **Type:** `number`
* **Default:** `Date.now()`
* **CLI:** `--sequence.seed=1000`
Sets the randomization seed, if tests are running in random order.
## sequence.hooks
* **Type:** `'stack' | 'list' | 'parallel'`
* **Default:** `'stack'`
* **CLI:** `--sequence.hooks=`
Changes the order in which hooks are executed.
* `stack` will order "after" hooks in reverse order, "before" hooks will run in the order they were defined
* `list` will order all hooks in the order they are defined
* `parallel` runs hooks in a single group in parallel (hooks in parent suites still run before the current suite's hooks). The actual number of simultaneously running hooks is limited by [`maxConcurrency`](/config/maxconcurrency).
::: tip
This option doesn't affect [`onTestFinished`](/api/hooks#ontestfinished). It is always called in reverse order.
:::
## sequence.setupFiles {#sequence-setupfiles}
* **Type:** `'list' | 'parallel'`
* **Default:** `'parallel'`
* **CLI:** `--sequence.setupFiles=`
Changes the order in which setup files are executed.
* `list` will run setup files in the order they are defined
* `parallel` will run setup files in parallel
---
---
url: /config/tags.md
---
# tags 4.1.0 {#tags}
* **Type:** `TestTagDefinition[]`
* **Default:** `[]`
Defines all [available tags](/guide/test-tags) in your test project. By default, if test defines a name not listed here, Vitest will throw an error, but this can be configured via a [`strictTags`](/config/stricttags) option.
If you are using [`projects`](/config/projects), they will inherit all global tags definitions automatically.
Use [`--tags-filter`](/guide/test-tags#syntax) to filter tests by their tags. Use [`--list-tags`](/guide/cli#listtags) to print every tag in your Vitest workspace.
## name
* **Type:** `string`
* **Required:** `true`
The name of the tag. This is what you use in the `tags` option in tests.
```ts
export default defineConfig({
test: {
tags: [
{ name: 'unit' },
{ name: 'e2e' },
],
},
})
```
::: tip
If you are using TypeScript, you can enforce what tags are available by augmenting the `TestTags` type with a property that contains a union of strings (make sure this file is included by your `tsconfig`):
```ts [vitest.shims.ts]
import 'vitest'
declare module 'vitest' {
interface TestTags {
tags:
| 'frontend'
| 'backend'
| 'db'
| 'flaky'
}
}
```
:::
## description
* **Type:** `string`
A human-readable description for the tag. This will be shown in UI and inside error messages when a tag is not found.
```ts
export default defineConfig({
test: {
tags: [
{
name: 'slow',
description: 'Tests that take a long time to run.',
},
],
},
})
```
## priority
* **Type:** `number`
* **Default:** `Infinity`
Priority for merging options when multiple tags with the same options are applied to a test. Lower number means higher priority (e.g., priority `1` takes precedence over priority `3`).
```ts
export default defineConfig({
test: {
tags: [
{
name: 'flaky',
timeout: 30_000,
priority: 1, // higher priority
},
{
name: 'db',
timeout: 60_000,
priority: 2, // lower priority
},
],
},
})
```
When a test has both tags, the `timeout` will be `30_000` because `flaky` has a higher priority.
## Test Options
Tags can define [test options](/api/test#test-options) that will be applied to every test marked with the tag. These options are merged with the test's own options, with the test's options taking precedence.
::: warning
The [`retry.condition`](/api/test#retry) can only be a regexp because the config values need to be serialised.
Tags also cannot apply other [tags](/api/test#tags) via these options.
:::
## Example
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
tags: [
{
name: 'unit',
description: 'Unit tests.',
},
{
name: 'e2e',
description: 'End-to-end tests.',
timeout: 60_000,
},
{
name: 'flaky',
description: 'Flaky tests that need retries.',
retry: process.env.CI ? 3 : 0,
priority: 1,
},
{
name: 'slow',
description: 'Slow tests.',
timeout: 120_000,
},
{
name: 'skip-ci',
description: 'Tests to skip in CI.',
skip: !!process.env.CI,
},
],
},
})
```
---
---
url: /config/stricttags.md
---
# strictTags 4.1.0 {#stricttags}
* **Type:** `boolean`
* **Default:** `true`
* **CLI:** `--strict-tags`, `--no-strict-tags`
Should Vitest throw an error if test has a [`tag`](/config/tags) that is not defined in the config to avoid silently doing something surprising due to mistyped names (applying the wrong configuration or skipping the test due to a `--tags-filter` flag).
Note that Vitest will always throw an error if `--tags-filter` flag defines a tag not present in the config.
For example, this test will throw an error because the tag `fortnend` has a typo (it should be `frontend`):
::: code-group
```js [form.test.js]
test('renders a form', { tags: ['fortnend'] }, () => {
// ...
})
```
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
tags: [
{ name: 'frontend' },
],
},
})
```
:::
---
---
url: /config/typecheck.md
---
# typecheck {#typecheck}
Options for configuring [typechecking](/guide/testing-types) test environment.
## typecheck.enabled {#typecheck-enabled}
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--typecheck`, `--typecheck.enabled`
Enable typechecking alongside your regular tests.
## typecheck.only {#typecheck-only}
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--typecheck.only`
Run only typecheck tests, when typechecking is enabled. When using CLI, this option will automatically enable typechecking.
## typecheck.checker
* **Type:** `'tsc' | 'vue-tsc' | string`
* **Default:** `tsc`
What tools to use for type checking. Vitest will spawn a process with certain parameters for easier parsing, depending on the type. Checker should implement the same output format as `tsc`.
You need to have a package installed to use typechecker:
* `tsc` requires `typescript` package
* `vue-tsc` requires `vue-tsc` package
You can also pass down a path to custom binary or command name that produces the same output as `tsc --noEmit --pretty false`.
## typecheck.include
* **Type:** `string[]`
* **Default:** `['**/*.{test,spec}-d.?(c|m)[jt]s?(x)']`
Glob pattern for files that should be treated as test files.
## typecheck.exclude
* **Type:** `string[]`
* **Default:** `['**/node_modules/**', '**/dist/**', '**/cypress/**', '**/.{idea,git,cache,output,temp}/**']`
Glob pattern for files that should not be treated as test files.
## typecheck.allowJs
* **Type:** `boolean`
* **Default:** `false`
Check JS files that have `@ts-check` comment. If you have it enabled in tsconfig, this will not overwrite it.
## typecheck.ignoreSourceErrors
* **Type:** `boolean`
* **Default:** `false`
Do not fail, if Vitest found errors outside the test files. This will not show you non-test errors at all.
By default, if Vitest finds source error, it will fail test suite.
## typecheck.tsconfig
* **Type:** `string`
* **Default:** *tries to find closest tsconfig.json*
Path to custom tsconfig, relative to the project root.
## typecheck.spawnTimeout
* **Type:** `number`
* **Default:** `10_000`
Minimum time in milliseconds it takes to spawn the typechecker.
---
---
url: /config/slowtestthreshold.md
---
# slowTestThreshold
* **Type:** `number`
* **Default:** `300`
* **CLI:** `--slow-test-threshold=`, `--slowTestThreshold=`
The number of milliseconds after which a test or suite is considered slow and reported as such in the results.
---
---
url: /config/chaiconfig.md
---
# chaiConfig
* **Type:** `{ includeStack?, showDiff?, truncateThreshold? }`
* **Default:** `{ includeStack: false, showDiff: true, truncateThreshold: 40 }`
Equivalent to [Chai config](https://github.com/chaijs/chai/blob/4.x.x/lib/chai/config.js).
## chaiConfig.includeStack
* **Type:** `boolean`
* **Default:** `false`
Influences whether stack trace is included in Assertion error message. Default of false suppresses stack trace in the error message.
## chaiConfig.showDiff
* **Type:** `boolean`
* **Default:** `true`
Influences whether or not the `showDiff` flag should be included in the thrown AssertionErrors. `false` will always be `false`; `true` will be true when the assertion has requested a diff to be shown.
## chaiConfig.truncateThreshold
* **Type:** `number`
* **Default:** `40`
Sets length threshold for actual and expected values in assertion error messages. If this threshold is exceeded, for example for large data structures, the value is replaced with something like `[ Array(3) ]` or `{ Object (prop1, prop2) }`. Set it to `0` if you want to disable truncating altogether.
---
---
url: /config/bail.md
---
# bail
* **Type:** `number`
* **Default:** `0`
* **CLI:** `--bail=`
Stop test execution when given number of tests have failed.
By default Vitest will run all of your test cases even if some of them fail. This may not be desired for CI builds where you are only interested in 100% successful builds and would like to stop test execution as early as possible when test failures occur. The `bail` option can be used to speed up CI runs by preventing it from running more tests when failures have occurred.
---
---
url: /config/retry.md
---
# retry
Retry the test specific number of times if it fails.
* **Type:** `number | { count?: number, delay?: number, condition?: RegExp }`
* **Default:** `0`
* **CLI:** `--retry `, `--retry.count `, `--retry.delay `, `--retry.condition `
## Basic Usage
Specify a number to retry failed tests:
```ts
export default defineConfig({
test: {
retry: 3,
},
})
```
## CLI Usage
You can also configure retry options from the command line:
```bash
# Simple retry count
vitest --retry 3
# Advanced options using dot notation
vitest --retry.count 3 --retry.delay 500 --retry.condition 'ECONNREFUSED|timeout'
```
## Advanced Options 4.1.0 {#advanced-options}
Use an object to configure retry behavior:
```ts
export default defineConfig({
test: {
retry: {
count: 3, // Number of times to retry
delay: 1000, // Delay in milliseconds between retries
condition: /ECONNREFUSED|timeout/i, // RegExp to match errors that should trigger retry
},
},
})
```
### count
Number of times to retry a test if it fails. Default is `0`.
```ts
export default defineConfig({
test: {
retry: {
count: 2,
},
},
})
```
### delay
Delay in milliseconds between retry attempts. Useful for tests that interact with rate-limited APIs or need time to recover. Default is `0`.
```ts
export default defineConfig({
test: {
retry: {
count: 3,
delay: 500, // Wait 500ms between retries
},
},
})
```
### condition
A RegExp pattern or a function to determine if a test should be retried based on the error.
* When a **RegExp**, it's tested against the error message
* When a **function**, it receives the error and returns a boolean
::: warning
When defining `condition` as a function, it must be done in a test file directly, not in a configuration file (configurations are serialized for worker threads).
:::
#### RegExp condition (in config file):
```ts
export default defineConfig({
test: {
retry: {
count: 2,
condition: /ECONNREFUSED|ETIMEDOUT/i, // Retry on connection/timeout errors
},
},
})
```
#### Function condition (in test file):
```ts
import { describe, test } from 'vitest'
describe('tests with advanced retry condition', () => {
test('with function condition', { retry: { count: 2, condition: error => error.message.includes('Network') } }, () => {
// test code
})
})
```
## Test File Override
You can also define retry options per test or suite in test files:
```ts
import { describe, test } from 'vitest'
describe('flaky tests', {
retry: {
count: 2,
delay: 100,
},
}, () => {
test('network request', () => {
// test code
})
})
test('another test', {
retry: {
count: 3,
condition: error => error.message.includes('timeout'),
},
}, () => {
// test code
})
```
---
---
url: /config/repeats.md
---
# repeats
* **Type:** `number`
* **Default:** `0`
* **CLI:** `--repeats=`
Repeat every test a specific number of times regardless of the result. A test that uses the [`repeats`](/api/test#repeats) test option takes precedence over this value.
This is useful for verifying that tests are stable across multiple runs. If a test fails on any repetition, the whole test is reported as failed.
---
---
url: /config/onconsolelog.md
---
# onConsoleLog
```ts
function onConsoleLog(
log: string,
type: 'stdout' | 'stderr',
entity: TestModule | TestSuite | TestCase | undefined,
): boolean | void
```
Custom handler for `console` methods in tests. If you return `false`, Vitest will not print the log to the console. Note that Vitest ignores all other falsy values.
Can be useful for filtering out logs from third-party libraries.
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
return !(log === 'message from third party library' && type === 'stdout')
},
},
})
```
---
---
url: /config/onstacktrace.md
---
# onStackTrace
* **Type:** `(error: Error, frame: ParsedStack) => boolean | void`
Apply a filtering function to each frame of each stack trace when handling errors. This does not apply to stack traces printed by [`printConsoleTrace`](/config/printconsoletrace#printconsoletrace). The first argument, `error`, is a `TestError`.
Can be useful for filtering out stack trace frames from third-party libraries.
::: tip
The stack trace's total size is also typically limited by V8's [`Error.stackTraceLimit`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stackTraceLimit) number. You could set this to a high value in your test setup function to prevent stacks from being truncated.
:::
```ts
import type { ParsedStack, TestError } from 'vitest'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
onStackTrace(error: TestError, { file }: ParsedStack): boolean | void {
// If we've encountered a ReferenceError, show the whole stack.
if (error.name === 'ReferenceError') {
return
}
// Reject all frames from third party libraries.
if (file.includes('node_modules')) {
return false
}
},
},
})
```
---
---
url: /config/onunhandlederror.md
---
# onUnhandledError 4.0.0
* **Type:**
```ts
function onUnhandledError(
error: (TestError | Error) & { type: string }
): boolean | void
```
A custom callback for filtering unhandled errors that should not be reported. When an error is filtered out, it no longer affects the result of the test run.
To report unhandled errors without affecting the test outcome, use the [`dangerouslyIgnoreUnhandledErrors`](/config/dangerouslyignoreunhandlederrors) option instead.
::: tip
This callback is called on the main thread, it doesn't have access to your test context.
:::
## Example
```ts
import type { ParsedStack } from 'vitest'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
onUnhandledError(error): boolean | void {
// Ignore all errors with the name "MySpecialError".
if (error.name === 'MySpecialError') {
return false
}
},
},
})
```
---
---
url: /config/dangerouslyignoreunhandlederrors.md
---
# dangerouslyIgnoreUnhandledErrors
* **Type:** `boolean`
* **Default:** `false`
* **CLI:**
* `--dangerouslyIgnoreUnhandledErrors`
* `--dangerouslyIgnoreUnhandledErrors=false`
If this option is set to `true`, Vitest will not fail the test run if there are unhandled errors. Note that built-in reporters will still report them.
If you want to filter out certain errors conditionally, use [`onUnhandledError`](/config/onunhandlederror) callback instead.
## Example
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
dangerouslyIgnoreUnhandledErrors: true,
},
})
```
---
---
url: /config/diff.md
---
# diff
* **Type:** `string`
* **CLI:** `--diff=`
`DiffOptions` object or a path to a module which exports `DiffOptions`. Useful if you want to customize diff display.
Vitest diff rendering uses [`@vitest/pretty-format`](https://npmx.dev/package/@vitest/pretty-format) under the hood and a part of `DiffOptions` is forwarded to the pretty-format configuration, while the rest affects diff rendering itself.
For example, as a config object:
```ts
import { defineConfig } from 'vitest/config'
import c from 'picocolors'
export default defineConfig({
test: {
diff: {
aIndicator: c.bold('--'),
bIndicator: c.bold('++'),
omitAnnotationLines: true,
},
},
})
```
Or as a module:
:::code-group
```ts [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
diff: './vitest.diff.ts',
},
})
```
```ts [vitest.diff.ts]
import type { DiffOptions } from 'vitest'
import c from 'picocolors'
export default {
aIndicator: c.bold('--'),
bIndicator: c.bold('++'),
omitAnnotationLines: true,
} satisfies DiffOptions
```
:::
## diff.expand
* **Type:** `boolean`
* **Default:** `true`
* **CLI:** `--diff.expand=false`
Expand all common lines.
## diff.truncateThreshold
* **Type:** `number`
* **Default:** `0`
* **CLI:** `--diff.truncateThreshold=`
The maximum length of diff result to be displayed. Diffs above this threshold will be truncated.
Truncation won't take effect with default value 0.
## diff.truncateAnnotation
* **Type:** `string`
* **Default:** `'... Diff result is truncated'`
* **CLI:** `--diff.truncateAnnotation=`
Annotation that is output at the end of diff result if it's truncated.
## diff.truncateAnnotationColor
* **Type:** `DiffOptionsColor = (arg: string) => string`
* **Default:** `noColor = (string: string): string => string`
Color of truncate annotation, default is output with no color.
## diff.printBasicPrototype
* **Type:** `boolean`
* **Default:** `false`
Print basic prototype `Object` and `Array` in diff output.
## diff.maxDepth
* **Type:** `number`
* **Default:** `20` (or `8` when comparing different types)
Limit the depth to recurse when printing nested objects.
---
---
url: /config/faketimers.md
---
# fakeTimers
* **Type:** `FakeTimerConfig`
Options that Vitest will pass down to [`@sinon/fake-timers`](https://npmx.dev/package/@sinonjs/fake-timers) when using [`vi.useFakeTimers()`](/api/vi#vi-usefaketimers).
## fakeTimers.now
* **Type:** `number | Date`
* **Default:** `Date.now()`
Installs fake timers with the specified Unix epoch.
## fakeTimers.toFake
* **Type:** `('setTimeout' | 'clearTimeout' | 'setImmediate' | 'clearImmediate' | 'setInterval' | 'clearInterval' | 'Date' | 'nextTick' | 'hrtime' | 'requestAnimationFrame' | 'cancelAnimationFrame' | 'requestIdleCallback' | 'cancelIdleCallback' | 'performance' | 'queueMicrotask' | 'Intl' | 'Temporal')[]`
* **Default:** everything available globally except `nextTick` and `queueMicrotask`
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']`.
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
* **Type:** `('setTimeout' | 'clearTimeout' | 'setImmediate' | 'clearImmediate' | 'setInterval' | 'clearInterval' | 'Date' | 'nextTick' | 'hrtime' | 'requestAnimationFrame' | 'cancelAnimationFrame' | 'requestIdleCallback' | 'cancelIdleCallback' | 'performance' | 'queueMicrotask' | 'Intl' | 'Temporal')[]`
* **Default:** `[]`
An array with names of global methods and APIs to keep native. All other available timers will be mocked. For example, to keep `setInterval()` native and mock all other timers, specify this property as `['setInterval']`.
Mocking `nextTick` is not supported when running Vitest inside `node:child_process` by using `--pool=forks`. When running with `--pool=forks`, Vitest automatically adds `nextTick` to the `toNotFake` array.
::: warning
Using both `toFake` and `toNotFake` together is not supported.
:::
## fakeTimers.loopLimit
* **Type:** `number`
* **Default:** `10_000`
The maximum number of timers that will be run when calling [`vi.runAllTimers()`](/api/vi#vi-runalltimers).
## fakeTimers.shouldAdvanceTime
* **Type:** `boolean`
* **Default:** `false`
Tells @sinonjs/fake-timers to increment mocked time automatically based on the real system time shift (e.g. the mocked time will be incremented by 20ms for every 20ms change in the real system time).
## fakeTimers.advanceTimeDelta
* **Type:** `number`
* **Default:** `20`
Relevant only when using with `shouldAdvanceTime: true`. increment mocked time by advanceTimeDelta ms every advanceTimeDelta ms change in the real system time.
## fakeTimers.shouldClearNativeTimers
* **Type:** `boolean`
* **Default:** `true`
Tells fake timers to clear "native" (i.e. not fake) timers by delegating to their respective handlers. When disabled, it can lead to potentially unexpected behavior if timers existed prior to starting fake timers session.
---
---
url: /config/projects.md
---
# projects
* **Type:** `TestProjectConfiguration[]`
* **Default:** `[]`
An array of [projects](/guide/projects).
A config file that declares `projects` doesn't run tests itself, it only provides the projects that do. This also applies to project config files: a referenced config that declares `projects` becomes a container for [nested projects](/guide/projects#nested-projects). The option is not supported inside an inline project configuration.
---
---
url: /config/isolate.md
---
# isolate
* **Type:** `boolean`
* **Default:** `true`
* **CLI:** `--no-isolate`, `--isolate=false`
Run tests in an isolated environment. This option has no effect on `vmThreads` and `vmForks` pools.
Disabling this option might [improve performance](/guide/improving-performance) if your code doesn't rely on side effects (which is usually true for projects with `node` environment).
::: tip
You can disable isolation for specific test files by using Vitest workspaces and disabling isolation per project.
:::
---
---
url: /config/includetasklocation.md
---
# includeTaskLocation
* **Type:** `boolean`
* **Default:** `false`
Should `location` property be included when Vitest API receives tasks in [reporters](/config/reporters). If you have a lot of tests, this might cause a small performance regression.
The `location` property has `column` and `line` values that correspond to the `test` or `describe` position in the original file.
This option will be auto-enabled if you don't disable it explicitly, and you are running Vitest with:
* [Vitest UI](/guide/ui)
* or using the [Browser Mode](/guide/browser/) without [headless](/guide/browser/#headless) mode
* or using [HTML Reporter](/guide/reporters#html-reporter)
::: tip
This option has no effect if you do not use custom code that relies on this.
:::
---
---
url: /config/snapshotenvironment.md
---
# snapshotEnvironment
* **Type:** `string`
Path to a custom snapshot environment implementation. This is useful if you are running your tests in an environment that doesn't support Node.js APIs. This option doesn't have any effect on a browser runner.
This object should have the shape of `SnapshotEnvironment` and is used to resolve and read/write snapshot files:
```ts
export interface SnapshotEnvironment {
getVersion: () => string
getHeader: () => string
resolvePath: (filepath: string) => Promise
resolveRawPath: (testPath: string, rawPath: string) => Promise
saveSnapshotFile: (filepath: string, snapshot: string) => Promise
readSnapshotFile: (filepath: string) => Promise
removeSnapshotFile: (filepath: string) => Promise
}
```
You can extend default `VitestSnapshotEnvironment` from `vitest/snapshot` entry point if you need to overwrite only a part of the API.
::: warning
This is a low-level option and should be used only for advanced cases where you don't have access to default Node.js APIs.
If you just need to configure snapshots feature, use [`snapshotFormat`](/config/snapshotformat) or [`resolveSnapshotPath`](/config/resolvesnapshotpath) options.
:::
---
---
url: /config/env.md
---
# env
* **Type:** `Partial`
Environment variables available on `process.env` and `import.meta.env` during tests. These variables will not be available in the main process (in `globalSetup`, for example).
---
---
url: /config/expect.md
---
# expect
* **Type:** `ExpectOptions`
## expect.requireAssertions
* **Type:** `boolean`
* **Default:** `false`
The same as calling [`expect.hasAssertions()`](/api/expect#expect-hasassertions) at the start of every test. This makes sure that no test will pass accidentally.
::: tip
This only works with Vitest's `expect`. If you use `assert` or `.should` assertions, they will not count, and your test will fail due to the lack of expect assertions.
You can change the value of this by calling `vi.setConfig({ expect: { requireAssertions: false } })`. The config will be applied to every subsequent `expect` call until the `vi.resetConfig` is called manually.
:::
::: warning
When you run tests with `sequence.concurrent` and `expect.requireAssertions` set to `true`, you should use [local expect](/guide/test-context.html#expect) instead of the global one. Otherwise, this may cause false negatives in [some situations (#8469)](https://github.com/vitest-dev/vitest/issues/8469).
:::
## expect.poll
Global configuration options for [`expect.poll`](/api/expect#poll). These are the same options you can pass down to `expect.poll(condition, options)`.
### expect.poll.interval
* **Type:** `number`
* **Default:** `50`
Polling interval in milliseconds
### expect.poll.timeout
* **Type:** `number`
* **Default:** `1000`
Polling timeout in milliseconds
---
---
url: /config/printconsoletrace.md
---
# printConsoleTrace
* **Type:** `boolean`
* **Default:** `false`
Always print console traces when calling any `console` method. This is useful for debugging.
---
---
url: /config/attachmentsdir.md
---
# attachmentsDir
* **Type:** `string`
* **Default:** `'.vitest/attachments'`
Directory path for storing file attachments created by [`context.annotate`](/guide/test-context#annotate).
This option is resolved relative to the root Vitest config. When using [`projects`](/guide/projects), all projects share the same `attachmentsDir`; it cannot be configured per project.
---
---
url: /config/hideskippedtests.md
---
# hideSkippedTests
* **Type:** `boolean`
* **CLI:** `--hideSkippedTests`, `--hide-skipped-tests`
* **Default:** `false`
Hide logs for skipped tests
---
---
url: /config/mode.md
---
# mode
* **Type:** `string`
* **CLI:** `--mode=staging`
* **Default:** `'test'`
Overrides Vite mode.
---
---
url: /config/expandsnapshotdiff.md
---
# expandSnapshotDiff
* **Type:** `boolean`
* **CLI:** `--expandSnapshotDiff`, `--expand-snapshot-diff`
* **Default:** `false`
Show full diff when snapshot fails instead of a patch.
---
---
url: /config/disableconsoleintercept.md
---
# disableConsoleIntercept
* **Type:** `boolean`
* **CLI:** `--disableConsoleIntercept`
* **Default:** `false`
By default, Vitest intercepts console output during tests to add context such as the test file and test title.
In [browser mode](/guide/browser/), this interception is required to forward logs from the browser DevTools to the terminal. It is also required for console log previews in the Vitest UI.
Disabling console interception can be useful when you want to debug code with normal synchronous terminal logging.
---
---
url: /config/changed.md
---
### changed
* **Type:** `boolean | string`
* **Default:** `false`
* **CLI:** `--changed`, `--changed=HEAD~1`
Run tests only against changed files. If no value is provided, it will run tests against uncommitted changes (including staged and unstaged).
To run tests against changes made in the last commit, you can use `--changed HEAD~1`. You can also pass commit hash (e.g. `--changed 09a9920`) or branch name (e.g. `--changed origin/develop`).
When used with code coverage the report will contain only the files that were related to the changes.
If paired with the [`forceRerunTriggers`](/config/forcereruntriggers) config option it will run the whole test suite if at least one of the files listed in the `forceRerunTriggers` list changes. By default, changes to the Vitest config file and `package.json` will always rerun the whole suite.
---
---
url: /config/experimental.md
---
# experimental
## experimental.openTelemetry 4.0.11 {#experimental-opentelemetry}
::: tip FEEDBACK
Please leave feedback regarding this feature in a [GitHub Discussion](https://github.com/vitest-dev/vitest/discussions/9222).
:::
* **Type:**
```ts
interface OpenTelemetryOptions {
enabled: boolean
/**
* A path to a file that exposes an OpenTelemetry SDK for Node.js.
*/
sdkPath?: string
/**
* A path to a file that exposes an OpenTelemetry SDK for the browser.
*/
browserSdkPath?: string
}
```
* **Default:** `{ enabled: false }`
This option controls [OpenTelemetry](https://opentelemetry.io/) support. Vitest imports the SDK file in the main thread and before every test file, if `enabled` is set to `true`.
::: danger PERFORMANCE CONCERNS
OpenTelemetry may significantly impact Vitest performance; enable it only for local debugging.
:::
You can use a [custom service](/guide/open-telemetry) together with Vitest to pinpoint which tests or files are slowing down your test suite.
For browser mode, see the [Browser Mode](/guide/open-telemetry#browser-mode) section of the OpenTelemetry guide.
An `sdkPath` is resolved relative to the [`root`](/config/root) of the project and should point to a module that exposes a started SDK instance as a default export. For example:
::: code-group
```js [otel.js]
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { NodeSDK } from '@opentelemetry/sdk-node'
const sdk = new NodeSDK({
serviceName: 'vitest',
traceExporter: new OTLPTraceExporter(),
instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()
export default sdk
```
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
experimental: {
openTelemetry: {
enabled: true,
sdkPath: './otel.js',
},
},
},
})
```
:::
::: warning
It's important that Node can process `sdkPath` content because it is not transformed by Vitest. See [the guide](/guide/open-telemetry) on how to work with OpenTelemetry inside of Vitest.
:::
## experimental.importDurations 4.1.0 {#experimental-importdurations}
::: tip FEEDBACK
Please leave feedback regarding this feature in a [GitHub Discussion](https://github.com/vitest-dev/vitest/discussions/9224).
:::
* **Type:**
```ts
interface ImportDurationsOptions {
/**
* When to print import breakdown to CLI terminal.
* - false: Never print (default)
* - true: Always print
* - 'on-warn': Print only when any import exceeds warn threshold
*/
print?: boolean | 'on-warn'
/**
* Fail the test run if any import exceeds the danger threshold.
* When enabled and threshold exceeded, breakdown is always printed.
* @default false
*/
failOnDanger?: boolean
/**
* Maximum number of imports to collect and display.
*/
limit?: number
/**
* Duration thresholds in milliseconds for coloring and warnings.
*/
thresholds?: {
/** Threshold for yellow/warning color. @default 100 */
warn?: number
/** Threshold for red/danger color and failOnDanger. @default 500 */
danger?: number
}
}
```
* **Default:** `{ print: false, failOnDanger: false, limit: 0, thresholds: { warn: 100, danger: 500 } }` (`limit` is 10 if `print` or UI is enabled)
Configure import duration collection and display.
The `print` option controls CLI terminal output. The `limit` option controls how many imports to collect and display. [Vitest UI](/guide/ui#import-breakdown) can always toggle the breakdown display regardless of the `print` setting.
* Self: the time it took to import the module, excluding static imports;
* Total: the time it took to import the module, including static imports. Note that this does not include `transform` time of the current module.
Note that if the file path is too long, Vitest will truncate it at the start until it fits 45 character limit.
### experimental.importDurations.print {#experimental-importdurationsprint}
* **Type:** `boolean | 'on-warn'`
* **Default:** `false`
Controls when to print import breakdown to CLI terminal after tests finish. This only works with [`default`](/guide/reporters#default), [`verbose`](/guide/reporters#verbose), or [`tree`](/guide/reporters#tree) reporters.
* `false`: Never print breakdown
* `true`: Always print breakdown
* `'on-warn'`: Print only when any import exceeds the `thresholds.warn` value
### experimental.importDurations.failOnDanger {#experimental-importdurationsfailondanger}
* **Type:** `boolean`
* **Default:** `false`
Fail the test run if any import exceeds the `thresholds.danger` value. When enabled and the threshold is exceeded, the breakdown is always printed regardless of the `print` setting.
This is useful for enforcing import performance budgets in CI:
```bash
vitest --experimental.importDurations.failOnDanger
```
### experimental.importDurations.limit {#experimental-importdurationslimit}
* **Type:** `number`
* **Default:** `0` (or `10` if `print`, `failOnDanger`, or UI is enabled)
Maximum number of imports to collect and display in CLI output, [Vitest UI](/guide/ui#import-breakdown), and third-party reporters.
### experimental.importDurations.thresholds {#experimental-importdurationsthresholds}
* **Type:** `{ warn?: number; danger?: number }`
* **Default:** `{ warn: 100, danger: 500 }`
Duration thresholds in milliseconds for coloring and warnings:
* `warn`: Threshold for yellow/warning color (default: 100ms)
* `danger`: Threshold for red/danger color and `failOnDanger` (default: 500ms)
::: info
[Vitest UI](/guide/ui#import-breakdown) shows a breakdown of imports automatically if at least one file took longer than the `danger` threshold to load.
:::
## experimental.viteModuleRunner 4.1.0 {#experimental-vitemodulerunner}
::: tip FEEDBACK
Please leave feedback regarding this feature in a [GitHub Discussion](https://github.com/vitest-dev/vitest/discussions/9501).
:::
* **Type:** `boolean`
* **Default:** `true`
Controls whether Vitest uses Vite's [module runner](https://vite.dev/guide/api-environment-runtimes#modulerunner) to run the code or fallback to the native `import`.
If this option is defined in the root config, all [projects](/guide/projects) will inherit it automatically.
Consider disabling the module runner if you are running tests in the same environment as your code (server backend or simple scripts, for example). However, we still recommend running `jsdom`/`happy-dom` tests with Vite's module runner or in [the browser](/guide/browser/) since it doesn't require any additional configuration.
Disabling this flag will disable *all* file transforms:
* test files and your source code are not processed by Vite
* your global setup files are not processed
* your custom runner/pool/environment files are not processed
* your config file is still processed by Vite (this happens before Vitest knows the `viteModuleRunner` flag)
::: warning
At the moment, Vitest still requires Vite for certain functionality like the module graph or watch mode.
Also note that this option only works with `forks` or `threads` [pools](/config/pool).
:::
### Module Runner
By default, Vitest runs tests in a very permissive module runner sandbox powered by Vite's [Environment API](https://vite.dev/guide/api-environment.html#environment-api). Every file is categorized as either an "inline" module or an "external" module.
Module runner runs all "inlined" modules. It provides `import.meta.env`, `require`, `__dirname`, `__filename`, static `import`, and has its own module resolution mechanism. This makes it very easy to run code when you don't want to configure the environment and just need to test that the bare JavaScript logic you wrote works as intended.
All "external" modules run in native mode, meaning they are executed outside of the module runner sandbox. If you are running tests in Node.js, these files are imported with the native `import` keyword and processed by Node.js directly.
While running JSDOM/happy-dom tests in a permissive fake environment might be justified, running Node.js tests in a non-Node.js environment can hide and silence potential errors you may encounter in production, especially if your code doesn't require any additional transformations provided by Vite plugins.
### Known Limitations
Some Vitest features rely on files being transformed. Vitest uses synchronous [Node.js Loaders API](https://nodejs.org/api/module.html#customization-hooks) to transform test files and setup files to support these features:
* [`import.meta.vitest`](/guide/in-source)
* [`vi.mock`](/api/vi#vi-mock)
* [`vi.hoisted`](/api/vi#vi-hoisted)
::: warning
This means that Vitest requires at least Node 22.15 for those features to work. At the moment, they also do not work in Deno or Bun.
Vitest will only detect `vi.mock` and `vi.hoisted` inside of test files, they will not be hoisted inside imported modules.
:::
This could affect performance because Vitest needs to read the file and process it. If you do not use these features, you can disable the transforms by setting `experimental.nodeLoader` to `false`. Vitest only reads test files and setup files while looking for `vi.mock` or `vi.hoisted`. Using these in other files won't hoist them to the top of the file and can lead to unexpected behavior.
Some features will not work due to the nature of `viteModuleRunner`, including:
* no `import.meta.env`: `import.meta.env` is a Vite feature, use `process.env` instead
* no `plugins`: plugins are not applied because there is no transformation phase, use [customization hooks](https://nodejs.org/api/module.html#customization-hooks) via [`execArgv`](/config/execargv) instead
* no `alias`: aliases are not applied because there is no transformation phase
* `istanbul` coverage provider doesn't work because there is no transformation phase, use `v8` instead
* `vi.resetModules()`: there is no API to invalidate ES modules from the module cache
::: warning Coverage Support
At the moment Vitest supports coverage via `v8` provider as long as files can be transformed into JavaScript. To transform TypeScript, Vitest uses [`module.stripTypeScriptTypes`](https://nodejs.org/api/module.html#modulestriptypescripttypescode-options) which is available in Node.js since v22.13. If you are using a custom [module loader](https://nodejs.org/api/module.html#customization-hooks), Vitest is not able to reuse it to transform files for analysis.
:::
With regards to mocking, it is also important to point out that ES modules do not support property override. This means that code like this won't work anymore:
```ts
import * as fs from 'node:fs'
import { vi } from 'vitest'
vi.spyOn(fs, 'readFileSync').mockImplementation(() => '42') // ❌
```
However, Vitest supports auto-spying on modules without overriding their implementation. When `vi.mock` is called with a `spy: true` argument, the module is mocked in a way that preserves original implementations, but all exported functions are wrapped in a `vi.fn()` spy:
```ts
import * as fs from 'node:fs'
import { vi } from 'vitest'
vi.mock('node:fs', { spy: true })
fs.readFileSync.mockImplementation(() => '42') // ✅
```
Factory mocking is implemented using a top-level await. This means that mocked modules cannot be loaded with `require()` in your source code:
```ts
vi.mock('node:fs', async (importOriginal) => {
return {
...await importOriginal(),
readFileSync: vi.fn(),
}
})
const fs = require('node:fs') // throws an error
```
This limitation exists because factories can be asynchronous. This should not be a problem because Vitest doesn't mock builtin modules inside `node_modules`, which is similar to how Vitest works by default.
### TypeScript
If you are using Node.js 22.18/23.6 or higher, TypeScript will be [transformed natively](https://nodejs.org/en/learn/typescript/run-natively) by Node.js.
::: warning TypeScript with Node.js 22.6-22.18
If you are using Node.js version between 22.6 and 22.18, you can also enable native TypeScript support via `--experimental-strip-types` flag:
```shell
NODE_OPTIONS="--experimental-strip-types" vitest
```
If you are using TypeScript and Node.js version lower than 22.6, then you will need to either:
* build your test files and source code and run those files directly
* import a [custom loader](https://nodejs.org/api/module.html#customization-hooks) via `execArgv` flag
```ts
import { defineConfig } from 'vitest/config'
const tsxApi = import.meta.resolve('tsx/esm/api')
export default defineConfig({
test: {
execArgv: [
`--import=data:text/javascript,import * as tsx from "${tsxApi}";tsx.register()`,
],
experimental: {
viteModuleRunner: false,
},
},
})
```
If you are running tests in Deno, TypeScript files are processed by the runtime without any additional configurations.
:::
## experimental.vcsProvider 4.1.1 {#experimental-vcsprovider}
* **Type:** `VCSProvider | string`
```ts
interface VCSProvider {
findChangedFiles(options: VCSProviderOptions): Promise
}
interface VCSProviderOptions {
root: string
changedSince?: string | boolean
}
```
* **Default:** `'git'`
Custom provider for detecting changed files. Used with the [`--changed`](/guide/cli#changed) flag to determine which files have been modified.
By default, Vitest uses Git to detect changed files. You can provide a custom implementation of the `VCSProvider` interface to use a different version control system:
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
experimental: {
vcsProvider: {
async findChangedFiles({ root, changedSince }) {
// return paths of changed files
return []
},
},
},
},
})
```
You can also pass a string path to a module with a default export that implements the `VCSProvider` interface:
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
experimental: {
vcsProvider: './my-vcs-provider.js',
},
},
})
```
```js [my-vcs-provider.js]
export default {
async findChangedFiles({ root, changedSince }) {
// return paths of changed files
return []
},
}
```
## experimental.nodeLoader 4.1.0 {#experimental-nodeloader}
* **Type:** `boolean`
* **Default:** `true`
If module runner is disabled, Vitest uses a native [Node.js module loader](https://nodejs.org/api/module.html#customization-hooks) to transform files to support `import.meta.vitest`, `vi.mock` and `vi.hoisted`.
If you don't use these features, you can disable this to improve performance.
## experimental.preParse 4.1.3 {#experimental-preparse}
* **Type:** `boolean`
* **Default:** `false`
Parses test specifications before running them. This applies the [`.only`](/api/test#test-only) modifier, the [`-t`](/config/testnamepattern) test name pattern, [`--tags-filter`](/guide/test-tags#syntax), [test lines](/api/advanced/test-specification#testlines), and [test IDs](/api/advanced/test-specification#testids) across all files without executing them. For example, if only a single test is marked with `.only`, Vitest will skip all other tests in all files.
::: tip
This option is recommended when using [`.only`](/api/test#test-only), the [`-t`](/config/testnamepattern) flag, or [`--tags-filter`](/guide/test-tags#syntax).
Enabling it unconditionally may slow down your test runs due to the additional parsing step.
:::
::: warning
Pre-parsing uses static analysis (AST parsing) instead of executing your test files. This means that test names, tags, and modifiers (`.only`, `.skip`, `.todo`) must be statically analyzable. Dynamic test names (e.g., names stored in variables or returned from function calls) and non-literal tags will not be resolved correctly.
```ts
// ✅ works — static string literal
test('adds numbers', () => {})
// ✅ works — static tags
test('my test', { tags: ['unit'] }, () => {})
// ❌ won't match correctly — dynamic name
const name = getName()
test(name, () => {})
// ❌ won't match correctly — dynamic tags
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.
---
---
url: /config/browser/playwright.md
---
# Configuring Playwright
To run tests using playwright, you need to install the [`@vitest/browser-playwright`](https://npmx.dev/package/@vitest/browser-playwright) npm package and specify its `playwright` export in the `test.browser.provider` property of your config:
```ts [vitest.config.js]
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
provider: playwright(),
instances: [{ browser: 'chromium' }]
},
},
})
```
You can configure the [`launchOptions`](https://playwright.dev/docs/api/class-browsertype#browser-type-launch), [`connectOptions`](https://playwright.dev/docs/api/class-browsertype#browser-type-connect) and [`contextOptions`](https://playwright.dev/docs/api/class-browser#browser-new-context) when calling `playwright` at the top level or inside instances:
```ts{7-14,21-26} [vitest.config.js]
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
// shared provider options between all instances
provider: playwright({
launchOptions: {
slowMo: 50,
channel: 'chrome-beta',
},
actionTimeout: 5_000,
}),
instances: [
{ browser: 'chromium' },
{
browser: 'firefox',
// overriding options only for a single instance
// this will NOT merge options with the parent one
provider: playwright({
launchOptions: {
firefoxUserPrefs: {
'browser.startup.homepage': 'https://example.com',
},
},
})
}
],
},
},
})
```
::: warning
Unlike Playwright test runner, Vitest opens a *single* page to run all tests that are defined in the same file. This means that isolation is restricted to a single test file, not to every individual test.
:::
## launchOptions
These options are directly passed down to `playwright[browser].launch` command. You can read more about the command and available arguments in the [Playwright documentation](https://playwright.dev/docs/api/class-browsertype#browser-type-launch).
::: warning
Vitest will ignore `launch.headless` option. Instead, use [`test.browser.headless`](/config/browser/headless).
Note that Vitest will push debugging flags to `launch.args` if [`--inspect`](/guide/cli#inspect) is enabled.
:::
::: tip Enabling new Chromium headless mode
Playwright supports a [new headless mode](https://playwright.dev/docs/browsers#chromium-new-headless-mode) for Chromium that uses the real Chrome browser instead of the dedicated headless shell. This provides more authentic, reliable test execution and removes the need to install a separate headless Chromium build.
To opt in, set `channel` to `'chromium'` in `launchOptions`:
```ts [vitest.config.ts]
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
headless: true,
provider: playwright({
launchOptions: {
channel: 'chromium',
},
}),
instances: [{ browser: 'chromium' }],
},
},
})
```
:::
## connectOptions
These options are directly passed down to `playwright[browser].connect` command. You can read more about the command and available arguments in the [Playwright documentation](https://playwright.dev/docs/api/class-browsertype#browser-type-connect).
Use `connectOptions.wsEndpoint` to connect to an existing Playwright server instead of launching browsers locally. This is useful for running browsers in Docker, in CI, or on a remote machine.
::: warning
Vitest forwards `launchOptions` to Playwright server via the `x-playwright-launch-options` header. This works only if the remote Playwright server supports this header, for example when using the `playwright run-server` CLI.
:::
::: details Example: Running a Playwright Server in Docker
To run browsers in a Docker container (see [Playwright Docker guide](https://playwright.dev/docs/docker#remote-connection)):
Start a Playwright server using Docker Compose:
```yaml [docker-compose.yml]
services:
playwright:
image: mcr.microsoft.com/playwright:v1.61.0-noble
command: /bin/sh -c "npx -y playwright@1.61.0 run-server --port 6677 --host 0.0.0.0"
init: true
ipc: host
user: pwuser
ports:
- '6677:6677'
```
```sh
docker compose up -d
```
Then configure Vitest to connect to it. The [`exposeNetwork`](https://playwright.dev/docs/api/class-browsertype#browser-type-connect-option-expose-network) option lets the containerized browser reach Vitest's dev server on the host:
```ts [vitest.config.ts]
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
provider: playwright({
connectOptions: {
wsEndpoint: 'ws://127.0.0.1:6677/',
exposeNetwork: '',
},
}),
instances: [
{ browser: 'chromium' },
{ browser: 'firefox' },
{ browser: 'webkit' },
],
},
},
})
```
:::
## contextOptions
Vitest creates a new context for every test file by calling [`browser.newContext()`](https://playwright.dev/docs/api/class-browsercontext). You can configure this behaviour by specifying [custom arguments](https://playwright.dev/docs/api/class-browser#browser-new-context).
::: tip
Note that the context is created for every *test file*, not every *test* like in playwright test runner.
:::
::: warning
Vitest always sets `ignoreHTTPSErrors` to `true` in case your server is served via HTTPS and `serviceWorkers` to `'allow'` to support module mocking via [MSW](https://mswjs.io).
It is also recommended to use [`test.browser.viewport`](/config/browser/headless) instead of specifying it here as it will be lost when tests are running in headless mode.
:::
## `actionTimeout`
* **Default:** no timeout
This value configures the default timeout it takes for Playwright to wait until all accessibility checks pass and [the action](/api/browser/interactivity) is actually done.
You can also configure the action timeout per-action:
```ts
import { page, userEvent } from 'vitest/browser'
await userEvent.click(page.getByRole('button'), {
timeout: 1_000,
})
```
## `persistentContext` 4.1.0 {#persistentcontext}
* **Type:** `boolean | string`
* **Default:** `false`
When enabled, Vitest uses Playwright's [persistent context](https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context) instead of a regular browser context. This allows browser state (cookies, localStorage, DevTools settings, etc.) to persist between test runs.
::: warning
This option is ignored when running tests in parallel (e.g. when headless with [`fileParallelism`](/config/fileparallelism) enabled) since persistent context cannot be shared across parallel sessions.
:::
* When set to `true`, the user data is stored in `./node_modules/.cache/vitest-playwright-user-data`
* When set to a string, the value is used as the path to the user data directory
```ts [vitest.config.js]
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
provider: playwright({
persistentContext: true,
// or specify a custom directory:
// persistentContext: './my-browser-data',
}),
instances: [{ browser: 'chromium' }],
},
},
})
```
---
---
url: /config/browser/webdriverio.md
---
# Configuring WebdriverIO
::: info Community maintained
The WebdriverIO provider ([`@vitest/browser-webdriverio`](https://github.com/vitest-community/vitest-webdriverio)) is maintained by the Vitest community in the [`vitest-community`](https://github.com/vitest-community) organization, separately from the core Vitest packages. Please report provider-specific issues to its repository.
:::
To run tests using WebdriverIO, you need to install the [`@vitest/browser-webdriverio`](https://npmx.dev/package/@vitest/browser-webdriverio) npm package and specify its `webdriverio` export in the `test.browser.provider` property of your config:
```ts [vitest.config.js]
import { webdriverio } from '@vitest/browser-webdriverio'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
provider: webdriverio(),
instances: [{ browser: 'chrome' }]
},
},
})
```
You can configure all the parameters that [`remote`](https://webdriver.io/docs/api/modules/#remoteoptions-modifier) function accepts:
```ts{8-12,19-25} [vitest.config.js]
import { webdriverio } from '@vitest/browser-webdriverio'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
// shared provider options between all instances
provider: webdriverio({
capabilities: {
browserVersion: '82',
},
}),
instances: [
{ browser: 'chrome' },
{
browser: 'firefox',
// overriding options only for a single instance
// this will NOT merge options with the parent one
provider: webdriverio({
capabilities: {
'moz:firefoxOptions': {
args: ['--disable-gpu'],
},
},
})
},
],
},
},
})
```
You can find most available options in the [WebdriverIO documentation](https://webdriver.io/docs/configuration/). Note that Vitest will ignore all test runner options because we only use `webdriverio`'s browser capabilities.
::: tip
Most useful options are located on `capabilities` object. WebdriverIO allows nested capabilities, but Vitest will ignore those options because we rely on a different mechanism to spawn several browsers.
Note that Vitest will ignore `capabilities.browserName`; use [`test.browser.instances.browser`](/config/browser/instances#browser) instead.
:::
## Headful Chrome in CI
Vitest enables [`browser.headless`](/config/browser/headless) automatically in CI.
If you explicitly set `headless: false` for Chrome on a Linux CI runner, Chrome
still needs a display server. Without one, WebDriverIO or ChromeDriver can fail
with a misleading error such as `session not created: probably user data
directory is already in use`.
Run the test command through `xvfb-run` when you need headful Chrome in GitHub
Actions or another Linux CI environment:
```bash
xvfb-run npm test
```
Alternatively, keep `browser.headless` enabled in CI and use headful mode only
for local debugging.
---
---
url: /config/browser/preview.md
---
# Configuring Preview
::: warning
The `preview` provider's main functionality is to show tests in a real browser environment. However, it does not support advanced browser automation features like multiple browser instances or headless mode. For more complex scenarios, consider using [Playwright](/config/browser/playwright) or [WebdriverIO](/config/browser/webdriverio).
:::
To see your tests running in a real browser, you need to install the [`@vitest/browser-preview`](https://npmx.dev/package/@vitest/browser-preview) npm package and specify its `preview` export in the `test.browser.provider` property of your config:
```ts [vitest.config.js]
import { preview } from '@vitest/browser-preview'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
provider: preview(),
instances: [{ browser: 'chromium' }]
},
},
})
```
This will open a new browser window using your default browser to run the tests. You can configure which browser to use by setting the `browser` property in the `instances` array. Vitest will try to open that browser automatically, but it might not work in some environments. In that case, you can manually open the provided URL in your desired browser.
## Differences with Other Providers
The preview provider has some limitations compared to other providers like [Playwright](/config/browser/playwright) or [WebdriverIO](/config/browser/webdriverio):
* It does not support headless mode; the browser window will always be visible.
* It does not support multiple instances of the same browser; each instance must use a different browser.
* It does not support advanced browser capabilities or options; you can only specify the browser name.
* It does not support CDP (Chrome DevTools Protocol) commands or other low-level browser interactions. Unlike Playwright or WebdriverIO, the [`userEvent`](/api/browser/interactivity) API is just re-exported from [`@testing-library/user-event`](https://npmx.dev/package/@testing-library/user-event) and does not have any special integration with the browser.
---
---
url: /config/browser/enabled.md
---
# browser.enabled
* **Type:** `boolean`
* **Default:** `false`
* **CLI:** `--browser`, `--browser.enabled=false`
Enabling this flag makes Vitest run all tests in a [browser](/guide/browser/) by default. If you are configuring other browser options via the CLI, you can use `--browser.enabled` alongside them instead of `--browser`:
```sh
vitest --browser.enabled --browser.headless
```
::: warning
To enable [Browser Mode](/guide/browser/), you must also specify the [`provider`](/config/browser/provider) and at least one [`instance`](/config/browser/instances). Available providers:
* [playwright](/config/browser/playwright)
* [webdriverio](/config/browser/webdriverio)
* [preview](/config/browser/preview)
:::
## Example
```js{7} [vitest.config.js]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
},
},
})
```
If you use TypeScript, the `browser` field in `instances` provides autocompletion based on your provider.
---
---
url: /config/browser/instances.md
---
# browser.instances
* **Type:** `BrowserConfig`
* **Default:** `[]`
Defines multiple browser setups. Every config has to have at least a `browser` field.
You can specify most of the [project options](/config/) (not marked with a icon) and some of the `browser` options like `browser.testerHtmlPath`.
::: warning
Every browser config inherits options from the root config:
```ts{3,9} [vitest.config.ts]
export default defineConfig({
test: {
setupFile: ['./root-setup-file.js'],
browser: {
enabled: true,
testerHtmlPath: './custom-path.html',
instances: [
{
// will have both setup files: "root" and "browser"
setupFile: ['./browser-setup-file.js'],
// implicitly has "testerHtmlPath" from the root config // [!code warning]
// testerHtmlPath: './custom-path.html', // [!code warning]
},
],
},
},
})
```
For more examples, refer to the ["Multiple Setups" guide](/guide/browser/multiple-setups).
:::
List of available `browser` options:
* `browser` (the name of the browser)
* [`headless`](/config/browser/headless)
* [`locators`](/config/browser/locators)
* [`viewport`](/config/browser/viewport)
* [`testerHtmlPath`](/config/browser/testerhtmlpath)
* [`screenshotDirectory`](/config/browser/screenshotdirectory)
* [`screenshotFailures`](/config/browser/screenshotfailures)
* [`provider`](/config/browser/provider)
Under the hood, Vitest transforms these instances into separate [test projects](/api/advanced/test-project) sharing a single Vite server for better caching performance.
---
---
url: /config/browser/headless.md
---
# browser.headless
* **Type:** `boolean`
* **Default:** `process.env.CI`
* **CLI:** `--browser.headless`, `--browser.headless=false`
Run the browser in a `headless` mode. If you are running Vitest in CI, it will be enabled by default.
---
---
url: /config/browser/testerhtmlpath.md
---
# browser.testerHtmlPath
* **Type:** `string`
A path to the HTML entry point. Can be relative to the root of the project. This file will be processed with [`transformIndexHtml`](https://vite.dev/guide/api-plugin#transformindexhtml) hook.
---
---
url: /config/browser/provider.md
---
# browser.provider {#browser-provider}
* **Type:** `BrowserProviderOption`
The return value of the provider factory. You can import the factory from `@vitest/browser-` or make your own provider:
```ts{8-10}
import { playwright } from '@vitest/browser-playwright'
import { webdriverio } from '@vitest/browser-webdriverio'
import { preview } from '@vitest/browser-preview'
export default defineConfig({
test: {
browser: {
provider: playwright(),
provider: webdriverio(),
provider: preview(),
},
},
})
```
To configure how provider initializes the browser, you can pass down options to the factory function:
```ts{7-13,20-26}
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
// shared provider options between all instances
provider: playwright({
launchOptions: {
slowMo: 50,
channel: 'chrome-beta',
},
actionTimeout: 5_000,
}),
instances: [
{ browser: 'chromium' },
{
browser: 'firefox',
// overriding options only for a single instance
// this will NOT merge options with the parent one
provider: playwright({
launchOptions: {
firefoxUserPrefs: {
'browser.startup.homepage': 'https://example.com',
},
},
})
}
],
},
},
})
```
## Custom Provider advanced {#custom-provider}
::: danger ADVANCED API
The custom provider API is highly experimental and can change between patches. If you just need to run tests in a browser, use the [`browser.instances`](/config/browser/instances) option instead.
:::
```ts
export interface BrowserProvider {
name: string
mocker?: BrowserModuleMocker
readonly initScripts?: string[]
/**
* @experimental opt-in into file parallelisation
*/
supportsParallelism: boolean
getCommandsContext: (sessionId: string) => Record
openPage: (sessionId: string, url: string) => Promise
getCDPSession?: (sessionId: string) => Promise
close: () => Awaitable
}
```
---
---
url: /config/browser/ui.md
---
# browser.ui
* **Type:** `boolean`
* **Default:** `!isCI`
* **CLI:** `--browser.ui=false`
Should Vitest UI be injected into the page. By default, injects UI iframe during development.
---
---
url: /config/browser/detailspanelposition.md
---
# browser.detailsPanelPosition
* **Type:** `'right' | 'bottom'`
* **Default:** `'right'`
* **CLI:** `--browser.detailsPanelPosition=bottom`, `--browser.detailsPanelPosition=right`
Controls the default position of the details panel in the Vitest UI when running browser tests.
* `'right'` - Shows the details panel on the right side with a horizontal split between the browser viewport and the details panel.
* `'bottom'` - Shows the details panel at the bottom with a vertical split between the browser viewport and the details panel.
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
enabled: true,
detailsPanelPosition: 'bottom', // or 'right'
},
},
})
```
## Example
::: tabs
\== bottom
---
---
url: /config/browser/viewport.md
---
# browser.viewport
* **Type:** `{ width, height }`
* **Default:** `414x896`
Default iframe's viewport.
---
---
url: /config/browser/locators.md
---
# browser.locators
Options for built-in [browser locators](/api/browser/locators).
## browser.locators.testIdAttribute
* **Type:** `string`
* **Default:** `data-testid`
Attribute used to find elements with `getByTestId` locator.
## browser.locators.exact
* **Type:** `boolean`
* **Default:** `true`
When set to `true`, [locators](/api/browser/locators) match text exactly by default, requiring a full, case-sensitive match. Individual locator calls can override this default via their own `exact` option.
```ts
// With exact: true (default), this only matches the string "Hello, World" exactly.
// With exact: false, this matches "Hello, World!", "Say Hello, World", etc.
const locator = page.getByText('Hello, World', { exact: true })
await locator.click()
```
## browser.locators.errorFormat 5.0.0 {#browser-locators-errorformat}
* **Type:** `'html' | 'aria' | 'all'`
* **Default:** `'all'`
Controls what Vitest prints when a locator cannot find an element. Vitest prints information for the DOM subtree where the locator search ran, or `document.body` for page-level locators.
* `'html'` prints that DOM subtree as HTML using [`utils.prettyDOM`](/api/browser/context#prettydom).
* `'aria'` prints that DOM subtree as an [ARIA snapshot](/guide/browser/aria-snapshots), which focuses on accessible roles, names, and state.
* `'all'` prints the ARIA snapshot first, followed by the HTML output.
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
enabled: true,
locators: {
errorFormat: 'aria',
},
},
},
})
```
For example, `all` displays a following error:
```html
VitestBrowserElementError: Cannot find element with locator: getByRole('button', { name: 'Save' })
ARIA tree:
- main:
- heading "Settings" [level=1]
- button "Cancel"
HTML:
Settings
```
---
---
url: /config/browser/screenshotdirectory.md
---
# browser.screenshotDirectory
* **Type:** `string`
* **Default:** `__screenshots__` in the test file directory
Path to the screenshots directory relative to the `root`.
---
---
url: /config/browser/screenshotfailures.md
---
# browser.screenshotFailures
* **Type:** `boolean`
* **Default:** `!browser.ui`
Should Vitest take screenshots if the test fails.
---
---
url: /config/browser/dependencysourcemaps.md
---
# browser.dependencySourcemaps
* **Type:** `boolean`
* **Default:** `true`
Serve sourcemaps of your dependencies (files in `node_modules`) to the browser during headless test runs.
These sourcemaps are used by browser devtools: with `dependencySourcemaps: false`, pausing inside dependency code shows the compiled code the browser actually runs instead of the dependency's original sources. If you don't debug into your dependencies this way, disabling them makes test runs faster: the server doesn't generate and inline the maps, and every browser tab downloads several times fewer bytes.
Reported test errors are not affected: when an error is thrown inside a pre-bundled dependency, Vitest maps its stack frames using the sourcemaps stored on disk even when this option is disabled. Frames from dependencies that are served without pre-bundling (for example, [linked packages](https://vite.dev/guide/dep-pre-bundling#monorepos-and-linked-dependencies)) that don't ship their own sourcemaps fall back to the position in the served code, which usually matches the original file.
Vitest never serves sourcemaps of its own pre-built modules in headless runs (unless [`--inspect`](/guide/cli#inspect) is used) — their frames are hidden from stack traces anyway. Sourcemaps of your own source files are always served.
::: tip
If some of your workspace code resolves to a `node_modules` path (for example, with `resolve.preserveSymlinks`), set [`server.sourcemapIgnoreList`](https://vite.dev/config/server-options#server-sourcemapignorelist) to keep its sourcemaps even when this option is disabled.
:::
---
---
url: /config/browser/orchestratorscripts.md
---
# browser.orchestratorScripts
* **Type:** `BrowserScript[]`
* **Default:** `[]`
Custom scripts that should be injected into the orchestrator HTML before test iframes are initiated. This HTML document only sets up iframes and doesn't actually import your code.
The script `src` and `content` will be processed by Vite plugins. Script should be provided in the following shape:
```ts
export interface BrowserScript {
/**
* If "content" is provided and type is "module", this will be its identifier.
*
* If you are using TypeScript, you can add `.ts` extension here for example.
* @default `injected-${index}.js`
*/
id?: string
/**
* JavaScript content to be injected. This string is processed by Vite plugins if type is "module".
*
* You can use `id` to give Vite a hint about the file extension.
*/
content?: string
/**
* Path to the script. This value is resolved by Vite so it can be a node module or a file path.
*/
src?: string
/**
* If the script should be loaded asynchronously.
*/
async?: boolean
/**
* Script type.
* @default 'module'
*/
type?: string
}
```
---
---
url: /config/browser/commands.md
---
# browser.commands
* **Type:** `Record`
* **Default:** `{ readFile, writeFile, ... }`
Custom [commands](/api/browser/commands) that can be imported during browser tests from `vitest/browser`.
::: warning Security
Commands run in the Vitest Node process. If a command exposes filesystem, process, network, database, or shell access based on browser-provided input, validate and restrict that input inside the command. Built-in file commands apply Vite `server.fs` checks and write-access checks, but custom commands are responsible for their own protections.
See [Custom Commands security notes](/api/browser/commands#custom-commands).
:::
---
---
url: /config/browser/connecttimeout.md
---
# browser.connectTimeout
* **Type:** `number`
* **Default:** `60_000`
The timeout in milliseconds. If connection to the browser takes longer, the test suite will fail.
::: info
This is the time it should take for the browser to establish the WebSocket connection with the Vitest server. In normal circumstances, this timeout should never be reached.
:::
---
---
url: /config/browser/trace.md
---
# browser.trace
* **Type:** `'on' | 'off' | 'on-first-retry' | 'on-all-retries' | 'retain-on-failure' | object`
* **CLI:** `--browser.trace=on`, `--browser.trace=retain-on-failure`
* **Default:** `'off'`
Capture a trace of your browser test runs. You can preview traces with [Playwright Trace Viewer](https://trace.playwright.dev/).
See [Playwright Traces](/guide/browser/playwright-traces) for the full workflow.
This options supports the following values:
* `'on'` - capture trace for all tests. (not recommended as it's performance heavy)
* `'off'` - do not capture traces.
* `'on-first-retry'` - capture trace only when retrying the test for the first time.
* `'on-all-retries'` - capture trace on every retry of the test.
* `'retain-on-failure'` - capture trace only for tests that fail. This will automatically delete traces for tests that pass.
* `object` - an object with the following shape:
```ts
interface TraceOptions {
mode: 'on' | 'off' | 'on-first-retry' | 'on-all-retries' | 'retain-on-failure'
/**
* The directory where all traces will be stored. By default, Vitest
* stores all traces in `__traces__` folder close to the test file.
*/
tracesDir?: string
/**
* Whether to capture screenshots during tracing. Screenshots are used to build a timeline preview.
* @default true
*/
screenshots?: boolean
/**
* If this option is true tracing will
* - capture DOM snapshot on every action
* - record network activity
* @default true
*/
snapshots?: boolean
}
```
::: danger WARNING
This option is supported only by the [**playwright**](/config/browser/playwright) provider.
:::
---
---
url: /config/browser/trackunhandlederrors.md
---
# browser.trackUnhandledErrors
* **Type:** `boolean`
* **Default:** `true`
Enables tracking uncaught errors and exceptions so they can be reported by Vitest.
If you need to hide certain errors, it is recommended to use [`onUnhandledError`](/config/onunhandlederror) option instead.
Disabling this will completely remove all Vitest error handlers, which can help debugging with the "Pause on exceptions" checkbox turned on.
---
---
url: /config/browser/expect.md
---
# browser.expect
* **Type:** `ExpectOptions`
## browser.expect.toMatchScreenshot
Default options for the
[`toMatchScreenshot` assertion](/api/browser/assertions.html#tomatchscreenshot).
These options will be applied to all screenshot assertions.
::: tip
Setting global defaults for screenshot assertions helps maintain consistency
across your test suite and reduces repetition in individual tests. You can still
override these defaults at the assertion level when needed for specific test cases.
:::
```ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
enabled: true,
expect: {
toMatchScreenshot: {
comparatorName: 'pixelmatch',
comparatorOptions: {
threshold: 0.2,
allowedMismatchedPixels: 100,
},
resolveScreenshotPath: ({ arg, browserName, ext, testFileName }) =>
`custom-screenshots/${testFileName}/${arg}-${browserName}${ext}`,
},
},
},
},
})
```
[All options available in the `toMatchScreenshot` assertion](/api/browser/assertions#options)
can be configured here. Additionally, two path resolution functions are
available: `resolveScreenshotPath` and `resolveDiffPath`.
## browser.expect.toMatchScreenshot.screenshotDirectory
* **Type:** `string | undefined`
* **Default:** `__screenshots__`
The directory name used for storing reference screenshots.
This value is passed as `screenshotDirectory` to [`browser.expect.toMatchScreenshot.resolveScreenshotPath`](#browserexpecttomatchscreenshotresolvescreenshotpath) and [`browser.expect.toMatchScreenshot.resolveDiffPath`](#browserexpecttomatchscreenshotresolvediffpath), and used in the default path resolution of `resolveScreenshotPath`.
## browser.expect.toMatchScreenshot.resolveScreenshotPath
* **Type:** `(data: PathResolveData) => string`
* **Default output:** ``path.resolve(root, testFileDirectory, screenshotDirectory, testFileName, `${arg}-${browserName}-${platform}${ext}`)``
A function to customize where reference screenshots are stored. The function
receives an object with the following properties:
* `arg: string`
Path **without** extension, sanitized and relative to the test file.
This comes from the arguments passed to `toMatchScreenshot`; if called
without arguments this will be the auto-generated name.
```ts
test('calls `onClick`', () => {
expect(locator).toMatchScreenshot()
// arg = "calls-onclick-1"
})
expect(locator).toMatchScreenshot('foo/bar/baz.png')
// arg = "foo/bar/baz"
expect(locator).toMatchScreenshot('../foo/bar/baz.png')
// arg = "foo/bar/baz"
```
* `ext: string`
Screenshot extension, with leading dot.
This can be set through the arguments passed to `toMatchScreenshot`, but
the value will fall back to `'.png'` if an unsupported extension is used.
* `browserName: string`
The instance's browser name.
* `platform: NodeJS.Platform`
The value of
[`process.platform`](https://nodejs.org/docs/v22.16.0/api/process.html#processplatform).
* `screenshotDirectory: string`
The value provided to [`browser.expect.toMatchScreenshot.screenshotDirectory`](#browserexpecttomatchscreenshotscreenshotdirectory), if none is provided, its default value (`__screenshots__`).
* `root: string`
Absolute path to the project's [`root`](/config/root).
* `testFileDirectory: string`
Path to the test file, relative to the project's [`root`](/config/root).
* `testFileName: string`
The test's filename.
* `testName: string`
The [`test`](/api/test)'s name, including parent
[`describe`](/api/describe), sanitized.
* `attachmentsDir: string`
The value provided to [`attachmentsDir`](/config/attachmentsdir), if none is
provided, its default value.
* `project: TestProject` 4.1.6
The [`TestProject`](/api/advanced/test-project) the test belongs to.
For example, to group screenshots by browser:
```ts
resolveScreenshotPath: ({ arg, browserName, ext, root, testFileName }) =>
`${root}/screenshots/${browserName}/${testFileName}/${arg}${ext}`
```
## browser.expect.toMatchScreenshot.resolveDiffPath
* **Type:** `(data: PathResolveData) => string`
* **Default output:** ``path.resolve(root, attachmentsDir, testFileDirectory, testFileName, `${arg}-${browserName}-${platform}${ext}`)``
A function to customize where diff images are stored when screenshot comparisons
fail. Receives the same data object as
[`resolveScreenshotPath`](#browser-expect-tomatchscreenshot-resolvescreenshotpath).
For example, to store diffs in a subdirectory of attachments:
```ts
resolveDiffPath: ({ arg, attachmentsDir, browserName, ext, root, testFileName }) =>
`${root}/${attachmentsDir}/screenshot-diffs/${testFileName}/${arg}-${browserName}${ext}`
```
## browser.expect.toMatchScreenshot.comparators
* **Type:** `Record`
Register custom screenshot comparison algorithms, like [SSIM](https://en.wikipedia.org/wiki/Structural_similarity_index_measure) or other perceptual similarity metrics.
To create a custom comparator, you need to register it in your config. If using TypeScript, declare its options in the `ScreenshotComparatorRegistry` interface.
```ts
import { defineConfig } from 'vitest/config'
// 1. Declare the comparator's options type
declare module 'vitest/browser' {
interface ScreenshotComparatorRegistry {
myCustomComparator: {
sensitivity?: number
ignoreColors?: boolean
}
}
}
// 2. Implement the comparator
export default defineConfig({
test: {
browser: {
expect: {
toMatchScreenshot: {
comparators: {
myCustomComparator: async (
reference,
actual,
{
createDiff, // always provided by Vitest
sensitivity = 0.01,
ignoreColors = false,
}
) => {
// ...algorithm implementation
return { pass, diff, message }
},
},
},
},
},
},
})
```
Then use it in your tests:
```ts
await expect(locator).toMatchScreenshot({
comparatorName: 'myCustomComparator',
comparatorOptions: {
sensitivity: 0.08,
ignoreColors: true,
},
})
```
**Comparator Function Signature:**
```ts
type Comparator = (
reference: {
metadata: { height: number; width: number }
data: TypedArray
},
actual: {
metadata: { height: number; width: number }
data: TypedArray
},
options: {
createDiff: boolean
} & Options
) => Promise<{
pass: boolean
diff: TypedArray | null
message: string | null
}> | {
pass: boolean
diff: TypedArray | null
message: string | null
}
```
The `reference` and `actual` images are decoded using the appropriate codec (currently only PNG). The `data` property is a flat `TypedArray` (`Buffer`, `Uint8Array`, or `Uint8ClampedArray`) containing pixel data in RGBA format:
* **4 bytes per pixel**: red, green, blue, alpha (from `0` to `255` each)
* **Row-major order**: pixels are stored left-to-right, top-to-bottom
* **Total length**: `width × height × 4` bytes
* **Alpha channel**: always present. Images without transparency have alpha values set to `255` (fully opaque)
::: tip Performance Considerations
The `createDiff` option indicates whether a diff image is needed. During [stable screenshot detection](/guide/browser/visual-regression-testing#how-visual-tests-work), Vitest calls comparators with `createDiff: false` to avoid unnecessary work.
**Respect this flag to keep your tests fast**.
:::
::: warning Handle Missing Options
The `options` parameter in `toMatchScreenshot()` is optional, so users might not provide all your comparator options. Always make them optional with default values:
```ts
myCustomComparator: (
reference,
actual,
{ createDiff, threshold = 0.1, maxDiff = 100 },
) => {
// ...comparison logic
}
```
:::
---
---
url: /guide/why.md
---
# Why Vitest
:::tip NOTE
Vitest is powered by Vite. While you do not need to know Vite to use Vitest, understanding Vite helps explain some of Vitest's unique advantages. To learn more about Vite, read the [Why Vite Guide](https://vitejs.dev/guide/why.html) or watch [Next generation frontend tooling with ViteJS](https://www.youtube.com/watch?v=UJypSr8IhKY) by [Evan You](https://bsky.app/profile/evanyou.me).
:::
## The Need for a Vite Native Test Runner
Vite's out-of-the-box support for common web patterns, features like glob imports and SSR primitives, and its many plugins and integrations are fostering a vibrant ecosystem. Its dev and build story are key to its success. For docs, there are several SSG-based alternatives powered by Vite. Vite's Unit Testing story hasn't been clear though. Existing options like [Jest](https://jestjs.io/) were created in a different context. There is a lot of duplication between Jest and Vite, forcing users to configure two different pipelines.
Using the Vite dev server to transform your files during testing enables the creation of a simple runner that doesn't need to deal with the complexity of transforming source files and can solely focus on providing the best DX during testing. It is a test runner that uses the same configuration as your app (through `vite.config.js`), sharing a common transformation pipeline during dev, build, and test time. It is extensible with the same plugin API that lets you and the maintainers of your tools provide first-class integration with Vite. It is a tool that is built with Vite in mind from the start, taking advantage of its improvements in DX, like its instant Hot Module Replacement (HMR). This is Vitest, a next-generation testing framework powered by Vite.
Given Jest's massive adoption, Vitest provides a compatible API that allows you to use it as a drop-in replacement in most projects. It also includes the most common features required when setting up your unit tests (mocking, snapshots, coverage). Vitest cares a lot about performance and uses Worker threads to run as much as possible in parallel. Some ports have seen test running an order of magnitude faster. Watch mode is enabled by default, aligning itself with the way Vite pushes for a dev first experience. Even with all these improvements in DX, Vitest stays lightweight by carefully choosing its dependencies (or directly inlining needed pieces).
**Vitest aims to position itself as the Test Runner of choice for Vite projects, and as a solid alternative even for projects not using Vite.**
Continue reading in the [Getting Started Guide](./index)
## How is Vitest Different from X?
You can check out the [Comparisons](./comparisons) section for more details on how Vitest differs from other similar tools.
---
---
url: /guide.md
---
# Getting Started
## Overview
Vitest (pronounced as *"veetest"*) is a next generation testing framework
powered by
Vite.
You can learn more about the rationale behind the project in the [Why Vitest](/guide/why) section.
## Trying Vitest Online
You can try Vitest online on [StackBlitz](https://vitest.new). It runs Vitest directly in the browser, and it is almost identical to the local setup but doesn't require installing anything on your machine.
## Adding Vitest to Your Project
Learn how to install by Video
::: code-group
```bash [npm]
npm install -D vitest
```
```bash [yarn]
yarn add -D vitest
```
```bash [pnpm]
pnpm add -D vitest
```
```bash [bun]
bun add -D vitest
```
```bash [deno]
deno add -D vitest
```
:::
:::tip
Vitest requires Vite >=v6.4.0 and Node >=v22.12.0
:::
It is recommended that you install a copy of `vitest` in your `package.json`, using one of the methods listed above. However, if you would prefer to run `vitest` directly, you can use `npx vitest` (the `npx` tool comes with npm and Node.js).
The `npx` tool will execute the specified command. By default, `npx` will first check if the command exists in the local project's binaries. If it is not found there, `npx` will look in the system's `$PATH` and execute it if found. If the command is not found in either location, `npx` will install it in a temporary location prior to execution.
Vitest and third party integrations can use `.vitest` directory to store generated artifacts. It's recommended to add this in your `.gitignore`.
```sh [.gitignore]
# Vitest reports and artifacts
.vitest/
```
## Writing Tests
As an example, we will write a simple test that verifies the output of a function that adds two numbers.
```js [sum.js]
export function sum(a, b) {
return a + b
}
```
```js [sum.test.js]
import { expect, test } from 'vitest'
import { sum } from './sum.js'
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3)
})
```
::: tip
By default, tests must contain `.test.` or `.spec.` in their file name.
:::
Next, in order to execute the test, add the following section to your `package.json`:
```json [package.json]
{
"scripts": {
"test": "vitest"
}
}
```
Finally, run `npm run test`, `yarn test` or `pnpm test`, depending on your package manager, and Vitest will print this message:
```txt
✓ sum.test.js (1)
✓ adds 1 + 2 to equal 3
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 02:15:44
Duration 311ms
```
::: warning
If you are using Bun as your package manager, make sure to use `bun run test` command instead of `bun test`, otherwise Bun will run its own test runner.
:::
Your first test is passing! Continue to [Writing Tests](/guide/learn/writing-tests) to learn about organizing tests, reading test output, and the core testing patterns you'll use every day.
To run tests once without watching for file changes, use `vitest run`. You can also pass additional flags like `--reporter` or `--coverage`. For a full list of CLI options, run `npx vitest --help` or see the [CLI guide](/guide/cli).
## Configuring Vitest
Vitest reads your `vite.config.*` by default, so your existing Vite plugins and configuration work out-of-the-box. You can also create a dedicated `vitest.config.*` for test-specific settings. See the [Config Reference](/config/) for details.
## IDE Integrations
We also provided an official extension for Visual Studio Code to enhance your testing experience with Vitest.
[Install from VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=vitest.explorer)
Learn more about [IDE Integrations](/guide/ide)
## Examples
| Example | Source | Playground |
|---|---|---|
| `basic` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/basic) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/basic?initialPath=__vitest__/) |
| `fastify` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/fastify) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/fastify?initialPath=__vitest__/) |
| `in-source-test` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/in-source-test) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/in-source-test?initialPath=__vitest__/) |
| `lit` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/lit) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/lit?initialPath=__vitest__/) |
| `vue` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/vue) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/vue?initialPath=__vitest__/) |
| `marko` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/marko) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/marko?initialPath=__vitest__/) |
| `preact` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/preact) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/preact?initialPath=__vitest__/) |
| `qwik` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/qwik) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/qwik?initialPath=__vitest__/) |
| `react` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/react) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/react?initialPath=__vitest__/) |
| `solid` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/solid) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/solid?initialPath=__vitest__/) |
| `svelte` | [GitHub](https://github.com/vitest-tests/browser-examples/tree/main/examples/svelte) | [Play Online](https://stackblitz.com/fork/github/vitest-tests/browser-examples/tree/main/examples/svelte?initialPath=__vitest__/) |
| `profiling` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/profiling) | Not Available |
| `typecheck` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/typecheck) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/typecheck?initialPath=__vitest__/) |
| `projects` | [GitHub](https://github.com/vitest-dev/vitest/tree/main/examples/projects) | [Play Online](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/projects?initialPath=__vitest__/) |
## Community
If you have questions or need help, reach out to the community at [Discord](https://chat.vitest.dev) and [GitHub Discussions](https://github.com/vitest-dev/vitest/discussions).
---
---
url: /guide/features.md
---
# Features
::: tip
This page is a high-level overview of Vitest's capabilities. If you're new to Vitest, we recommend reading the [Learn](/guide/learn/writing-tests) tutorial first for a hands-on introduction.
:::
## Shared Config between Test, Dev and Build
Vite's config, transformers, resolvers, and plugins. Use the same setup from your app to run the tests.
Learn more at [Configuring Vitest](/config/).
## Watch Mode
```bash
$ vitest
```
When you modify your source code or the test files, Vitest smartly searches the module graph and only reruns the related tests, just like how HMR works in Vite!
`vitest` starts in `watch mode` **by default in development environment** and `run mode` in CI environment (when `process.env.CI` presents) smartly. You can use `vitest watch` or `vitest run` to explicitly specify the desired mode.
Start Vitest with the `--standalone` flag to keep it running in the background. It won't run any tests until they change. Vitest will not run tests if the source code is changed until the test that imports the source has been run
## Common Web Idioms Out-Of-The-Box
Out-of-the-box ES Module / TypeScript / JSX support / PostCSS
## Threads
By default Vitest runs test files in [multiple processes](/guide/parallelism) using [`node:child_process`](https://nodejs.org/api/child_process.html), allowing tests to run simultaneously. If you want to speed up your test suite even further, consider enabling `--pool=threads` to run tests using [`node:worker_threads`](https://nodejs.org/api/worker_threads.html) (beware that some packages might not work with this setup).
To run tests in a single thread or process, see [`fileParallelism`](/config/fileparallelism).
Vitest also isolates each file's environment so env mutations in one file don't affect others. Isolation can be disabled by passing `--no-isolate` to the CLI (trading correctness for run performance).
## Test Filtering
Vitest provides many ways to narrow down the tests to run in order to speed up testing so you can focus on development.
Learn more about [Test Filtering](/guide/filtering).
## Running Tests Concurrently
Use `.concurrent` in consecutive tests to start them in parallel.
```ts
import { describe, it } from 'vitest'
// The two tests marked with concurrent will be started in parallel
describe('suite', () => {
it('serial test', async () => { /* ... */ })
it.concurrent('concurrent test 1', async ({ expect }) => { /* ... */ })
it.concurrent('concurrent test 2', async ({ expect }) => { /* ... */ })
})
```
If you use `.concurrent` on a suite, every test in it will be started in parallel.
```ts
import { describe, it } from 'vitest'
// All tests within this suite will be started in parallel
describe.concurrent('suite', () => {
it('concurrent test 1', async ({ expect }) => { /* ... */ })
it('concurrent test 2', async ({ expect }) => { /* ... */ })
it.concurrent('concurrent test 3', async ({ expect }) => { /* ... */ })
})
```
You can also use `.skip`, `.only`, and `.todo` with concurrent suites and tests. Read more in the [API Reference](/api/test#test-concurrent).
::: warning
When running concurrent tests, Snapshots and Assertions must use `expect` from the local [Test Context](/guide/test-context) to ensure the right test is detected.
:::
## Snapshot
[Jest-compatible](https://jestjs.io/docs/snapshot-testing) snapshot support.
```ts
import { expect, it } from 'vitest'
it('renders correctly', () => {
const result = render()
expect(result).toMatchSnapshot()
})
```
Learn more at [Snapshot](/guide/snapshot).
## Chai and Jest `expect` Compatibility
[Chai](https://www.chaijs.com/) is built-in for assertions with [Jest `expect`](https://jestjs.io/docs/expect)-compatible APIs.
Notice that if you are using third-party libraries that add matchers, setting [`test.globals`](/config/globals) to `true` will provide better compatibility.
## Mocking
Vitest provides `jest`-compatible APIs on `vi` object.
```ts
import { expect, vi } from 'vitest'
const fn = vi.fn()
fn('hello', 1)
expect(vi.isMockFunction(fn)).toBe(true)
expect(fn.mock.calls[0]).toEqual(['hello', 1])
fn.mockImplementation((arg: string) => arg)
fn('world', 2)
expect(fn.mock.results[1].value).toBe('world')
```
Vitest supports both [happy-dom](https://github.com/capricorn86/happy-dom) or [jsdom](https://github.com/jsdom/jsdom) for mocking DOM and browser APIs. They don't come with Vitest, you will need to install them separately:
::: code-group
```bash [happy-dom]
$ npm i -D happy-dom
```
```bash [jsdom]
$ npm i -D jsdom
```
:::
After that, change the `environment` option in your config file:
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'happy-dom', // or 'jsdom', 'node'
},
})
```
Learn more at [Mocking](/guide/mocking).
## Coverage
Vitest supports Native code coverage via [`v8`](https://v8.dev/blog/javascript-code-coverage) and instrumented code coverage via [`istanbul`](https://istanbul.js.org/).
```json [package.json]
{
"scripts": {
"test": "vitest",
"coverage": "vitest run --coverage"
}
}
```
Learn more at [Coverage](/guide/coverage).
## In-Source Testing
Vitest also provides a way to run tests within your source code along with the implementation, similar to [Rust's module tests](https://doc.rust-lang.org/book/ch11-03-test-organization.html#the-tests-module-and-cfgtest).
This makes the tests share the same closure as the implementations and able to test against private states without exporting. Meanwhile, it also brings the feedback loop closer for development.
```ts [src/index.ts]
// the implementation
export function add(...args: number[]): number {
return args.reduce((a, b) => a + b, 0)
}
// in-source test suites
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('add', () => {
expect(add()).toBe(0)
expect(add(1)).toBe(1)
expect(add(1, 2, 3)).toBe(6)
})
}
```
Learn more at [In-source testing](/guide/in-source).
## Benchmarking {#benchmarking}
You can run benchmark tests with [`bench`](/api/test#bench) function via [Tinybench](https://github.com/tinylibs/tinybench) to compare performance results.
```ts [sort.bench.ts]
import { bench, describe } from 'vitest'
describe('sort', () => {
bench('normal', () => {
const x = [1, 5, 4, 2, 3]
x.sort((a, b) => {
return a - b
})
})
bench('reverse', () => {
const x = [1, 5, 4, 2, 3]
x.reverse().sort((a, b) => {
return a - b
})
})
})
```
## Type Testing {#type-testing}
You can [write tests](/guide/testing-types) to catch type regressions. Vitest comes with [`expect-type`](https://github.com/mmkal/expect-type) package to provide you with a similar and easy to understand API.
```ts [types.test-d.ts]
import { assertType, expectTypeOf, test } from 'vitest'
import { mount } from './mount.js'
test('my types work properly', () => {
expectTypeOf(mount).toBeFunction()
expectTypeOf(mount).parameter(0).toExtend<{ name: string }>()
// @ts-expect-error name is a string
assertType(mount({ name: 42 }))
})
```
## Sharding
Run tests on different machines using [`--shard`](/guide/cli#shard) and [`--reporter=blob`](/guide/reporters#blob-reporter) flags.
All test and coverage results can be merged at the end of your CI pipeline using `--merge-reports` command:
```bash
vitest --shard=1/2 --reporter=blob --coverage
vitest --shard=2/2 --reporter=blob --coverage
vitest --merge-reports --reporter=junit --coverage
```
See [`Improving Performance | Sharding`](/guide/improving-performance#sharding) for more information.
## Environment Variables
Vitest exclusively autoloads environment variables prefixed with `VITE_` from `.env` files to maintain compatibility with frontend-related tests, adhering to [Vite's established convention](https://vitejs.dev/guide/env-and-mode.html#env-files). To load every environmental variable from `.env` files anyway, you can use `loadEnv` method imported from `vite`:
```ts [vitest.config.ts]
import { loadEnv } from 'vite'
import { defineConfig } from 'vitest/config'
export default defineConfig(({ mode }) => ({
test: {
// mode defines what ".env.{mode}" file to choose if exists
env: loadEnv(mode, process.cwd(), ''),
},
}))
```
## Unhandled Errors
By default, Vitest catches and reports all [unhandled rejections](https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event), [uncaught exceptions](https://nodejs.org/api/process.html#event-uncaughtexception) (in Node.js) and [error](https://developer.mozilla.org/en-US/docs/Web/API/Window/error_event) events (in the [browser](/guide/browser/)).
You can disable this behaviour by catching them manually. Vitest assumes the callback is handled by you and won't report the error.
::: code-group
```ts [setup.node.js]
// in Node.js
process.on('unhandledRejection', () => {
// your own handler
})
process.on('uncaughtException', () => {
// your own handler
})
```
```ts [setup.browser.js]
// in the browser
window.addEventListener('error', () => {
// your own handler
})
window.addEventListener('unhandledrejection', () => {
// your own handler
})
```
:::
Alternatively, you can also ignore reported errors with a [`dangerouslyIgnoreUnhandledErrors`](/config/dangerouslyignoreunhandlederrors) option. Vitest will still report them, but they won't affect the test result (exit code won't be changed).
If you need to test that error was not caught, you can create a test that looks like this:
```ts
test('my function throws uncaught error', async ({ onTestFinished }) => {
const unhandledRejectionListener = vi.fn()
process.on('unhandledRejection', unhandledRejectionListener)
onTestFinished(() => {
process.off('unhandledRejection', unhandledRejectionListener)
})
callMyFunctionThatRejectsError()
await expect.poll(unhandledRejectionListener).toHaveBeenCalled()
})
```
---
---
url: /guide/learn/writing-tests.md
---
# Writing Tests
In the [Getting Started](/guide/) guide, you installed Vitest and ran your first test. This page dives deeper into how to write and organize tests in Vitest.
## Your First Test
A test verifies that a piece of code produces the expected result. In Vitest, you use the [`test`](/api/test) function to define a test, and [`expect`](/api/expect) to make assertions. Each test has a name (a string describing what it checks) and a function that contains one or more assertions. If any assertion fails, the test fails.
```js
import { expect, test } from 'vitest'
test('Math.sqrt works for perfect squares', () => {
expect(Math.sqrt(4)).toBe(2)
expect(Math.sqrt(144)).toBe(12)
expect(Math.sqrt(0)).toBe(0)
})
```
::: details Use `test` or `it`?
You might also see tests written with [`it`](/api/test) instead of `test`. They behave identically. `it` is just an alias that some people prefer because it reads more naturally with a descriptive name:
```js
import { expect, it } from 'vitest'
it('should compute square roots', () => {
expect(Math.sqrt(4)).toBe(2)
})
```
Use whichever you prefer. Both work the same way, and you can mix them freely in a project. If you want to enforce a consistent choice across your codebase, the [`consistent-test-it`](https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/consistent-test-it.md) ESLint rule (also available in [oxlint](https://oxc.rs/docs/guide/usage/linter/rules/jest/consistent-test-it.html)) can help with that.
:::
## Grouping Tests with `describe`
As your test files grow, you'll want to organize related tests together. [`describe`](/api/describe) creates a test suite, which is a named group of tests:
```js
import { describe, expect, test } from 'vitest'
describe('Math.sqrt', () => {
test('returns the square root of perfect squares', () => {
expect(Math.sqrt(4)).toBe(2)
expect(Math.sqrt(9)).toBe(3)
})
test('returns NaN for negative numbers', () => {
expect(Math.sqrt(-1)).toBeNaN()
})
test('returns 0 for 0', () => {
expect(Math.sqrt(0)).toBe(0)
})
})
```
You can nest `describe` blocks for further organization, but keep nesting shallow. Deeply nested tests are harder to read. A flat list of tests is often enough for simple modules, and `describe` becomes more useful when a file tests multiple functions or methods that each need their own group.
## Test Files
By default, Vitest looks for any file that contains `.test.` or `.spec.` in its name, such as `utils.test.js`, `app.spec.js`, or `math.test.jsx`. It searches in all subdirectories, so it doesn't matter where you place them.
The exact patterns are:
* `**/*.test.{ts,js,mjs,cjs,tsx,jsx}`
* `**/*.spec.{ts,js,mjs,cjs,tsx,jsx}`
There's no single "right" way to organize your test files. Some teams prefer placing tests right next to the source code they test, while others keep them in a dedicated directory. Vitest will find them either way:
```
src/
utils.js
utils.test.js # co-located with the source
__tests__/
utils.test.js # in a test directory
```
If the default patterns don't work for your project, you can customize which files are included with the [`include`](/config/include) and [`exclude`](/config/exclude) config options.
## Testing TypeScript
Because Vitest runs on top of Vite, TypeScript works out of the box. There's no extra compiler to install, no `ts-jest` to configure, and no separate build step for your tests. Just name your test file `.test.ts` instead of `.test.js` and start writing:
```ts
import { expect, test } from 'vitest'
interface User {
name: string
age: number
}
function createUser(name: string, age: number): User {
return { name, age }
}
test('creates a user with the correct fields', () => {
const user = createUser('Alice', 30)
expect(user).toEqual({ name: 'Alice', age: 30 })
expect(user.name).toBe('Alice')
})
```
You can import your production types, use generics, and write typed test utilities exactly as you would in the rest of your codebase. Vite transforms TypeScript on the fly, so tests start fast even in large projects.
::: tip
Vitest transforms TypeScript for execution but does **not** type-check your tests during the test run. This is the same trade-off Vite makes for speed: you get fast feedback in the terminal, and run `tsc` or `vitest typecheck` separately when you want full type checking. See the [Testing Types](/guide/testing-types) guide for more details.
:::
## Reading Test Output
When you run `vitest` and only a single test file matches, the output is expanded into a tree structure showing `describe` groups and individual tests along with their duration:
```ansi
[32m✓[39m src/utils.test.js [90m(3 tests)[39m [32m5ms[39m
[32m✓[39m Math.sqrt [32m4ms[39m
[32m✓[39m returns the square root of perfect squares [32m2ms[39m
[32m✓[39m returns NaN for negative numbers [32m1ms[39m
[32m✓[39m returns 0 for 0 [32m1ms[39m
[1mTest Files[22m [32m1 passed[39m (1)
[1m Tests[22m [32m3 passed[39m (3)
```
When multiple test files run, Vitest collapses each file into a single line to keep the output manageable:
```ansi
[32m✓[39m src/utils.test.js [90m(3 tests)[39m [32m5ms[39m
[32m✓[39m src/math.test.js [90m(2 tests)[39m [32m3ms[39m
[32m✓[39m src/strings.test.js [90m(4 tests)[39m [32m7ms[39m
[1mTest Files[22m [32m3 passed[39m (3)
[1m Tests[22m [32m9 passed[39m (9)
```
When a test fails, Vitest shows you exactly what went wrong. You'll see the expected value, the actual value, a diff highlighting the difference, and a code snippet of the surrounding lines with the failing assertion highlighted. It also includes the file and line number so you can jump straight to the source:
```ansi
[31mFAIL[39m src/utils.test.js [2m>[22m Math.sqrt [2m>[22m returns the square root of perfect squares
[31mAssertionError: expected 3 to be 2[39m
[31m- Expected[39m
[32m+ Received[39m
[31m2[39m
[32m3[39m
[2m❯[22m [36msrc/utils.test.js[39m[2m:5:28[22m
[2m 3|[22m [34mtest[39m([32m'returns the square root of perfect squares'[39m, () => {
[2m 4|[22m [34mexpect[39m(Math.[34msqrt[39m([34m4[39m)).[34mtoBe[39m([34m2[39m)
[2m 5|[22m [34mexpect[39m(Math.[34msqrt[39m([34m9[39m)).[34mtoBe[39m([34m2[39m)
[31m^[39m
[2m 6|[22m })
[2m 7|[22m
```
Between the diff and the code snippet, you can usually understand what went wrong without needing to add extra `console.log` statements or open the file yourself.
## Skipping and Focusing Tests
While developing, you'll often want to run only a subset of tests. Vitest provides modifiers for this:
[`.only`](/api/test#only) tells Vitest to run only this test (or suite) and skip everything else in the file. This is useful when you're working on a specific test and don't want to wait for the entire suite to finish:
```js
test.only('focus on this test', () => {
// only this test runs in the file
})
```
[`.skip`](/api/test#skip) does the opposite. It skips a test without removing it, which is handy when a test is temporarily broken or you want to ignore it while you work on something else:
```js
test.skip('not ready yet', () => {
// this test is skipped
})
```
[`.todo`](/api/test#todo) lets you mark a placeholder for a test you haven't written yet. Vitest will list it in the output so you won't forget about it:
```js
test.todo('implement validation later')
```
These modifiers are great for quick, local changes while developing. For more permanent ways to filter tests (by filename, line number, or tags), see the [Test Filtering](/guide/filtering) guide.
## Parameterized Tests
When you have several test cases that only differ in their inputs and expected outputs, writing a separate `test` for each one gets repetitive. [`test.for`](/api/test#test-for) lets you define the cases as data and run the same test logic for all of them:
```js
import { expect, test } from 'vitest'
test.for([
[1, 1, 2],
[1, 2, 3],
[2, 1, 3],
])('add(%i, %i) -> %i', ([a, b, expected]) => {
expect(a + b).toBe(expected)
})
```
In the example above, the %i placeholders are replaced with the integer values from each data row. Vitest also supports other placeholder types, such as %s for strings and %f for floating-point numbers. As a result, the test runner generates test names such as add(1, 1) -> 2, add(1, 2) -> 3, and add(2, 1) -> 3.
If your cases have more than two or three values, passing objects is more readable. Use `$property` in the name to interpolate fields:
```js
test.for([
{ a: 1, b: 1, expected: 2 },
{ a: 1, b: 2, expected: 3 },
{ a: 2, b: 1, expected: 3 },
])('add($a, $b) -> $expected', ({ a, b, expected }) => {
expect(a + b).toBe(expected)
})
```
The second argument to the test function is the [Test Context](/guide/test-context), which gives you access to fixtures, per-test `expect`, and other utilities. This is especially useful with [`test.concurrent`](/api/test#concurrent), where concurrent tests run in parallel and the global `expect` can't reliably associate a snapshot with the right test. The context-scoped `expect` solves this:
```js
test.concurrent.for([
[1, 1],
[1, 2],
[2, 1],
])('add(%i, %i)', ([a, b], { expect }) => {
expect(a + b).toMatchSnapshot()
})
```
[`describe.for`](/api/describe#describe-for) works the same way but creates a suite for each set of parameters, which is useful when multiple tests share the same parameterized setup.
::: tip
Vitest also provides [`test.each`](/api/test#each), which you may recognize from Jest. It works similarly but spreads array arguments instead of passing them as a single value, and doesn't provide access to the Test Context. It exists mainly for Jest compatibility. Prefer `test.for` in new code.
:::
## Using Global Imports
By default, you import `test`, `expect`, `describe`, and other functions from `vitest` at the top of every test file. If you'd rather use them as globals without importing (similar to how Jest works), you can enable the [`globals`](/config/globals) option in your config:
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
},
})
```
With this enabled, you can write tests without the import line:
```js
test('no import needed', () => {
expect(1 + 1).toBe(2)
})
```
::: tip
If you use TypeScript, add `"types": ["vitest/globals"]` to your `tsconfig.json` `compilerOptions` for proper type support.
:::
## Running Tests
Vitest runs all test files **in parallel** by default, using [child processes](/config/pool). Each test file runs in its own isolated context, so your test files don't share state with each other. This prevents tests in different files from accidentally interfering.
Tests **within** a single file run sequentially by default, which is usually what you want since tests in the same file often share setup code. If your tests are truly independent, you can opt into running them concurrently with [`test.concurrent`](/api/test#concurrent) to speed things up. See the [Parallelism](/guide/parallelism) guide for more details on controlling test execution.
---
---
url: /guide/learn/matchers.md
---
# Using Matchers
Vitest uses `expect` with "matchers" to assert that values meet certain conditions. This page covers the matchers you'll use most often. For the complete list, see the [Expect API Reference](/api/expect).
## Common Matchers
The simplest way to test a value is with exact equality. When you write `expect(2 + 2).toBe(4)`, the [`toBe`](/api/expect#tobe) matcher checks that the value is exactly `4` using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is).
```js
import { expect, test } from 'vitest'
test('two plus two is four', () => {
expect(2 + 2).toBe(4)
})
```
This works great for primitive values like numbers, strings, and booleans. But when you're comparing objects, `toBe` checks *identity* (whether they're the exact same object in memory), not whether they have the same shape. That's where [`toEqual`](/api/expect#toequal) comes in. It recursively compares every field of an object or element of an array, ignoring object identity:
```js
test('object assignment', () => {
const data = { one: 1 }
data.two = 2
expect(data).toEqual({ one: 1, two: 2 })
})
```
Here's an example that shows the difference more clearly. Two objects with the same content are `toEqual` but not `toBe`:
```js
test('toBe vs toEqual', () => {
const a = { name: 'Alice' }
const b = { name: 'Alice' }
// These are different objects in memory
expect(a).not.toBe(b)
// But they have the same structure
expect(a).toEqual(b)
})
```
There's also [`toStrictEqual`](/api/expect#tostrictequal), which is stricter than `toEqual` in three ways: it checks `undefined` properties, distinguishes sparse arrays from `undefined` values, and verifies that objects have the same type (not just the same shape):
```js
test('toEqual vs toStrictEqual', () => {
// toEqual ignores undefined properties
expect({ a: 1 }).toEqual({ a: 1, b: undefined })
// toStrictEqual catches them
expect({ a: 1 }).not.toStrictEqual({ a: 1, b: undefined })
// toEqual doesn't check object types
class User {
constructor(name) {
this.name = name
}
}
expect(new User('Alice')).toEqual({ name: 'Alice' })
expect(new User('Alice')).not.toStrictEqual({ name: 'Alice' })
})
```
::: tip
A good rule of thumb: use `toBe` for primitives (numbers, strings, booleans), `toEqual` for comparing structure, and `toStrictEqual` when you also care about types and explicit `undefined` values.
:::
You can also negate any matcher by inserting `.not` before it. This is useful when you want to verify that something is *not* the case:
```js
test('adding positive numbers is not zero', () => {
expect(1 + 2).not.toBe(0)
})
```
## Truthiness
In tests you sometimes need to distinguish between `undefined`, `null`, and `false`. Other times you don't care about the exact value and just want to know if something is truthy or falsy. Vitest provides matchers for both situations:
* [`toBeNull`](/api/expect#tobenull) matches only `null`
* [`toBeUndefined`](/api/expect#tobeundefined) matches only `undefined`
* [`toBeDefined`](/api/expect#tobedefined) is the opposite of `toBeUndefined`. It passes for anything that isn't `undefined`
* [`toBeTruthy`](/api/expect#tobetruthy) matches anything that an `if` statement would treat as true
* [`toBeFalsy`](/api/expect#tobefalsy) matches anything that an `if` statement would treat as false
You should pick the matcher that most precisely describes what you're checking. Using `toBeTruthy` when you really mean `toBeDefined` can hide bugs, because `0` and `""` are both defined but falsy.
```js
test('null checks', () => {
const n = null
expect(n).toBeNull()
expect(n).toBeDefined()
expect(n).toBeFalsy()
expect(n).not.toBeTruthy()
expect(n).not.toBeUndefined()
})
test('zero', () => {
const z = 0
expect(z).toBeDefined() // passes: 0 is defined
expect(z).toBeFalsy() // passes: 0 is falsy
expect(z).not.toBeNull() // passes: 0 is not null
})
```
## Numbers
Most number comparisons are straightforward. Vitest provides the matchers you'd expect for greater-than, less-than, and equality checks:
```js
test('number comparisons', () => {
const value = 2 + 2
expect(value).toBeGreaterThan(3)
expect(value).toBeGreaterThanOrEqual(3.5)
expect(value).toBeLessThan(5)
expect(value).toBeLessThanOrEqual(4.5)
// For exact equality, both toBe and toEqual work the same for numbers
expect(value).toBe(4)
expect(value).toEqual(4)
})
```
There is one common gotcha with floating point arithmetic. In JavaScript, `0.1 + 0.2` doesn't equal `0.3` exactly (it's `0.30000000000000004`). This means a `toBe(0.3)` check will fail. Use [`toBeCloseTo`](/api/expect#tobecloseto) instead, which compares numbers within a small rounding error:
```js
test('adding floating point numbers', () => {
const value = 0.1 + 0.2
// This won't work because of floating point rounding
// expect(value).toBe(0.3)
// This works
expect(value).toBeCloseTo(0.3)
})
```
## Strings
You can test strings against regular expressions with [`toMatch`](/api/expect#tomatch). This is especially handy when you care about a pattern rather than an exact value, like checking that an error message contains a certain word or that a URL matches a particular format:
```js
test('there is no I in team', () => {
expect('team').not.toMatch(/I/)
})
test('version string matches semver format', () => {
expect('vitest@1.0.0').toMatch(/vitest@\d+\.\d+\.\d+/)
})
```
## Arrays and Iterables
[`toContain`](/api/expect#tocontain) checks that an array (or any iterable, like a `Set`) includes a particular item. It uses `===` for comparison, so it works well for primitives:
```js
test('the shopping list has milk in it', () => {
const shoppingList = ['milk', 'bread', 'eggs', 'butter']
expect(shoppingList).toContain('milk')
expect(new Set(shoppingList)).toContain('milk')
})
```
If you need to check that an array contains an object with a particular structure, use [`toContainEqual`](/api/expect#tocontainequal) instead. It works like `toEqual` but for individual items inside an array.
## Objects
When testing objects, you often want to check only a few important fields without specifying every property. [`toMatchObject`](/api/expect#tomatchobject) lets you do exactly that. It verifies that the object contains at least the properties you specify, and ignores any additional ones:
```js
test('user has expected fields', () => {
const user = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
createdAt: '2024-01-01'
}
// We only care about name and email here
expect(user).toMatchObject({
name: 'Alice',
email: 'alice@example.com',
})
})
```
For checking individual properties, especially nested ones, [`toHaveProperty`](/api/expect#tohaveproperty) is more readable. You pass a dot-separated path and optionally an expected value:
```js
test('object has property', () => {
const user = {
name: 'Alice',
address: { city: 'Paris', zip: '75001' }
}
expect(user).toHaveProperty('name')
expect(user).toHaveProperty('name', 'Alice')
expect(user).toHaveProperty('address.city', 'Paris')
expect(user).toHaveProperty('address.zip')
})
```
## Asymmetric Matchers
Sometimes you don't know the exact value, but you know its type or shape. Asymmetric matchers let you describe what a value should *look like* without pinning down the exact content. They work inside any matcher that does deep comparison, like `toEqual` or `toMatchObject`:
```js
test('user has the right shape', () => {
const user = createUser('Alice')
expect(user).toEqual({
id: expect.any(Number),
name: 'Alice',
email: expect.stringContaining('@'),
roles: expect.arrayContaining(['viewer']),
})
})
```
The most common asymmetric matchers are:
* [`expect.any(Constructor)`](/api/expect#expect-any) matches any value created with the given constructor (e.g., `Number`, `String`, `Array`)
* [`expect.stringContaining(str)`](/api/expect#expect-stringcontaining) matches a string that includes the given substring
* [`expect.stringMatching(regex)`](/api/expect#expect-stringmatching) matches a string against a regular expression
* [`expect.arrayContaining(arr)`](/api/expect#expect-arraycontaining) matches an array that includes all items in the expected array (order doesn't matter, extra items are allowed)
* [`expect.objectContaining(obj)`](/api/expect#expect-objectcontaining) matches an object that includes at least the specified properties
## Exceptions
To verify that a function throws an error, use [`toThrow`](/api/expect#tothrow). You need to wrap the call in another function so that Vitest can catch the error instead of letting it crash the test:
```js
function compileCode(code) {
if (code === '') {
throw new Error('Cannot compile empty string')
}
return code
}
test('compiling an empty string throws', () => {
// Check that it throws at all
expect(() => compileCode('')).toThrow()
// Check the error message
expect(() => compileCode('')).toThrow('Cannot compile empty string')
// Check the message with a regex
expect(() => compileCode('')).toThrow(/empty string/)
})
```
::: tip
The wrapping function `() => compileCode('')` is important. If you wrote `expect(compileCode('')).toThrow()`, the error would be thrown *before* `expect` gets a chance to catch it, and the test would fail with an unhandled error instead.
:::
## Soft Assertions
Normally, a failing assertion stops the test immediately. That's useful most of the time, but sometimes you want to check several independent things and see all the failures at once rather than fixing them one by one.
[`expect.soft`](/api/expect#soft) does exactly that. It records the failure but lets the test keep running:
```js
test('check multiple fields', () => {
const user = { name: 'Alice', age: 30, role: 'admin' }
expect.soft(user.name).toBe('Alice')
expect.soft(user.age).toBe(25) // this fails but execution continues
expect.soft(user.role).toBe('admin')
// the test report will show that age didn't match
})
```
This is especially useful for validating the shape of an API response or a complex object where multiple fields might be wrong at the same time.
---
---
url: /guide/learn/async.md
---
# Testing Asynchronous Code
JavaScript code frequently runs asynchronously. Whether you're fetching data, reading files, or waiting on timers, Vitest needs to know when the code it is testing has completed before moving on to the next test. Here are the patterns you'll use most often.
## Async/Await
The most straightforward approach is to make your test function `async`. Vitest will automatically wait for the returned promise to resolve before considering the test complete. If the promise rejects, the test fails with the rejection reason.
```js
import { expect, test } from 'vitest'
function fetchUser(id) {
return Promise.resolve({ id, name: 'Alice' })
}
test('fetches user by id', async () => {
const user = await fetchUser(1)
expect(user.name).toBe('Alice')
})
```
This is the pattern you'll use the vast majority of the time. It reads just like synchronous code, and errors propagate naturally through `await`.
## Resolves and Rejects
Sometimes you'd rather assert on a promise directly instead of `await`-ing it into a variable first. The [`.resolves`](/api/expect#resolves) and [`.rejects`](/api/expect#rejects) helpers let you do this. They unwrap the promise and then apply the matcher to the resolved or rejected value:
```js
test('resolves to Alice', async () => {
await expect(fetchUser(1)).resolves.toMatchObject({ name: 'Alice' })
})
test('rejects with an error', async () => {
await expect(fetchInvalidUser()).rejects.toThrow('User not found')
})
```
::: warning
Don't forget the `await` before `expect`. Vitest will detect unawaited assertions and fail the test at the end of it.
:::
## Assertion Counting
With async code, there's a subtle risk: an assertion inside a callback or `.then()` chain might never execute, and the test would still pass because no assertion failed. [`expect.hasAssertions()`](/api/expect#hasassertions) guards against this by verifying that at least one assertion ran during the test:
```js
test('callback is invoked', async () => {
expect.hasAssertions()
const data = await fetchData()
data.items.forEach((item) => {
expect(item.id).toBeDefined()
})
// if data.items is empty, the test fails instead of silently passing
})
```
When you know exactly how many assertions should run, [`expect.assertions(n)`](/api/expect#assertions) is more precise:
```js
test('both callbacks are called', async () => {
expect.assertions(2)
await Promise.all([
fetchUser(1).then(user => expect(user.name).toBe('Alice')),
fetchUser(2).then(user => expect(user.name).toBe('Bob')),
])
})
```
In most cases, `async`/`await` with direct assertions is clear enough and you don't need assertion counting. It's most useful when assertions are inside callbacks, loops, or conditional branches where you want to guarantee they actually executed.
::: tip
If you want every test in your project to require at least one assertion, enable [`expect.requireAssertions`](/config/expect#expect-requireassertions) in your config instead of adding `expect.hasAssertions()` to each test manually.
:::
## Callbacks
Some older APIs use callbacks instead of promises. Since Vitest works with promises, the simplest approach is to wrap the callback in a `Promise`:
```js
function fetchData(callback) {
setTimeout(callback, 100, 'peanut butter')
}
test('the data is peanut butter', async () => {
const data = await new Promise((resolve) => {
fetchData(resolve)
})
expect(data).toBe('peanut butter')
})
```
This pattern works for any callback-based API. Pass `resolve` as the success callback, and the test will wait until the callback is invoked.
::: tip
Most modern Node.js APIs (such as `fs/promises` and `fetch`) support promises natively, so you can use `async`/`await` directly. The callback wrapping pattern above is mainly useful for older libraries that haven't adopted promises yet.
:::
## Timeouts
By default, each test has a 5-second timeout. If a test takes longer than that (perhaps because a promise never resolves, or a network request hangs), it will fail with a timeout error. This prevents your test suite from getting stuck indefinitely.
You can set a [custom timeout](/api/test#timeout) as the third argument to `test`, which is useful for tests that legitimately need more time:
```js
test('long-running operation', async () => {
await someSlowOperation()
}, 10_000) // 10 seconds
```
If you find yourself needing longer timeouts across many tests, you can change the default for all tests with the [`testTimeout`](/config/testtimeout) config option:
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
testTimeout: 10_000,
},
})
```
## Unhandled Rejections
By default, Vitest reports unhandled promise rejections as errors in the test run. If a promise rejects somewhere in your code and nothing catches it, the test run will fail, even if all your assertions passed. This is intentional: unhandled rejections usually indicate real bugs, like a forgotten `await` or a fire-and-forget promise that silently fails.
```js
test('this causes an unhandled rejection error', () => {
// This promise rejects but is never awaited or caught
Promise.reject(new Error('oops'))
})
```
To fix this, make sure you `await` all promises or catch expected rejections:
```js
test('handle the rejection', async () => {
// Either await the promise
await expect(Promise.reject(new Error('oops'))).rejects.toThrow('oops')
// Or catch it explicitly if you don't need to assert on it
Promise.reject(new Error('expected')).catch(() => {})
})
```
If your code intentionally produces unhandled rejections, you can filter specific errors with [`onUnhandledError`](/config/onunhandlederror) or disable the check entirely with [`dangerouslyIgnoreUnhandledErrors`](/config/dangerouslyignoreunhandlederrors).
---
---
url: /guide/learn/setup-teardown.md
---
# Setup and Teardown
Often while writing tests, you need to do some work before tests run (initialize data, connect to a database, start a server) and clean up afterwards. Rather than duplicating this code in every test, Vitest provides lifecycle hooks that run automatically at the right time.
## Repeating Setup for Each Test
The most common hooks are [`beforeEach`](/api/hooks#beforeeach) and [`afterEach`](/api/hooks#aftereach). As the names suggest, `beforeEach` runs before every test in the file, and `afterEach` runs after every test, even if the test fails. This makes them perfect for ensuring each test starts with a known state.
```js
import { afterEach, beforeEach, expect, test } from 'vitest'
let items
beforeEach(() => {
items = ['apple', 'banana', 'cherry']
})
afterEach(() => {
items = []
})
test('items starts with 3 fruits', () => {
expect(items).toHaveLength(3)
})
test('can remove an item', () => {
items.pop()
expect(items).toHaveLength(2)
})
test('can add an item', () => {
items.push('date')
expect(items).toHaveLength(4)
// beforeEach reset the array to 3 items before this test ran,
// proving that mutations from the previous test do not leak.
})
```
Without these hooks, mutations like `pop` or `push` from earlier tests would affect subsequent ones, which is a classic source of flaky tests, while the hooks guarantee clean state for every test.
## One-Time Setup
Some setup is too expensive to repeat for every test. If you need to connect to a database, start a server, or load a large file, doing that before every test would slow your suite down dramatically. That's what [`beforeAll`](/api/hooks#beforeall) and [`afterAll`](/api/hooks#afterall) are for. They run once for the entire file:
```js
import { afterAll, beforeAll, expect, test } from 'vitest'
let db
beforeAll(async () => {
db = await connectToDatabase()
})
afterAll(async () => {
await db.close()
})
test('can query users', async () => {
const users = await db.query('SELECT * FROM users')
expect(users.length).toBeGreaterThan(0)
})
test('can query products', async () => {
const products = await db.query('SELECT * FROM products')
expect(products.length).toBeGreaterThan(0)
})
```
The database connection is created once, shared across all tests, and then closed when the file finishes running.
## Scoping with `describe`
Hooks defined inside a `describe` block only apply to the tests within that block. Top-level hooks apply to every test in the file. This lets you set up different state for different groups of tests:
```js
import { beforeEach, describe, expect, test } from 'vitest'
describe('math operations', () => {
let value
beforeEach(() => {
value = 0
})
test('can add', () => {
value += 5
expect(value).toBe(5)
})
test('can subtract', () => {
value -= 3
expect(value).toBe(-3) // value was reset to 0 by beforeEach
})
})
describe('string operations', () => {
let text
beforeEach(() => {
text = 'hello'
})
test('can uppercase', () => {
expect(text.toUpperCase()).toBe('HELLO')
})
})
```
Each `describe` block has its own `beforeEach` that only affects the tests inside it. The string tests don't know or care about the `value` variable, and vice versa.
## Execution Order
When you have hooks at multiple levels, it's helpful to understand the order they run in. Top-level hooks wrap around inner hooks, forming a nesting structure:
```js
import { afterAll, afterEach, beforeAll, beforeEach, describe, test } from 'vitest'
beforeAll(() => console.log('1 - beforeAll'))
afterAll(() => console.log('8 - afterAll'))
beforeEach(() => console.log('2 - beforeEach'))
afterEach(() => console.log('5 - afterEach'))
describe('suite', () => {
beforeEach(() => console.log('3 - inner beforeEach'))
afterEach(() => console.log('4 - inner afterEach'))
test('first test', () => {
console.log(' first test')
})
test('second test', () => {
console.log(' second test')
})
})
```
This produces the following output:
```
1 - beforeAll
2 - beforeEach
3 - inner beforeEach
first test
4 - inner afterEach
5 - afterEach
2 - beforeEach
3 - inner beforeEach
second test
4 - inner afterEach
5 - afterEach
8 - afterAll
```
Notice the pattern: `beforeAll` and `afterAll` run once for the entire suite, while `beforeEach` and `afterEach` repeat for every test. Within each test, outer `beforeEach` runs first (setting up the broadest context), then inner `beforeEach` runs (narrowing the context). After the test, the order reverses: inner `afterEach` cleans up the narrow context first, then outer `afterEach` handles the broader cleanup.
## Cleanup with `onTestFinished`
Sometimes you create a resource inside a test that needs to be cleaned up afterwards. You could use `afterEach`, but that means the cleanup is separated from the setup, which can make the test harder to follow. [`onTestFinished`](/api/hooks#ontestfinished) lets you register a cleanup function right where you create the resource:
```js
import { expect, onTestFinished, test } from 'vitest'
test('creates a temporary file', () => {
const file = createTempFile()
onTestFinished(() => {
deleteTempFile(file)
})
expect(file.exists()).toBe(true)
})
```
A similar pattern works with `beforeEach`. You can return a cleanup function and Vitest will call it after each test. This is especially nice when the setup and teardown are closely related:
```js
import { beforeEach } from 'vitest'
beforeEach(() => {
const server = startServer()
return () => {
server.close()
}
})
```
## Fixtures with `test.extend`
The examples above use `let` variables and `beforeEach` to set up shared state. This works, but it has some downsides: the variable declarations are separated from the initialization, the types require explicit annotation, and it's easy to forget to clean up.
Vitest offers a better pattern for this with [`test.extend`](/guide/test-context#extend-test-context). You define reusable **fixtures** that are automatically created for each test and cleaned up afterwards:
```js [my-test.js]
import { test as baseTest } from 'vitest'
export const test = baseTest
.extend('db', async ({}, { onCleanup }) => {
const db = await createDatabase()
onCleanup(() => db.close())
return db
})
.extend('user', async ({ db }) => {
return await db.createUser({ name: 'Alice' })
})
```
```js [my-test.test.js]
import { expect } from 'vitest'
import { test } from './my-test.js'
test('user is created', ({ db, user }) => {
expect(user.name).toBe('Alice')
})
```
Fixtures are only initialized when a test actually uses them (by destructuring them from the context), and they can depend on each other. This makes them a great alternative to `beforeEach`/`afterEach` for most setup and teardown patterns.
See the [Test Context](/guide/test-context) guide for the full details on fixtures, scoping, and overrides.
## Setup Files
If you have setup code that should run before every test file in your project (things like polyfills, global configuration, or custom matchers), you can put it in a setup file and point to it with the [`setupFiles`](/config/setupfiles) config option:
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
setupFiles: ['./test/setup.js'],
},
})
```
```js [test/setup.js]
// This runs before every test file
import { expect } from 'vitest'
import { customMatchers } from './custom-matchers.js'
expect.extend(customMatchers)
```
Unlike `beforeAll`, which runs once per file, setup files run in a separate phase before the test file even starts being collected. This makes them the right place for things like extending the `expect` API or configuring global polyfills.
::: tip
For advanced cases where your test needs to run *inside* a wrapping context (like a database transaction or a tracing span), see the [`aroundEach`](/api/hooks#aroundeach) and [`aroundAll`](/api/hooks#aroundall) hooks. For the complete lifecycle picture, see [Test Run Lifecycle](/guide/lifecycle).
:::
---
---
url: /guide/learn/mock-functions.md
---
# Mock Functions
When writing tests, you often need to replace a real function or module with a controlled version. This is called **mocking**. There are several reasons you might want to do this: maybe the real function makes network requests that would slow down your tests, or maybe you need to simulate an error that's hard to trigger with real code. Mock functions let you control what a dependency returns, observe how it was called, and isolate the code under test from side effects.
Vitest provides mocking utilities through the [`vi`](/api/vi) object.
## Creating Mock Functions
The simplest way to create a mock is with [`vi.fn()`](/api/vi#vi-fn). This gives you a function that does nothing by default (returns `undefined`), but tracks every call made to it:
```js
import { expect, test, vi } from 'vitest'
test('mock function basics', () => {
const getApples = vi.fn()
// Call it
getApples()
// Check it was called
expect(getApples).toHaveBeenCalled()
expect(getApples).toHaveBeenCalledTimes(1)
// By default, a mock returns undefined
expect(getApples()).toBeUndefined()
})
```
## Mock Return Values
A mock that always returns `undefined` isn't very useful on its own. You'll usually want to control what it returns so you can test how your code reacts to different values:
```js
import { expect, test, vi } from 'vitest'
test('mock return values', () => {
const getApples = vi.fn()
// Always return this value
getApples.mockReturnValue(10)
expect(getApples()).toBe(10)
// Return this value only once, then fall back to the default
getApples.mockReturnValueOnce(20)
expect(getApples()).toBe(20) // 20 (one-time)
expect(getApples()).toBe(10) // back to default
})
```
If the function you're mocking is async, use [`mockResolvedValue`](/api/mock#mockresolvedvalue) and [`mockRejectedValue`](/api/mock#mockrejectedvalue) to control the promise outcome:
```js
test('mock async return values', async () => {
const fetchUser = vi.fn()
fetchUser.mockResolvedValue({ name: 'Alice' })
const user = await fetchUser()
expect(user.name).toBe('Alice')
fetchUser.mockRejectedValue(new Error('Not found'))
await expect(fetchUser()).rejects.toThrow('Not found')
})
```
::: tip
`mockReturnValue` always returns the same value regardless of the arguments the mock receives. If you need argument-specific return values, [`vi.when`](/api/vi#vi-when) lets you attach different behaviors for different argument combinations without writing your own `if/else` logic. See the [Conditional Mocking](/guide/recipes/conditional-mocking) recipe for details.
:::
## Mock Implementation
Sometimes you need more than a fixed return value. You want the mock to actually do something with its arguments. [`mockImplementation`](/api/mock#mockimplementation) lets you provide a full replacement function:
```js
import { expect, test, vi } from 'vitest'
test('mock with custom implementation', () => {
const add = vi.fn()
add.mockImplementation((a, b) => a + b)
expect(add(1, 2)).toBe(3)
expect(add(10, 20)).toBe(30)
})
```
As a shorthand, you can pass the implementation directly to `vi.fn()`:
```js
const add = vi.fn((a, b) => a + b)
```
## Inspecting Calls
One of the most powerful things about mock functions is that they remember every call made to them. You can assert on how many times a function was called, what arguments it received, and what it returned:
```js
import { expect, test, vi } from 'vitest'
test('inspecting mock calls', () => {
const greet = vi.fn()
greet('Alice')
greet('Bob', 'Charlie')
// Number of calls
expect(greet).toHaveBeenCalledTimes(2)
// Check specific arguments
expect(greet).toHaveBeenCalledWith('Alice')
expect(greet).toHaveBeenCalledWith('Bob', 'Charlie')
// Check the arguments of a specific call by position
expect(greet).toHaveBeenNthCalledWith(1, 'Alice')
expect(greet).toHaveBeenLastCalledWith('Bob', 'Charlie')
// Access the raw call data
expect(greet.mock.calls).toEqual([
['Alice'],
['Bob', 'Charlie'],
])
})
```
The `.mock` property gives you full access to the call history. In addition to `.mock.calls`, you can also inspect `.mock.results` to see what the mock returned (or threw) on each call:
```js
const double = vi.fn(x => x * 2)
double(5)
double(10)
expect(double.mock.results).toEqual([
{ type: 'return', value: 10 },
{ type: 'return', value: 20 },
])
```
::: warning
`.mock.calls` stores references to the arguments, not copies. If you pass an object to a mock and then mutate it afterwards, the recorded call will reflect the mutated state, not the state at the time of the call:
```js
const fn = vi.fn()
const obj = { count: 1 }
fn(obj)
obj.count = 2
// ❌ This fails! mock.calls[0][0].count is now 2, not 1
expect(fn).toHaveBeenCalledWith({ count: 1 })
```
If you need to assert on the original values, you can use `mockImplementation` to capture a clone at call time:
```js
const calls = []
const fn = vi.fn((obj) => {
calls.push(structuredClone(obj))
})
const obj = { count: 1 }
fn(obj)
obj.count = 2
expect(calls[0]).toEqual({ count: 1 }) // ✅ passes
```
Alternatively, you can make your assertion before the mutation happens.
:::
## Spying on Methods
[`vi.spyOn`](/api/vi#vi-spyon) is different from `vi.fn()` in an important way. Instead of creating a brand new function, it wraps an *existing* method on an object. The original implementation still works by default, but you can observe every call and optionally override the behavior:
```js
import { expect, test, vi } from 'vitest'
const calculator = {
add(a, b) {
return a + b
},
}
test('spy on a method', () => {
const spy = vi.spyOn(calculator, 'add')
// The original implementation still works
expect(calculator.add(1, 2)).toBe(3)
// But we can observe calls
expect(spy).toHaveBeenCalledWith(1, 2)
expect(spy).toHaveBeenCalledTimes(1)
})
test('spy can override implementation', () => {
const spy = vi.spyOn(calculator, 'add')
spy.mockReturnValue(42)
expect(calculator.add(1, 2)).toBe(42)
})
```
This is particularly useful when you want to verify that your code calls a method correctly without replacing the method's behavior entirely.
## Resetting Mocks
Mock functions accumulate state as tests run. They remember every call, every return value, and any custom implementation you've set. If you don't reset them between tests, this state can leak and cause confusing failures. Vitest provides three levels of cleanup:
* **[`mockClear()`](/api/mock#mockclear)** clears the recorded call history and return values, but keeps any custom implementation you've set
* **[`mockReset()`](/api/mock#mockreset)** does everything `mockClear` does, and also removes any custom implementation, returning the mock to its default state
* **[`mockRestore()`](/api/mock#mockrestore)** is specifically for spies created with `vi.spyOn`. It restores the original object method, effectively undoing the spy. On `vi.fn()` mocks, it behaves the same as `mockReset`
In practice, the easiest approach is to restore all mocks automatically after each test:
```js
import { afterEach, expect, test, vi } from 'vitest'
const calculator = {
add: (a, b) => a + b,
}
afterEach(() => {
vi.restoreAllMocks()
})
test('spy is restored after the test', () => {
const spy = vi.spyOn(calculator, 'add').mockReturnValue(42)
expect(calculator.add(1, 2)).toBe(42)
// afterEach will restore calculator.add to the original implementation
})
```
Even better, you can configure this globally with the [`restoreMocks`](/config/restoremocks) option so you don't need the `afterEach` at all:
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
restoreMocks: true,
},
})
```
## Mocking Modules
Sometimes you need to replace an [entire module](/guide/mocking/modules) rather than a single function. For example, a database client or a logger that you don't want running during tests. [`vi.mock`](/api/vi#vi-mock) lets you replace a module's exports with mock implementations:
```js
import { expect, test, vi } from 'vitest'
import { getUser } from './db.js'
vi.mock(import('./db.js'), () => ({
getUser: vi.fn(),
}))
test('mock a module', () => {
vi.mocked(getUser).mockReturnValue({ name: 'Alice' })
const user = getUser(1)
expect(user.name).toBe('Alice')
expect(getUser).toHaveBeenCalledWith(1)
})
```
::: warning
[`vi.mock`](/api/vi#vi-mock) calls are hoisted to the top of the file. They run before any imports. This means the mocked version is in place by the time your test code runs.
:::
::: warning
Always pass `import('./db.js')` rather than a plain string `'./db.js'`. When you use `import()`, TypeScript can infer the module's types, so the factory function's return value is type-checked and `importOriginal` returns the correctly typed module. As a bonus, if you rename or move the file in your IDE, the import path will be updated automatically. If you use a string, you lose both the type safety and the automatic refactoring.
:::
Vitest has comprehensive guides for specific mocking scenarios:
* [Mocking Functions](/guide/mocking/functions)
* [Mocking Modules](/guide/mocking/modules)
* [Mocking Timers](/guide/mocking/timers)
* [Mocking Dates](/guide/mocking/dates)
* [Mocking Globals](/guide/mocking/globals)
* [Mocking Requests](/guide/mocking/requests)
* [Mocking the File System](/guide/mocking/file-system)
* [Mocking Classes](/guide/mocking/classes)
---
---
url: /guide/learn/snapshots.md
---
# Snapshot Testing
Snapshot tests capture the output of a piece of code and save it to a file. On subsequent runs, the output is compared against the saved snapshot. If the output changes, the test fails. Either the change is a bug, or the snapshot needs to be updated.
This approach is particularly useful when you're testing something that produces structured output: a function that returns a complex object, a component that renders HTML, or an error formatter that produces multi-line messages. Writing manual assertions for every field or line would be tedious and fragile. Instead, you capture the entire output once, and let Vitest tell you if it ever changes.
## Your First Snapshot
To create a snapshot test, pass a value to [`toMatchSnapshot()`](/api/expect#tomatchsnapshot):
```js
import { expect, test } from 'vitest'
function generateGreeting(name) {
return {
message: `Hello, ${name}!`,
timestamp: null,
version: 2,
}
}
test('generates a greeting', () => {
expect(generateGreeting('Alice')).toMatchSnapshot()
})
```
The first time you run this test, there's no existing snapshot to compare against, so Vitest creates one. It stores the snapshot in a `__snapshots__` directory next to your test file:
```
__snapshots__/
example.test.js.snap
```
If you open that file, you'll see a serialized representation of the value:
```js
exports['generates a greeting 1'] = `
{
"message": "Hello, Alice!",
"timestamp": null,
"version": 2,
}
`
```
From now on, every time you run this test, Vitest serializes the output of `generateGreeting('Alice')` and compares it character-by-character against this stored snapshot. If the output changes (say, someone modifies the message format or bumps the version number), the test fails and shows a clear diff of what changed.
::: tip
Commit your snapshot files to version control. They serve as a record of the expected output and should be reviewed in code review just like any other test assertion.
:::
## Inline Snapshots
External snapshot files work well, but they mean you have to jump to a different file to see what the expected output actually looks like. For smaller values, it's often more convenient to keep the snapshot right in your test file with [`toMatchInlineSnapshot()`](/api/expect#tomatchinlinesnapshot).
Start by writing the assertion without any argument:
```js
test('generates a greeting', () => {
expect(generateGreeting('Alice')).toMatchInlineSnapshot()
})
```
When you run the test, Vitest will **automatically fill in** the snapshot as a string argument:
```js
test('generates a greeting', () => {
expect(generateGreeting('Alice')).toMatchInlineSnapshot(`
{
"message": "Hello, Alice!",
"timestamp": null,
"version": 2,
}
`)
})
```
Now the expected output lives right next to the code that produces it. You can read the test and immediately understand what `generateGreeting` is expected to return. When the output changes, Vitest updates the string in place, so you don't need to manage separate snapshot files.
Inline snapshots are great for small, focused values. For large outputs (like a full HTML page), external snapshots or file snapshots are a better fit.
::: tip
Unlike external snapshots, inline snapshots don't create separate `.snap` files. The expected value is stored directly in your test file as the argument to `toMatchInlineSnapshot()`, so there's nothing extra to commit.
:::
## Updating Snapshots
When you intentionally change the output of your code, existing snapshots will be outdated and the tests will fail. This is by design; it's the whole point of snapshot testing. But once you've verified that the new output is correct, you need to update the snapshots.
There are several ways to do this:
* **In watch mode**: press `u` in the terminal to update all failed snapshots
* **From the CLI**: run `vitest -u` or `vitest --update` to update snapshots and exit
* **In VS Code**: use the "Update Snapshots" command on the test gutter icon from the [Vitest extension](https://vitest.dev/vscode)
```bash
vitest -u
```
For inline snapshots, Vitest modifies your test file directly with the new values. For external snapshots, it rewrites the `.snap` file.
::: warning
Be careful when updating snapshots. Always review the diff to confirm the changes are intentional and not a bug. It's easy to accidentally accept a broken output by blindly pressing `u`.
:::
## File Snapshots
Sometimes the output you're testing is large enough that even an external `.snap` file feels awkward, or you want to view the snapshot with proper syntax highlighting in your editor. [`toMatchFileSnapshot()`](/api/expect#tomatchfilesnapshot) lets you save the snapshot to a file with any extension you want:
```js
test('renders the component', async () => {
const html = renderComponent()
await expect(html).toMatchFileSnapshot('./fixtures/component.html')
})
```
The snapshot is stored as a plain `.html` file that you can open in a browser, view with syntax highlighting, or diff with standard tools. This works well for HTML, SVG, CSS, generated code, or any output where the file format matters for readability.
## When to Use Snapshots
Snapshots shine when you're working with structured, serializable output that would be painful to assert on manually. Some common use cases:
* A function that returns a complex configuration object with many nested fields
* HTML or markup generated by a rendering function or template engine
* Error messages that include formatted stack traces or context information
* CLI output or log messages with specific formatting
* JSON API responses where you want to catch any unexpected field changes
On the other hand, snapshots are not always the best tool. If the output changes frequently (for instance, it includes timestamps or random IDs), you'll spend more time updating snapshots than they save you. And if you only care about one or two specific fields, a targeted assertion like [`toMatchObject`](/api/expect#tomatchobject) or [`toHaveProperty`](/api/expect#tohaveproperty) expresses your intent more clearly than a snapshot that captures everything.
The general rule: use snapshots when you want to protect against *any* change in the output, and use targeted assertions when you only care about *specific* properties.
## Handling Dynamic Values
If your output includes values that change every run (like timestamps or IDs), you can use property matchers to pin the structure while ignoring volatile fields. Pass an object with asymmetric matchers as the first argument to `toMatchSnapshot()` or `toMatchInlineSnapshot()`:
```js
test('user snapshot with dynamic fields', () => {
const user = createUser('Alice')
expect(user).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(Date),
})
})
```
The `id` and `createdAt` fields are checked against the matchers (any number, any date) instead of being compared to a stored value. All other fields are snapshotted as usual.
## Error Snapshots
A common use of inline snapshots is capturing error messages. [`toThrowErrorMatchingInlineSnapshot`](/api/expect#tothrowerrormatchinginlinesnapshot) combines `toThrow` with `toMatchInlineSnapshot` so you can snapshot the error message without a separate `.snap` file:
```js
test('throws on invalid input', () => {
expect(() => parse('')).toThrowErrorMatchingInlineSnapshot(
`[Error: Unexpected end of input at position 0]`
)
})
```
This is especially handy for verifying that error messages are clear and don't accidentally change. Like other inline snapshots, Vitest fills in the string on the first run and updates it when you press `u`.
::: tip
For custom snapshot serializers, snapshot matchers, and advanced configuration, see the [Snapshot](/guide/snapshot) guide.
:::
---
---
url: /guide/learn/testing-in-practice.md
---
# Testing in Practice
The previous pages covered the Vitest API: assertions, mocking, snapshots, and test lifecycle hooks. This page focuses on applying those tools to real code. It covers how to decide what to test, how to structure tests effectively, and how to organize test files as a project grows.
## What to Test
When you sit down to write tests for a function or module, start by thinking about its **contract**: what does it promise to do for the code that calls it? The contract is defined by its inputs (arguments, configuration) and its outputs (return values, side effects, errors). These are the things your tests should verify.
Consider a `formatPrice` function:
```js [formatPrice.js]
export function formatPrice(amount, currency) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount)
}
```
The contract here is: given an amount and a currency code, return a formatted price string. Good tests for this function would cover:
```js [formatPrice.test.js]
import { expect, test } from 'vitest'
import { formatPrice } from './formatPrice.js'
test('formats USD prices', () => {
expect(formatPrice(10, 'USD')).toBe('$10.00')
})
test('formats EUR prices', () => {
expect(formatPrice(10, 'EUR')).toBe('€10.00')
})
test('handles zero', () => {
expect(formatPrice(0, 'USD')).toBe('$0.00')
})
test('handles negative amounts', () => {
expect(formatPrice(-5.5, 'USD')).toBe('-$5.50')
})
test('rounds to two decimal places', () => {
expect(formatPrice(10.999, 'USD')).toBe('$11.00')
})
```
Notice what these tests *don't* do. They don't check which internal `Intl.NumberFormat` options were passed, or whether an intermediate variable was set. They only check the output.
::: tip
A good rule of thumb: if someone refactors the internals but the output stays the same, should the test break? If it would, you're probably testing implementation details rather than behavior.
:::
## Structuring a Test
Most tests follow a natural three-part structure, sometimes called "Arrange, Act, Assert":
1. **Set up** the data your test needs
2. **Call** the function or perform the action you're testing
3. **Check** that the result matches your expectations
```js
test('removes an item from the list', () => {
// Set up
const list = new ShoppingList()
list.add('milk')
list.add('bread')
// Act
list.remove('milk')
// Check
expect(list.getItems()).toEqual(['bread'])
})
```
You don't need comments labeling each section. The structure becomes natural once you've written a few tests. The important thing is keeping each test focused on one behavior.
### One Behavior Per Test
If you find yourself writing "and" in a test name ("formats price and handles errors and logs the result"), that's a sign you should split it into separate tests.
### Descriptive Names
Write test names that describe the behavior, not the implementation. "returns formatted price for USD" is better than "calls Intl.NumberFormat with correct options". When a test fails, the name should tell you what broke without having to read the test body.
## Testing Edge Cases
After covering the main behavior, think about the boundaries. What happens at the edges? What inputs are unusual but valid? What should happen when things go wrong?
Here's an example with a `parseAge` function that takes user input and returns a number:
```js [parseAge.js]
export function parseAge(input) {
const age = Number(input)
if (Number.isNaN(age) || age < 0 || age > 150) {
throw new Error(`Invalid age: ${input}`)
}
return Math.floor(age)
}
```
The happy path is straightforward, but the edge cases are where bugs hide:
```js [parseAge.test.js]
import { expect, test } from 'vitest'
import { parseAge } from './parseAge.js'
test('parses a valid age', () => {
expect(parseAge('25')).toBe(25)
})
test('rounds down decimal ages', () => {
expect(parseAge('25.9')).toBe(25)
})
test('handles zero', () => {
expect(parseAge('0')).toBe(0)
})
test('handles the upper boundary', () => {
expect(parseAge('150')).toBe(150)
})
test('throws for negative numbers', () => {
expect(() => parseAge('-1')).toThrow('Invalid age: -1')
})
test('throws for numbers above 150', () => {
expect(() => parseAge('151')).toThrow('Invalid age: 151')
})
test('throws for non-numeric strings', () => {
expect(() => parseAge('abc')).toThrow('Invalid age: abc')
})
test('throws for empty string', () => {
expect(() => parseAge('')).toThrow('Invalid age: ')
})
```
You don't need to test every possible input. Focus on the boundaries (0, 150, 151, -1), the error paths, and the types of inputs your function might realistically receive.
::: tip
If you're unsure whether an edge case matters, ask yourself: could a real user or a real caller trigger this? If yes, test it.
:::
### Property-Based Testing
For functions with a wide range of valid inputs, manually choosing edge cases can only go so far. **Property-based testing** is a technique where you describe the *properties* that should hold for any input, and the testing framework generates hundreds of random inputs to try to find one that breaks.
For example, you might say "for any valid age string, `parseAge` should return a non-negative integer" and let the tool find the counterexample. [fast-check](https://fast-check.dev/) is a popular property-based testing library that integrates well with Vitest. It's an advanced technique, but worth knowing about as your testing needs grow.
## When to Mock
Mocking is a powerful tool, but it's easy to overuse.
### Slow Dependencies
Network requests, file system operations, and database calls can make your tests take seconds instead of milliseconds. Replace them with mocks to keep the feedback loop fast.
For HTTP requests specifically, consider using [Mock Service Worker](https://mswjs.io/) instead of mocking fetch directly. See the [Mocking Requests](/guide/mocking/requests) guide for setup instructions.
### Non-Deterministic Values
If your code depends on the current date, a random number, or a UUID generator, mock those to make your tests predictable. Vitest provides [`vi.useFakeTimers()`](/api/vi#vi-usefaketimers) and [`vi.setSystemTime()`](/api/vi#vi-setsystemtime) for controlling time in tests.
### What Not to Mock
Don't mock the thing you're testing. If you're testing a `UserService`, don't mock the `UserService`. Mock its *dependencies* (the database, the email sender) and let the service itself run for real.
Also, prefer real implementations when they're fast and reliable. If a dependency is a simple in-memory data structure or a pure function, there's no reason to mock it. The closer your tests are to real usage, the more confidence they give you.
::: tip
Only reach for mocks when the real thing is slow, flaky, or has side effects you can't control in a test.
:::
## Fixing Bugs with Tests
When you find a bug, it's tempting to jump straight into the code and fix it. A better approach is to write a failing test first that reproduces the bug, then fix the code and watch the test turn green.
This has several benefits. The test proves the bug is real and not just a misunderstanding. It documents exactly what was broken. And it prevents the same bug from coming back later, because the test will catch it if someone accidentally reintroduces the same problem.
Here's what this looks like in practice. Suppose users report that `parseAge` crashes when given a string with leading spaces like `" 25"`. First, write a test that reproduces the problem:
```js
test('handles leading spaces', () => {
expect(parseAge(' 25')).toBe(25)
})
```
Run it and confirm it fails. Now you know exactly what's broken and have a clear target. Fix the implementation:
```js
export function parseAge(input) {
const age = Number(input.trim())
// ...
}
```
Run the test again. It passes. The bug is fixed, and you have a regression test that will catch it if someone removes the `.trim()` call later.
::: tip
If you use AI agents to fix bugs, configure them to follow the same principle: reproduce the issue with a failing test first, then fix the code. This prevents the agent from "fixing" a bug by changing the test instead of the code, and gives you confidence that the fix actually works.
:::
## Organizing Test Files
There's no single right way to organize tests, but some patterns scale better than others.
### File Layout
The simplest starting point is one test file per source file. For every `utils.js`, there's a `utils.test.js` right next to it. This makes it easy to find the tests for any given piece of code, and most editors will show them side by side in the file tree:
```
src/
utils.js
utils.test.js
formatPrice.js
formatPrice.test.js
```
Some teams prefer a separate `__tests__` or `test` directory instead. Either approach works. The important thing is consistency across the project. Vitest's [`include`](/config/include) pattern matches both layouts by default.
### Grouping with `describe`
When a module exports multiple functions, use `describe` blocks to group the tests for each one. This keeps the test output organized and makes it clear which function a failing test belongs to:
```js
describe('formatPrice', () => {
test('formats USD prices', () => { /* ... */ })
test('handles zero', () => { /* ... */ })
})
describe('parseAmount', () => {
test('parses valid amounts', () => { /* ... */ })
test('throws for invalid input', () => { /* ... */ })
})
```
Avoid nesting `describe` blocks more than one or two levels deep. Deeply nested test trees are hard to read and usually mean the source module is doing too many things at once.
### Splitting Large Files
As a project grows, some test files will inevitably get long. If a test file grows beyond a few hundred lines, consider splitting it by theme or feature area. For example, `userService.test.js` might become `userService.creation.test.js` and `userService.auth.test.js`. This also makes it faster to run a subset of tests during development.
### Naming Tests
Test names matter more than you might expect. When a test fails in CI, the name is often the first thing someone reads. Names like "works correctly" or "handles edge case" don't tell you what broke.
Prefer names that describe the specific behavior: "returns 0 for an empty cart", "throws if the email format is invalid", "preserves existing items when adding a new one". The test output should read like a specification of what the module does.
## A Worked Example
Let's put it all together. Here's a small `TodoList` module:
```js [todoList.js]
let nextId = 1
export function createTodoList() {
const items = []
return {
add(text) {
if (!text.trim()) {
throw new Error('Todo text cannot be empty')
}
const todo = { id: nextId++, text, completed: false }
items.push(todo)
return todo
},
remove(id) {
const index = items.findIndex(item => item.id === id)
if (index === -1) {
throw new Error(`Todo with id ${id} not found`)
}
items.splice(index, 1)
},
toggle(id) {
const todo = items.find(item => item.id === id)
if (!todo) {
throw new Error(`Todo with id ${id} not found`)
}
todo.completed = !todo.completed
},
getAll() {
return items
},
getCompleted() {
return items.filter(item => item.completed)
},
}
}
```
Looking at this code, we can identify the behaviors to test:
* Adding items (the main purpose)
* Adding empty items (should fail)
* Removing items by ID
* Removing items that don't exist (should fail)
* Toggling completion status
* Getting all items vs. completed items
Here's how the test file might look:
```js [todoList.test.js]
import { describe, expect, test } from 'vitest'
import { createTodoList } from './todoList.js'
describe('add', () => {
test('adds a new todo', () => {
const list = createTodoList()
const todo = list.add('Buy groceries')
expect(todo.text).toBe('Buy groceries')
expect(todo.completed).toBe(false)
expect(list.getAll()).toHaveLength(1)
})
test('assigns unique IDs to each todo', () => {
const list = createTodoList()
const first = list.add('First')
const second = list.add('Second')
expect(first.id).not.toBe(second.id)
})
test('throws when text is empty', () => {
const list = createTodoList()
expect(() => list.add('')).toThrow('Todo text cannot be empty')
})
test('throws when text is only whitespace', () => {
const list = createTodoList()
expect(() => list.add(' ')).toThrow('Todo text cannot be empty')
})
})
describe('remove', () => {
test('removes a todo by ID', () => {
const list = createTodoList()
const todo = list.add('Buy groceries')
list.remove(todo.id)
expect(list.getAll()).toHaveLength(0)
})
test('keeps other items when removing one', () => {
const list = createTodoList()
const first = list.add('First')
list.add('Second')
list.remove(first.id)
expect(list.getAll()).toHaveLength(1)
expect(list.getAll()[0].text).toBe('Second')
})
test('throws when ID does not exist', () => {
const list = createTodoList()
expect(() => list.remove(999)).toThrow('Todo with id 999 not found')
})
})
describe('toggle', () => {
test('marks a todo as completed', () => {
const list = createTodoList()
const todo = list.add('Buy groceries')
list.toggle(todo.id)
expect(list.getAll()[0].completed).toBe(true)
})
test('toggles back to incomplete', () => {
const list = createTodoList()
const todo = list.add('Buy groceries')
list.toggle(todo.id)
list.toggle(todo.id)
expect(list.getAll()[0].completed).toBe(false)
})
test('throws when ID does not exist', () => {
const list = createTodoList()
expect(() => list.toggle(999)).toThrow('Todo with id 999 not found')
})
})
describe('getCompleted', () => {
test('returns only completed todos', () => {
const list = createTodoList()
const buy = list.add('Buy groceries')
list.add('Clean house')
list.toggle(buy.id)
const completed = list.getCompleted()
expect(completed).toHaveLength(1)
expect(completed[0].text).toBe('Buy groceries')
})
test('returns empty array when nothing is completed', () => {
const list = createTodoList()
list.add('Buy groceries')
expect(list.getCompleted()).toHaveLength(0)
})
})
```
Each `describe` block focuses on one method. Each test verifies one specific behavior. The test names read like a specification of what the module does. And if any of these tests fail, the name and the assertion will tell you exactly what broke.
::: tip
Notice that we create a fresh `createTodoList()` in every test. This keeps tests independent, which means they can run in any order without affecting each other. If you find yourself repeating the same setup in every test, that's a good candidate for [`beforeEach`](/api/hooks#beforeeach) or a [`test.extend`](/guide/test-context#extend-test-context) fixture.
:::
::: details What about `nextId`?
The `nextId` counter at the top of the module is shared across all calls to `createTodoList()`, including across tests. This means IDs aren't predictable: one test might get IDs 1 and 2, while another gets 3 and 4 depending on execution order. This works fine here because the tests only check *relative* uniqueness (`first.id !== second.id`), not specific ID values. If a test asserted `expect(todo.id).toBe(1)`, it would break depending on which tests ran before it. When you have shared module-level state like this, make sure your tests don't depend on its specific value.
:::
***
If you're building a web application and want to test components in a real browser environment, check out [Component Testing](/guide/browser/component-testing) for testing React, Vue, Svelte, and other UI frameworks.
---
---
url: /guide/learn/debugging-tests.md
---
# Debugging Failing Tests
This page covers how to investigate test failures in Vitest: reading error output, isolating problems, identifying common causes, and using the available debugging tools.
## Reading the Error
When a test fails, Vitest gives you several pieces of information. Let's look at a real failure and break it down:
```ansi
[31mFAIL[39m src/user.test.js [2m>[22m createUser [2m>[22m sets the default role
[31mAssertionError: expected { name: 'Alice', role: 'viewer' } to deeply equal { name: 'Alice', role: 'member' }[39m
[31m- Expected[39m
[32m+ Received[39m
{
"name": "Alice",
[31m- "role": "member",[39m
[32m+ "role": "viewer",[39m
}
[2m❯[22m [36msrc/user.test.js[39m[2m:8:22[22m
[2m 6|[22m [34mtest[39m([32m'sets the default role'[39m, () => {
[2m 7|[22m [35mconst[39m user = [34mcreateUser[39m([32m'Alice'[39m)
[2m 8|[22m [34mexpect[39m(user).[34mtoEqual[39m({ name: [32m'Alice'[39m, role: [32m'member'[39m })
[31m^[39m
[2m 9|[22m })
[2m 10|[22m })
```
There's a lot here, but each part tells you something:
**The header** (`FAIL src/user.test.js > createUser > sets the default role`) tells you which file, describe block, and test failed. This is the full path in the test tree.
**The assertion message** (`expected { ... } to deeply equal { ... }`) tells you what kind of check failed and shows the two values being compared.
**The diff** shows exactly what's different. Lines starting with + are what you actually got, and lines starting with - are what you expected. In this case, the role was "viewer" but the test expected "member".
**The code snippet** shows the exact line and a few surrounding lines, with a caret (`^`) pointing to the failing assertion. You can click the file path in most terminals and IDEs to jump directly there.
At this point, the question is: did the code change (maybe the default role was intentionally updated to `"viewer"`), or is the test wrong? Check the source code for `createUser` to find out. If the default was intentionally changed, update the test. If not, you've found a bug.
## Isolating the Problem
When a test fails and the cause isn't immediately clear, the first step is to isolate it. Run just that one test, without the rest of your suite:
```bash
# Run only the failing test file
vitest src/user.test.js
# Run only tests matching a name pattern
vitest -t "sets the default role"
# Combine both for maximum precision
vitest src/user.test.js -t "sets the default role"
```
You can also add [`.only`](/api/test#only) to the test itself:
```js
test.only('sets the default role', () => {
// only this test runs in the file
})
```
If you have many failures and want to focus on the first one, use [`--bail`](/config/bail) to stop after a set number of failures:
```bash
vitest --bail 1
```
If the test passes when run alone but fails when run with others, you have a test isolation problem (more on that below). If it fails even when run alone, the issue is in the test itself or the code it's testing.
## Common Causes of Failures
### Shared State Between Tests
This is one of the most common and frustrating issues. A test passes when you run it alone, but fails when the full suite runs. The usual cause is that some other test modifies shared state (a global variable, a module-level cache, a database) and doesn't clean up after itself.
```js
// This is a problem: `users` is shared between tests
const users = []
test('adds a user', () => {
users.push('Alice')
expect(users).toEqual(['Alice'])
})
test('starts empty', () => {
// This fails because 'Alice' is still in the array!
expect(users).toEqual([])
})
```
The fix is to reset the state before each test with [`beforeEach`](/api/hooks#beforeeach), or better yet, use [`test.extend`](/guide/test-context#extend-test-context) to create fresh state for each test automatically:
```js
const test = baseTest.extend('users', () => [])
test('adds a user', ({ users }) => {
users.push('Alice')
expect(users).toEqual(['Alice'])
})
test('starts empty', ({ users }) => {
// Passes: each test gets its own array
expect(users).toEqual([])
})
```
### Async Issues
Tests that involve promises can fail intermittently or in confusing ways if the async flow isn't handled correctly. The most common mistake is forgetting an `await`:
```js
// This test always passes, even if fetchUser rejects!
test('fetches user', () => {
// Missing await: the test finishes before the promise settles
expect(fetchUser(1)).resolves.toMatchObject({ name: 'Alice' })
})
```
Vitest will usually warn you about unawaited assertions at the end of the test. If you see that warning, add the missing `await`:
```js
test('fetches user', async () => {
await expect(fetchUser(1)).resolves.toMatchObject({ name: 'Alice' })
})
```
If a test hangs and eventually times out, it usually means a promise never resolves. Check for missing callbacks, unresolved conditions, or deadlocks in the code you're testing.
### Stale Snapshots
If you're using [snapshot tests](/guide/learn/snapshots) and you intentionally changed the output of your code, the existing snapshots will be outdated. The test fails and shows a diff between the old snapshot and the new output.
This is expected. Review the diff to confirm the changes are correct, then update the snapshots by pressing `u` in watch mode or running `vitest -u`.
### Wrong Test Environment
If your code accesses browser APIs like `document` or `window` and you see errors like "document is not defined", your test is running in the Node environment (the default). You can switch to a browser-like environment with the [`environment`](/config/environment) config option, or better yet, use [Browser Mode](/guide/browser/) which runs tests in a real browser.
### Mocks Not Cleaned Up
If a mock from one test leaks into another, you'll get unexpected behavior. For example, a `vi.spyOn` that overrides a method's return value will persist into the next test unless it's restored.
The easiest fix is to enable automatic mock restoration in your config:
```js [vitest.config.js]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
restoreMocks: true,
},
})
```
This calls [`mockRestore()`](/api/mock#mockrestore) on every mock after each test. See the [Mock Functions](/guide/learn/mock-functions#resetting-mocks) tutorial for more details.
## Debugging Tools
### Console Logging
There's nothing wrong with adding `console.log` to your tests. It's the fastest way to inspect values and understand what's happening:
```js
test('transforms data correctly', () => {
const input = getData()
console.log('input:', input)
const result = transform(input)
console.log('result:', result)
expect(result).toMatchObject({ status: 'ok' })
})
```
Vitest displays console output inline with the test results, so you can see which test produced which log.
### Vitest UI
For a visual overview of your test suite, run Vitest with the `--ui` flag:
```bash
vitest --ui
```
This opens a browser-based dashboard where you can see all your tests, their status, and their output. It also includes a module graph that shows how your files are connected, which can help you understand why a change in one file causes failures in another. See the [Vitest UI](/guide/ui) guide for more details.
### VS Code Extension
The [Vitest VS Code extension](https://vitest.dev/vscode) lets you run and debug individual tests directly from your editor. You can click a "play" button next to any test, set breakpoints, and step through code in the VS Code debugger. This is often faster than switching between the terminal and your editor.
### Verbose Output
If the default output isn't showing enough detail, use the verbose reporter:
```bash
vitest --reporter=verbose
```
This shows every test individually (not just the files), which can help spot patterns in which tests pass and which fail.
### Attaching a Debugger
For more complex issues where you need to step through code line by line, you can run Vitest with the `--inspect-brk` flag and attach a debugger. The `--no-file-parallelism` flag ensures tests run in the main thread so breakpoints work reliably:
```bash
vitest --inspect-brk --no-file-parallelism
```
Then attach from VS Code, IntelliJ, or Chrome DevTools (`chrome://inspect`). See the [Debugging](/guide/debugging) guide for detailed setup instructions for each editor.
## Getting Help
If you're stuck, these resources can help:
* The [Common Errors](/guide/common-errors) page covers specific error messages and their solutions
* [GitHub Issues](https://github.com/vitest-dev/vitest/issues) for searching known bugs and workarounds
* The [Discord community](https://chat.vitest.dev) for real-time help from other Vitest users and maintainers
---
---
url: /guide/learn/writing-tests-with-ai.md
---
# Writing Tests with AI
AI coding assistants can help you write tests faster, but the quality of the output depends heavily on what you put in. A vague prompt produces vague tests. A specific prompt with the right context produces tests that are actually worth keeping.
This page covers how to get good test code from AI tools, and what to watch for when reviewing the results.
## Providing Context
The single most important thing you can do is give the AI enough context to understand what it's testing.
Start with the source file itself. The AI needs to see the actual implementation, not just a description of what the function does. Include the full file, or at least the function you want tested along with its imports and types.
Share existing test files from the same project. This helps the AI match your conventions: whether you use `test` or `it`, how you structure `describe` blocks, whether you prefer `test.extend` fixtures or `beforeEach`, and how you name your tests. AI tools are good at pattern matching, but they need patterns to match against.
Include your Vitest config, especially if you've enabled [`globals`](/config/globals), set a custom [`environment`](/config/environment), or configured [`setupFiles`](/config/setupfiles). Without this context, the AI might generate unnecessary imports, use the wrong test environment, or miss setup that your tests depend on.
If the code under test has dependencies that need mocking, share those files too (or at least their type signatures). The AI can't write a useful mock for a database client it's never seen.
::: tip
If your project has an `AGENTS.md` or similar file with coding conventions, include that as well. Many AI tools pick up on these automatically and will follow the rules defined there.
:::
## Writing Good Prompts
Specific prompts produce better tests than generic ones. Compare these two:
**Vague:** "Write tests for `userService.js`"
This will produce tests, but they'll likely be shallow: one happy-path test per function, minimal edge case coverage, and generic test names.
**Better:** "Write tests for the `createUser` function in `userService.js`. Cover validation errors (missing name, invalid email format, duplicate email), the successful creation path, and verify that the password is hashed before being stored."
This tells the AI exactly which function to focus on, which scenarios matter, and what behavior to verify. The output will be more thorough and more relevant.
### Tips for Better Prompts
* Ask for edge cases explicitly. "Include tests for empty inputs, boundary values, and error handling" produces more comprehensive coverage than leaving it to the AI's judgment. Without this nudge, most tools will generate a handful of happy-path tests and stop there.
* Mention specific Vitest features if you want them used. "Use `toMatchInlineSnapshot` for the error messages" or "use `test.for` for the different currency formats" guides the AI toward the right tools instead of letting it fall back to repetitive copy-paste tests.
* If you're testing async code, say so. "The function returns a Promise" or "this calls an external API" helps the AI use `async`/`await` and appropriate matchers like `.resolves` and `.rejects`.
* Tell the AI what *not* to do. "Test against the real implementation, don't mock any modules" or "don't use snapshot tests" prevents common defaults you don't want. AI tools tend to over-mock, and an explicit constraint prevents that.
* Describe the test structure you want. "Group tests by method using `describe` blocks" or "use `test.extend` fixtures for the database connection instead of `beforeEach`" saves you from restructuring the output afterwards.
* Reference existing tests when asking for additions. "Follow the same style as the tests in `auth.test.js`" is more effective than describing the style from scratch. The AI will pick up on naming conventions, assertion patterns, and import styles from the example.
* If the first result isn't right, iterate. "These tests are too focused on implementation details. Rewrite them to only assert on the return values and thrown errors" is a valid follow-up. Refining through conversation often produces better results than trying to write the perfect prompt upfront.
## Reviewing AI-Generated Tests
AI-generated tests can look convincing at first glance but still have problems. Here's what to check before committing them.
### Do the tests actually assert something meaningful?
Watch for tests that call a function but only check that it doesn't throw, or tests that assert on the mock itself rather than the behavior. A test like this gives false confidence:
```js
test('creates a user', () => {
const user = createUser('Alice', 'alice@example.com')
expect(user).toBeDefined() // this passes for almost anything
})
```
A better assertion checks the actual properties:
```js
test('creates a user with the correct fields', () => {
const user = createUser('Alice', 'alice@example.com')
expect(user).toMatchObject({
name: 'Alice',
email: 'alice@example.com',
})
expect(user.id).toBeTypeOf('string')
})
```
### Are they testing behavior or implementation?
AI tends to over-mock. If you see a test that mocks every dependency and then asserts that specific internal methods were called in a specific order, that's testing implementation details. These tests break every time you refactor, even if the behavior stays the same.
Ask yourself: if someone changed the internals but the function still returned the correct result, would this test break? If yes, it's probably too coupled to the implementation. See [Testing in Practice](/guide/learn/testing-in-practice#what-to-test) for more on this distinction.
### Do the tests actually run?
Always run the tests before committing. AI-generated tests can have import errors, reference functions that don't exist, or use APIs incorrectly. A test that looks correct in a chat window might fail immediately when you actually execute it:
```bash
vitest run src/userService.test.js
```
### Are there real edge cases?
AI tools tend to generate happy-path tests and skip the hard cases. After reviewing the generated tests, ask yourself: what happens with empty input? What about `null` or `undefined`? What if the network request fails? What if the list is empty?
If these scenarios aren't covered, ask the AI to add them, or write them yourself.
## Iterating on the Output
Treat AI-generated tests as a first draft, not a finished product. A good workflow looks like:
1. **Generate** the initial tests with a specific prompt and good context
2. **Run** them immediately to catch errors
3. **Review** each test for the issues described above
4. **Ask for revisions** if entire sections need improvement ("these tests mock too much, rewrite them to test the actual integration with the database module")
5. **Edit manually** for small fixes rather than re-prompting for every detail
Over time, as the AI sees more of your codebase and test patterns, its output will improve. The earlier tests in your project set the pattern for everything that follows, so it's worth getting those right.
## Common Pitfalls
### Wrong APIs
The most frequent issue with AI-generated Vitest tests is using the wrong API surface. AI models are trained on a lot of Jest code, so they sometimes generate `jest.fn()` instead of `vi.fn()`, or `jest.mock` instead of `vi.mock`. These will fail immediately.
A related problem is imports: if your config has `globals: true`, the AI might still add `import { test, expect } from 'vitest'` (harmless but unnecessary), or the reverse, generating tests without imports when globals aren't enabled. If you keep seeing Jest APIs, point the AI to the [Vitest API reference](/api/vi) or include it in the context.
### Mock Cleanup
AI-generated tests often set up spies with `vi.spyOn` or replace modules with `vi.mock` but never restore them. If your config doesn't have [`restoreMocks: true`](/config/restoremocks), these mocks leak between tests and cause confusing failures. The easiest fix is enabling that config option globally.
On a related note, AI tools tend to mock modules using string paths (`vi.mock('./module.js')`) when the `import()` form (`vi.mock(import('./module.js'))`) is preferable for type safety and automatic refactoring. See [Mock Functions](/guide/learn/mock-functions#mocking-modules) for why this matters.
### Verbose Test Names
AI tends to produce names like "should correctly return the formatted price string when given a valid positive number and a supported currency code." These are hard to scan when you have dozens of tests. Shorter names that describe the behavior work better: "formats USD prices", "throws for negative amounts", "returns empty array when no items match."
### Watch Mode
Vitest runs in watch mode by default, waiting for file changes and re-running tests interactively. Vitest tries to detect CI and non-interactive or agent environments and disable watch mode automatically, but this detection can be fragile.
When telling an AI agent to run tests, always use `vitest run` or `vitest --no-watch` to ensure the process exits after the tests finish.
---
---
url: /guide/browser/why.md
---
# Why Browser Mode
## Motivation
We developed the Vitest browser mode feature to help improve testing workflows and achieve more accurate and reliable test results. This addition to our testing API allows developers to run tests in a native browser environment. In this section, we'll explore the motivations behind this feature and its benefits for testing.
### Different Ways of Testing
There are different ways to test JavaScript code. Some testing frameworks simulate browser environments in Node.js, while others run tests in real browsers. In this context, [jsdom](https://npmx.dev/package/jsdom) is an example of a spec implementation that simulates a browser environment by being used with a test runner like Jest or Vitest, while other testing tools such as [WebdriverIO](https://webdriver.io/) or [Cypress](https://www.cypress.io/) allow developers to test their applications in a real browser or in case of [Playwright](https://playwright.dev/) provide you a browser engine.
### The Simulation Caveat
Testing JavaScript programs in simulated environments such as jsdom or happy-dom has simplified the test setup and provided an easy-to-use API, making them suitable for many projects and increasing confidence in test results. However, it is crucial to keep in mind that these tools only simulate a browser environment and not an actual browser, which may result in some discrepancies between the simulated environment and the real environment. Therefore, false positives or negatives in test results may occur.
To achieve the highest level of confidence in our tests, it's crucial to test in a real browser environment. This is why we developed the browser mode feature in Vitest, allowing developers to run tests natively in a browser and gain more accurate and reliable test results. With browser-level testing, developers can be more confident that their application will work as intended in a real-world scenario.
## Drawbacks
When using Vitest browser, it is important to consider the following drawbacks:
### Not a Drop-In Replacement
The browser mode feature of Vitest does not completely replace standalone end-to-end test runners. It is recommended that users augment their Vitest browser experience with a standalone browser-side test runner like WebdriverIO, Cypress or Playwright.
### Longer Initialization
Vitest browser requires spinning up the provider and the browser during the initialization process, which can take some time. This can result in longer initialization times compared to other testing patterns.
---
---
url: /guide/browser.md
---
# Browser Mode {#browser-mode}
This page provides information about the browser mode feature in the Vitest API, which allows you to run your tests in the browser natively, providing access to browser globals like window and document.
::: tip
If you are looking for documentation for `expect`, `vi` or any general API like test projects or type testing, refer to the ["Getting Started" guide](/guide/).
:::
## Installation
For easier setup, you can use `vitest init browser` command to install required dependencies and create browser configuration.
::: code-group
```bash [npm]
npx vitest init browser
```
```bash [yarn]
yarn exec vitest init browser
```
```bash [pnpm]
pnpx vitest init browser
```
```bash [bun]
bunx vitest init browser
```
:::
### Manual Installation
You can also install packages manually. Vitest always requires a provider to be defined. You can chose either [`preview`](/config/browser/preview), [`playwright`](/config/browser/playwright) or [`webdriverio`](/config/browser/webdriverio).
If you want to just preview how your tests look, you can use the `preview` provider:
::: code-group
```bash [npm]
npm install -D vitest @vitest/browser-preview
```
```bash [yarn]
yarn add -D vitest @vitest/browser-preview
```
```bash [pnpm]
pnpm add -D vitest @vitest/browser-preview
```
```bash [bun]
bun add -D vitest @vitest/browser-preview
```
:::
::: warning
However, to run tests in CI you need to install either [`playwright`](https://npmx.dev/package/playwright) or [`webdriverio`](https://npmx.dev/package/webdriverio). We also recommend switching to either one of them for testing locally instead of using the default `preview` provider since it relies on simulating events instead of using Chrome DevTools Protocol.
If you don't already use one of these tools, we recommend starting with Playwright because it supports parallel execution, which makes your tests run faster.
::: tabs key:provider
\== Playwright
[Playwright](https://npmx.dev/package/playwright) is a framework for Web Testing and Automation.
::: code-group
```bash [npm]
npm install -D vitest @vitest/browser-playwright
```
```bash [yarn]
yarn add -D vitest @vitest/browser-playwright
```
```bash [pnpm]
pnpm add -D vitest @vitest/browser-playwright
```
```bash [bun]
bun add -D vitest @vitest/browser-playwright
```
\== WebdriverIO
[WebdriverIO](https://npmx.dev/package/webdriverio) allows you to run tests locally using the WebDriver protocol.
::: code-group
```bash [npm]
npm install -D vitest @vitest/browser-webdriverio
```
```bash [yarn]
yarn add -D vitest @vitest/browser-webdriverio
```
```bash [pnpm]
pnpm add -D vitest @vitest/browser-webdriverio
```
```bash [bun]
bun add -D vitest @vitest/browser-webdriverio
```
:::
## Configuration
To activate browser mode in your Vitest configuration, set the `browser.enabled` field to `true` in your Vitest configuration file. Here is an example configuration using the browser field:
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
provider: playwright(),
enabled: true,
// at least one instance is required
instances: [
{ browser: 'chromium' },
],
},
}
})
```
::: info
Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. You can change that with the [`api`](/config/api) option.
:::
If you have not used Vite before, make sure you have your framework's plugin installed and specified in the config. Some frameworks might require extra configuration to work - check their Vite related documentation to be sure.
::: code-group
```ts [react]
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
}
}
})
```
```ts [vue]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
}
}
})
```
```ts [svelte]
import { defineConfig } from 'vitest/config'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [svelte()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
}
}
})
```
```ts [solid]
import { defineConfig } from 'vitest/config'
import solidPlugin from 'vite-plugin-solid'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [solidPlugin()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
}
}
})
```
```ts [marko]
import { defineConfig } from 'vitest/config'
import marko from '@marko/vite'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
plugins: [marko()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
}
}
})
```
```ts [qwik]
import { defineConfig } from 'vitest/config'
import { qwikVite } from '@builder.io/qwik/optimizer'
import { playwright } from '@vitest/browser-playwright'
// optional, run the tests in SSR mode
import { testSSR } from 'vitest-browser-qwik/ssr-plugin'
export default defineConfig({
plugins: [testSSR(), qwikVite()],
test: {
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }]
},
},
})
```
:::
If you need to run some tests using Node-based runner, you can define a [`projects`](/guide/projects) option with separate configurations for different testing strategies:
{#projects-config}
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
projects: [
{
test: {
// an example of file based convention,
// you don't have to follow it
include: [
'tests/unit/**/*.{test,spec}.ts',
'tests/**/*.unit.{test,spec}.ts',
],
name: 'unit',
environment: 'node',
},
},
{
test: {
// an example of file based convention,
// you don't have to follow it
include: [
'tests/browser/**/*.{test,spec}.ts',
'tests/**/*.browser.{test,spec}.ts',
],
name: 'browser',
browser: {
enabled: true,
provider: playwright(),
instances: [
{ browser: 'chromium' },
],
},
},
},
],
},
})
```
## Browser Option Types
The browser option in Vitest depends on the provider. Vitest will fail, if you pass `--browser` and don't specify its name in the config file. Available options:
* `webdriverio` supports these browsers:
* `firefox`
* `chrome`
* `edge`
* `safari`
* `playwright` supports these browsers:
* `firefox`
* `webkit`
* `chromium`
## Browser Compatibility
Vitest uses [Vite dev server](https://vitejs.dev/guide/#browser-support) to run your tests, so we only support features specified in the [`esbuild.target`](https://vitejs.dev/config/shared-options.html#esbuild) option (`esnext` by default).
By default, Vite targets browsers which support the native [ES Modules](https://caniuse.com/es6-module), native [ESM dynamic import](https://caniuse.com/es6-module-dynamic-import), and [`import.meta`](https://caniuse.com/mdn-javascript_operators_import_meta). On top of that, we utilize [`BroadcastChannel`](https://caniuse.com/?search=BroadcastChannel) to communicate between iframes:
* Chrome >=87
* Firefox >=78
* Safari >=15.4
* Edge >=88
## Running Tests
When you specify a browser name in the browser option, Vitest will try to run the specified browser using `preview` by default, and then run the tests there. If you don't want to use `preview`, you can configure the custom browser provider by using `browser.provider` option.
To specify a browser using the CLI, use the `--browser` flag followed by the browser name, like this:
```sh
npx vitest --browser=chromium
```
Or you can provide browser options to CLI with dot notation:
```sh
npx vitest --browser.headless
```
::: warning
Since Vitest 3.2, if you don't have the `browser` option in your config but specify the `--browser` flag, Vitest will fail because it can't assume that config is meant for the browser and not Node.js tests.
:::
By default, Vitest will automatically open the browser UI for development. Your tests will run inside an iframe in the center. You can configure the viewport by selecting the preferred dimensions, calling `page.viewport` inside the test, or setting default values in [the config](/config/browser/viewport).
For an alternative debugging model that captures DOM snapshots for every test instead of showing a live iframe, see [Trace View](/guide/browser/trace-view).
## Headless
Headless mode is another option available in the browser mode. In headless mode, the browser runs in the background without a user interface, which makes it useful for running automated tests. The headless option in Vitest can be set to a boolean value to enable or disable headless mode.
When using headless mode, Vitest won't open the UI automatically. If you want to continue using the UI but have tests run headlessly, you can install the [`@vitest/ui`](/guide/ui) package and pass the `--ui` flag when running Vitest.
Here's an example configuration enabling headless mode:
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
provider: playwright(),
enabled: true,
headless: true,
},
}
})
```
You can also set headless mode using the `--browser.headless` flag in the CLI, like this:
```sh
npx vitest --browser.headless
```
In this case, Vitest will run in headless mode using the Chrome browser.
::: warning
Headless mode is not available by default. You need to use either [`playwright`](https://npmx.dev/package/playwright) or [`webdriverio`](https://npmx.dev/package/webdriverio) providers to enable this feature.
:::
## Examples
By default, you don't need any external packages to work with the Browser Mode:
```js [example.test.js]
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
import { render } from './my-render-function.js'
test('properly handles form inputs', async () => {
render() // mount DOM elements
// Asserts initial state.
await expect.element(page.getByText('Hi, my name is Alice')).toBeInTheDocument()
// Get the input DOM node by querying the associated label.
const usernameInput = page.getByLabelText(/username/i)
// Type the name into the input. This already validates that the input
// is filled correctly, no need to check the value manually.
await usernameInput.fill('Bob')
await expect.element(page.getByText('Hi, my name is Bob')).toBeInTheDocument()
})
```
However, Vitest also provides packages to render components for several popular frameworks out of the box:
* [`vitest-browser-vue`](https://github.com/vitest-dev/vitest-browser-vue) to render [vue](https://vuejs.org) components
* [`vitest-browser-svelte`](https://github.com/vitest-dev/vitest-browser-svelte) to render [svelte](https://svelte.dev) components
* [`vitest-browser-react`](https://github.com/vitest-dev/vitest-browser-react) to render [react](https://react.dev) components
* [`vitest-browser-angular`](https://github.com/vitest-community/vitest-browser-angular) to render [Angular](https://angular.dev) components
Community packages are available for other frameworks:
* [`vitest-browser-lit`](https://github.com/EskiMojo14/vitest-browser-lit) to render [lit](https://lit.dev) components
* [`vitest-browser-preact`](https://github.com/JoviDeCroock/vitest-browser-preact) to render [preact](https://preactjs.com) components
* [`vitest-browser-qwik`](https://github.com/QwikDev/vitest-browser-qwik) to render [qwik](https://qwik.dev) components
If your framework is not represented, feel free to create your own package - it is a simple wrapper around the framework renderer and `page.elementLocator` API. We will add a link to it on this page. Make sure it has a name starting with `vitest-browser-`.
Besides rendering components and locating elements, you will also need to make assertions. Vitest forks the [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. Read more at the [Assertions API](/api/browser/assertions).
```ts
import { expect } from 'vitest'
import { page } from 'vitest/browser'
// element is rendered correctly
await expect.element(page.getByText('Hello World')).toBeInTheDocument()
```
Vitest exposes a [Context API](/api/browser/context) with a small set of utilities that might be useful to you in tests. For example, if you need to make an interaction, like clicking an element or typing text into an input, you can use `userEvent` from `vitest/browser`. Read more at the [Interactivity API](/api/browser/interactivity).
```ts
import { page, userEvent } from 'vitest/browser'
await userEvent.fill(page.getByLabelText(/username/i), 'Alice')
// or just locator.fill
await page.getByLabelText(/username/i).fill('Alice')
```
::: code-group
```ts [vue]
import { render } from 'vitest-browser-vue'
import Component from './Component.vue'
test('properly handles v-model', async () => {
const screen = await render(Component)
// Asserts initial state.
await expect.element(screen.getByText('Hi, my name is Alice')).toBeInTheDocument()
// Get the input DOM node by querying the associated label.
const usernameInput = screen.getByLabelText(/username/i)
// Type the name into the input. This already validates that the input
// is filled correctly, no need to check the value manually.
await usernameInput.fill('Bob')
await expect.element(screen.getByText('Hi, my name is Bob')).toBeInTheDocument()
})
```
```ts [svelte]
import { render } from 'vitest-browser-svelte'
import { expect, test } from 'vitest'
import Greeter from './greeter.svelte'
test('greeting appears on click', async () => {
const screen = await render(Greeter, { name: 'World' })
const button = screen.getByRole('button')
await button.click()
const greeting = screen.getByText(/hello world/iu)
await expect.element(greeting).toBeInTheDocument()
})
```
```tsx [react]
import { render } from 'vitest-browser-react'
import Fetch from './fetch'
test('loads and displays greeting', async () => {
// Render a React element into the DOM
const screen = render()
await screen.getByText('Load Greeting').click()
// wait before throwing an error if it cannot find an element
const heading = screen.getByRole('heading')
// assert that the alert message is correct
await expect.element(heading).toHaveTextContent('hello there')
await expect.element(screen.getByRole('button')).toBeDisabled()
})
```
```ts [lit]
import { render } from 'vitest-browser-lit'
import { html } from 'lit'
import './greeter-button'
test('greeting appears on click', async () => {
const screen = render(html``)
const button = screen.getByRole('button')
await button.click()
const greeting = screen.getByText(/hello world/iu)
await expect.element(greeting).toBeInTheDocument()
})
```
```tsx [preact]
import { render } from 'vitest-browser-preact'
import { createElement } from 'preact'
import Greeting from '.Greeting'
test('greeting appears on click', async () => {
const screen = render()
const button = screen.getByRole('button')
await button.click()
const greeting = screen.getByText(/hello world/iu)
await expect.element(greeting).toBeInTheDocument()
})
```
```tsx [qwik]
import { render } from 'vitest-browser-qwik'
import Greeting from './greeting'
test('greeting appears on click', async () => {
// renderSSR and renderHook are also available
const screen = render()
const button = screen.getByRole('button')
await button.click()
const greeting = screen.getByText(/hello world/iu)
await expect.element(greeting).toBeInTheDocument()
})
```
:::
Vitest doesn't support all frameworks out of the box, but you can use external tools to run tests with these frameworks. We also encourage the community to create their own `vitest-browser` wrappers - if you have one, feel free to add it to the examples above.
For unsupported frameworks, we recommend using `testing-library` packages:
* [`@solidjs/testing-library`](https://testing-library.com/docs/solid-testing-library/intro) to render [solid](https://www.solidjs.com) components
* [`@marko/testing-library`](https://testing-library.com/docs/marko-testing-library/intro) to render [marko](https://markojs.com) components
You can also see more examples in [`browser-examples`](https://github.com/vitest-tests/browser-examples) repository.
::: warning
`testing-library` provides a package `@testing-library/user-event`. We do not recommend using it directly because it simulates events instead of actually triggering them - instead, use [`userEvent`](/api/browser/interactivity) imported from `vitest/browser` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood.
:::
::: code-group
```tsx [solid]
// based on @testing-library/solid API
// https://testing-library.com/docs/solid-testing-library/api
import { render } from '@testing-library/solid'
it('uses params', async () => {
const App = () => (
<>
(
`)
})
```
:::
## Limitations
### Thread Blocking Dialogs
When using Vitest Browser, it's important to note that thread blocking dialogs like `alert`, `confirm` or `print` cannot be used natively. This is because they block the web page, which means Vitest cannot continue communicating with the page, causing the execution to hang.
In such situations, Vitest provides default mocks with default returned values for these APIs. This ensures that if the user accidentally uses synchronous popup web APIs, the execution would not hang. However, it's still recommended for the user to mock these web APIs for a better experience. Read more in [Mocking](/guide/mocking).
### Spying on Module Exports
Browser Mode uses the browser's native ESM support to serve modules. The module namespace object is sealed and can't be reconfigured, unlike in Node.js tests where Vitest can patch the Module Runner. This means you can't call `vi.spyOn` on an imported object:
```ts
import { vi } from 'vitest'
import * as module from './module.js'
vi.spyOn(module, 'method') // ❌ throws an error
```
To bypass this limitation, Vitest supports `{ spy: true }` option in `vi.mock('./module.js')`. This will automatically spy on every export in the module without replacing them with fake ones.
```ts
import { vi } from 'vitest'
import * as module from './module.js'
vi.mock('./module.js', { spy: true })
vi.mocked(module.method).mockImplementation(() => {
// ...
})
```
However, the only way to mock exported *variables* is to export a method that will change the internal value:
::: code-group
```js [module.js]
export let MODE = 'test'
export function changeMode(newMode) {
MODE = newMode
}
```
```js [module.test.ts]
import { expect } from 'vitest'
import { changeMode, MODE } from './module.js'
changeMode('production')
expect(MODE).toBe('production')
```
:::
---
---
url: /guide/browser/multiple-setups.md
---
# Multiple Setups
You can specify several different browser setups using the [`browser.instances`](/config/browser/instances) option.
The main advantage of using the `browser.instances` over the [test projects](/guide/projects) is improved caching. Every project will use the same Vite server meaning the file transform and [dependency pre-bundling](https://vite.dev/guide/dep-pre-bundling.html) has to happen only once.
## Several Browsers
You can use the `browser.instances` field to specify options for different browsers. For example, if you want to run the same tests in different browsers, the minimal configuration will look like this:
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [
{ browser: 'chromium' },
{ browser: 'firefox' },
{ browser: 'webkit' },
],
},
},
})
```
## Different Setups
You can also specify different config options independently from the browser (although, the instances *can* also have `browser` fields):
::: code-group
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [
{
browser: 'chromium',
name: 'chromium-1',
setupFiles: ['./ratio-setup.ts'],
provide: {
ratio: 1,
},
},
{
browser: 'chromium',
name: 'chromium-2',
provide: {
ratio: 2,
},
},
],
},
},
})
```
```ts [example.test.ts]
import { expect, inject, test } from 'vitest'
import { globalSetupModifier } from './example.js'
test('ratio works', () => {
expect(inject('ratio') * globalSetupModifier).toBe(14)
})
```
:::
In this example Vitest will run all tests in `chromium` browser, but execute a `'./ratio-setup.ts'` file only in the first configuration and inject a different `ratio` value depending on the [`provide` field](/config/provide).
::: warning
Note that you need to define the custom `name` value if you are using the same browser name because Vitest will assign the `browser` as the project name otherwise.
:::
## Filtering
You can filter what projects to run with the [`--project` flag](/guide/cli#project). Vitest will automatically assign the browser name as a project name if it is not assigned manually. If the root config already has a name, Vitest will merge them: `custom` -> `custom (browser)`.
```shell
$ vitest --project=chromium
```
::: code-group
```ts{6,8} [default]
export default defineConfig({
test: {
browser: {
instances: [
// name: chromium
{ browser: 'chromium' },
// name: custom
{ browser: 'firefox', name: 'custom' },
]
}
}
})
```
```ts{3,7,9} [custom]
export default defineConfig({
test: {
name: 'custom',
browser: {
instances: [
// name: custom (chromium)
{ browser: 'chromium' },
// name: manual
{ browser: 'firefox', name: 'manual' },
]
}
}
})
```
:::
---
---
url: /guide/browser/component-testing.md
---
# Component Testing
Component testing is a testing strategy that focuses on testing individual UI components in isolation. Unlike end-to-end tests that test entire user flows, component tests verify that each component works correctly on its own, making them faster to run and easier to debug.
Vitest provides comprehensive support for component testing across multiple frameworks including Vue, React, Svelte, Lit, Preact, Qwik, Solid, Marko, and more. This guide covers the specific patterns, tools, and best practices for testing components effectively with Vitest.
## Why Component Testing?
Component testing sits between unit tests and end-to-end tests, offering several advantages:
* **Faster feedback** - Test individual components without loading entire applications
* **Isolated testing** - Focus on component behavior without external dependencies
* **Better debugging** - Easier to pinpoint issues in specific components
* **Comprehensive coverage** - Test edge cases and error states more easily
## Browser Mode for Component Testing
Component testing in Vitest uses **Browser Mode** to run tests in real browser environments using Playwright, WebdriverIO, or preview mode. This provides the most accurate testing environment as your components run in real browsers with actual DOM implementations, CSS rendering, and browser APIs.
### Why Browser Mode?
Browser Mode is the recommended approach for component testing because it provides the most accurate testing environment. Unlike DOM simulation libraries, Browser Mode catches real-world issues that can affect your users.
::: tip
Browser Mode catches issues that DOM simulation libraries might miss, including:
* CSS layout and styling problems
* Real browser API behavior
* Accurate event handling and propagation
* Proper focus management and accessibility features
:::
### Purpose of This Guide
This guide focuses specifically on **component testing patterns and best practices** using Vitest's capabilities. While many examples use Browser Mode (as it's the recommended approach), the focus here is on component-specific testing strategies rather than browser configuration details.
For detailed browser setup, configuration options, and advanced browser features, refer to the [Browser Mode documentation](/guide/browser/).
## What Makes a Good Component Test
Good component tests focus on **behavior and user experience** rather than implementation details:
* **Test the contract** - How components receive inputs (props) and produce outputs (events, renders)
* **Test user interactions** - Clicks, form submissions, keyboard navigation
* **Test edge cases** - Error states, loading states, empty states
* **Avoid testing internals** - State variables, private methods, CSS classes
### Component Testing Hierarchy
```
1. Critical User Paths → Always test these
2. Error Handling → Test failure scenarios
3. Edge Cases → Empty data, extreme values
4. Accessibility → Screen readers, keyboard nav
5. Performance → Large datasets, animations
```
## Component Testing Strategies
### Isolation Strategy
Test components in isolation by mocking dependencies:
```tsx
// For API requests, we recommend MSW (Mock Service Worker)
// See: https://vitest.dev/guide/mocking/requests
//
// vi.mock(import('../api/userService'), () => ({
// fetchUser: vi.fn().mockResolvedValue({ name: 'John' })
// }))
// Mock child components to focus on parent logic
vi.mock(import('../components/UserCard'), () => ({
default: vi.fn(({ user }) => `
User: ${user.name}
`)
}))
test('UserProfile handles loading and data states', async () => {
const { getByText } = render()
// Test loading state
await expect.element(getByText('Loading...')).toBeInTheDocument()
// Test for data to load (expect.element auto-retries)
await expect.element(getByText('User: John')).toBeInTheDocument()
})
```
### Integration Strategy
Test component collaboration and data flow:
```tsx
test('ProductList filters and displays products correctly', async () => {
const mockProducts = [
{ id: 1, name: 'Laptop', category: 'Electronics', price: 999 },
{ id: 2, name: 'Book', category: 'Education', price: 29 }
]
const { getByLabelText, getByText } = render(
)
// Initially shows all products
await expect.element(getByText('Laptop')).toBeInTheDocument()
await expect.element(getByText('Book')).toBeInTheDocument()
// Filter by category
await userEvent.selectOptions(
getByLabelText(/category/i),
'Electronics'
)
// Only electronics should remain
await expect.element(getByText('Laptop')).toBeInTheDocument()
await expect.element(queryByText('Book')).not.toBeInTheDocument()
})
```
## Testing Library Integration
While Vitest provides official packages for popular frameworks ([`vitest-browser-vue`](https://npmx.dev/package/vitest-browser-vue), [`vitest-browser-react`](https://npmx.dev/package/vitest-browser-react), [`vitest-browser-svelte`](https://npmx.dev/package/vitest-browser-svelte)), you can integrate with [Testing Library](https://testing-library.com/) for frameworks not yet officially supported.
### When to Use Testing Library
* Your framework doesn't have an official Vitest browser package yet
* You're migrating existing tests that use Testing Library
* You prefer Testing Library's API for specific testing scenarios
### Integration Pattern
The key is using `page.elementLocator()` to bridge Testing Library's DOM output with Vitest's browser mode APIs:
```jsx
// For Solid.js components
import { render } from '@testing-library/solid'
import { page } from 'vitest/browser'
test('Solid component handles user interaction', async () => {
// Use Testing Library to render the component
const { baseElement, getByRole } = render(() =>
)
// Bridge to Vitest's browser mode for interactions and assertions
const screen = page.elementLocator(baseElement)
// Use Vitest's page queries for finding elements
const incrementButton = screen.getByRole('button', { name: /increment/i })
// Use Vitest's assertions and interactions
await expect.element(screen.getByText('Count: 0')).toBeInTheDocument()
// Trigger user interaction using Vitest's page API
await incrementButton.click()
await expect.element(screen.getByText('Count: 1')).toBeInTheDocument()
})
```
### Available Testing Library Packages
Popular Testing Library packages that work well with Vitest:
* [`@testing-library/solid`](https://github.com/solidjs/solid-testing-library) - For Solid.js
* [`@marko/testing-library`](https://testing-library.com/docs/marko-testing-library/intro) - For Marko
* [`@testing-library/svelte`](https://testing-library.com/docs/svelte-testing-library/intro) - Alternative to [`vitest-browser-svelte`](https://npmx.dev/package/vitest-browser-svelte)
* [`@testing-library/vue`](https://testing-library.com/docs/vue-testing-library/intro) - Alternative to [`vitest-browser-vue`](https://npmx.dev/package/vitest-browser-vue)
::: tip Migration Path
If your framework gets official Vitest support later, you can gradually migrate by replacing Testing Library's `render` function while keeping most of your test logic intact.
:::
## Best Practices
### 1. Use Browser Mode for CI/CD
Ensure tests run in real browser environments for the most accurate testing. Browser Mode provides accurate CSS rendering, real browser APIs, and proper event handling.
### 2. Test User Interactions
Simulate real user behavior using Vitest's [Interactivity API](/api/browser/interactivity). Use `page.getByRole()` and `userEvent` methods as shown in our [Advanced Testing Patterns](#advanced-testing-patterns):
```tsx
// Good: Test actual user interactions
await page.getByRole('button', { name: /submit/i }).click()
await page.getByLabelText(/email/i).fill('user@example.com')
// Avoid: Testing implementation details
// component.setState({ email: 'user@example.com' })
```
### 3. Test Accessibility
Ensure components work for all users by testing keyboard navigation, focus management, and ARIA attributes. See our [Testing Accessibility](#testing-accessibility) example for practical patterns:
```tsx
// Test keyboard navigation
await userEvent.keyboard('{Tab}')
await expect.element(document.activeElement).toHaveFocus()
// Test ARIA attributes
await expect.element(modal).toHaveAttribute('aria-modal', 'true')
```
### 4. Mock External Dependencies
Focus tests on component logic by mocking APIs and external services. This makes tests faster and more reliable. See our [Isolation Strategy](#isolation-strategy) for examples:
```tsx
// For API requests, we recommend using MSW (Mock Service Worker)
// See: https://vitest.dev/guide/mocking/requests
// This provides more realistic request/response mocking
// For module mocking, use the import() syntax
vi.mock(import('../components/UserCard'), () => ({
default: vi.fn(() =>
Mocked UserCard
)
}))
```
### 5. Use Meaningful Test Descriptions
Write test descriptions that explain the expected behavior, not implementation details:
```tsx
// Good: Describes user-facing behavior
test('shows error message when email format is invalid')
test('disables submit button while form is submitting')
// Avoid: Implementation-focused descriptions
test('calls validateEmail function')
test('sets isSubmitting state to true')
```
## Advanced Testing Patterns
### Testing Component State Management
```tsx
// Testing stateful components and state transitions
test('ShoppingCart manages items correctly', async () => {
const { getByText, getByTestId } = render()
// Initially empty
await expect.element(getByText('Your cart is empty')).toBeInTheDocument()
// Add item
await page.getByRole('button', { name: /add laptop/i }).click()
// Verify state change
await expect.element(getByText('1 item')).toBeInTheDocument()
await expect.element(getByText('Laptop - $999')).toBeInTheDocument()
// Test quantity updates
await page.getByRole('button', { name: /increase quantity/i }).click()
await expect.element(getByText('2 items')).toBeInTheDocument()
})
```
### Testing Async Components with Data Fetching
```tsx
// Option 1: Recommended - Use MSW (Mock Service Worker) for API mocking
import { http, HttpResponse } from 'msw'
import { setupWorker } from 'msw/browser'
// Set up MSW worker with API handlers
const worker = setupWorker(
http.get('/api/users/:id', ({ params }) => {
// Describe the happy path
return HttpResponse.json({ id: params.id, name: 'John Doe', email: 'john@example.com' })
})
)
// Start the worker before all tests
beforeAll(() => worker.start())
afterEach(() => worker.resetHandlers())
afterAll(() => worker.stop())
test('UserProfile handles loading, success, and error states', async () => {
// Test success state
const { getByText } = render()
// expect.element auto-retries until elements are found
await expect.element(getByText('John Doe')).toBeInTheDocument()
await expect.element(getByText('john@example.com')).toBeInTheDocument()
// Test error state by overriding the handler for this test
worker.use(
http.get('/api/users/:id', () => {
return HttpResponse.json({ error: 'User not found' }, { status: 404 })
})
)
const { getByText: getErrorText } = render()
await expect.element(getErrorText('Error: User not found')).toBeInTheDocument()
})
```
::: tip
See more details on [using MSW in the browser](https://mswjs.io/docs/integrations/browser).
:::
### Testing Component Communication
```tsx
// Test parent-child component interaction
test('parent and child components communicate correctly', async () => {
const mockOnSelectionChange = vi.fn()
const { getByText } = render(
)
// Interact with child component
await page.getByRole('checkbox', { name: /electronics/i }).click()
// Verify parent receives the communication
expect(mockOnSelectionChange).toHaveBeenCalledWith({
category: 'electronics',
filters: ['electronics']
})
// Verify other child component updates (expect.element auto-retries)
await expect.element(getByText('Showing Electronics products')).toBeInTheDocument()
})
```
### Testing Complex Forms with Validation
```tsx
test('ContactForm handles complex validation scenarios', async () => {
const mockSubmit = vi.fn()
const { getByLabelText, getByText } = render(
)
const nameInput = page.getByLabelText(/full name/i)
const emailInput = page.getByLabelText(/email/i)
const messageInput = page.getByLabelText(/message/i)
const submitButton = page.getByRole('button', { name: /send message/i })
// Test validation triggers
await submitButton.click()
await expect.element(getByText('Name is required')).toBeInTheDocument()
await expect.element(getByText('Email is required')).toBeInTheDocument()
await expect.element(getByText('Message is required')).toBeInTheDocument()
// Test partial validation
await nameInput.fill('John Doe')
await submitButton.click()
await expect.element(getByText('Name is required')).not.toBeInTheDocument()
await expect.element(getByText('Email is required')).toBeInTheDocument()
// Test email format validation
await emailInput.fill('invalid-email')
await submitButton.click()
await expect.element(getByText('Please enter a valid email')).toBeInTheDocument()
// Test successful submission
await emailInput.fill('john@example.com')
await messageInput.fill('Hello, this is a test message.')
await submitButton.click()
expect(mockSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: 'john@example.com',
message: 'Hello, this is a test message.'
})
})
```
### Testing Error Boundaries
```tsx
// Test how components handle and recover from errors
function ThrowError({ shouldThrow }: { shouldThrow: boolean }) {
if (shouldThrow) {
throw new Error('Component error!')
}
return
Component working fine
}
test('ErrorBoundary catches and displays errors gracefully', async () => {
const { getByText, rerender } = render(
Something went wrong}>
)
// Initially working
await expect.element(getByText('Component working fine')).toBeInTheDocument()
// Trigger error
rerender(
Something went wrong}>
)
// Error boundary should catch it
await expect.element(getByText('Something went wrong')).toBeInTheDocument()
})
```
### Testing Accessibility
```tsx
test('Modal component is accessible', async () => {
const { getByRole, getByLabelText } = render(
)
// Test focus management - modal should receive focus when opened
// This is crucial for screen reader users to know a modal opened
const modal = getByRole('dialog')
await expect.element(modal).toHaveFocus()
// Test ARIA attributes - these provide semantic information to screen readers
await expect.element(modal).toHaveAttribute('aria-labelledby') // Links to title element
await expect.element(modal).toHaveAttribute('aria-modal', 'true') // Indicates modal behavior
// Test keyboard navigation - Escape key should close modal
// This is required by ARIA authoring practices
await userEvent.keyboard('{Escape}')
// expect.element auto-retries until modal is removed
await expect.element(modal).not.toBeInTheDocument()
// Test focus trap - tab navigation should cycle within modal
// This prevents users from tabbing to content behind the modal
const firstInput = getByLabelText(/username/i)
const lastButton = getByRole('button', { name: /save/i })
// Use click to focus on the first input, then test tab navigation
await firstInput.click()
await userEvent.keyboard('{Shift>}{Tab}{/Shift}') // Shift+Tab goes backwards
await expect.element(lastButton).toHaveFocus() // Should wrap to last element
})
```
## Debugging Component Tests
### 1. Use Browser Dev Tools
Browser Mode runs tests in real browsers, giving you access to full developer tools. When tests fail, you can:
* **Open browser dev tools** during test execution (F12 or right-click → Inspect)
* **Set breakpoints** in your test code or component code
* **Inspect the DOM** to see the actual rendered output
* **Check console errors** for JavaScript errors or warnings
* **Monitor network requests** to debug API calls
For headful mode debugging, add `headless: false` to your browser config temporarily.
### 2. Add Debug Statements
Use strategic logging to understand test failures:
```tsx
test('debug form validation', async () => {
render()
const submitButton = page.getByRole('button', { name: /submit/i })
await submitButton.click()
// Debug: Check if element exists with different query
const errorElement = page.getByText('Email is required')
console.log('Error element found:', errorElement.length)
await expect.element(errorElement).toBeInTheDocument()
})
```
### 3. Inspect Rendered Output
When components don't render as expected, investigate systematically:
**Use Vitest's browser UI:**
* Run tests with browser mode enabled
* Open the browser URL shown in the terminal to see tests running
* Visual inspection helps identify CSS issues, layout problems, or missing elements
**Test element queries:**
```tsx
// Debug why elements can't be found
const button = page.getByRole('button', { name: /submit/i })
console.log('Button count:', button.length) // Should be 1
// Try alternative queries if the first one fails
if (button.length === 0) {
console.log('All buttons:', page.getByRole('button').length)
console.log('By test ID:', page.getByTestId('submit-btn').length)
}
```
### 4. Verify Selectors
Selector issues are common causes of test failures. Debug them systematically:
**Check accessible names:**
```tsx
// If getByRole fails, check what roles/names are available
const buttons = page.getByRole('button').all()
for (const button of buttons) {
// Use element() to get the DOM element and access native properties
const element = button.element()
const accessibleName = element.getAttribute('aria-label') || element.textContent
console.log(`Button: "${accessibleName}"`)
}
```
**Test different query strategies:**
```tsx
// Multiple ways to find the same element using .or for auto-retrying
const submitButton = page.getByRole('button', { name: /submit/i }) // By accessible name
.or(page.getByTestId('submit-button')) // By test ID
.or(page.getByText('Submit')) // By exact text
// Note: Vitest doesn't have page.locator(), use specific getBy* methods instead
```
**Common selector debugging patterns:**
```tsx
test('debug element queries', async () => {
render()
// Check if element is visible and enabled
const emailInput = page.getByLabelText(/email/i)
await expect.element(emailInput).toBeVisible() // Will show if element is visible and print DOM if not
})
```
### 5. Debugging Async Issues
Component tests often involve timing issues:
```tsx
test('debug async component behavior', async () => {
render()
// expect.element will automatically retry and show helpful error messages
await expect.element(page.getByText('John Doe')).toBeInTheDocument()
})
```
## Migration from Other Testing Frameworks
### From Jest + Testing Library
Most Jest + Testing Library tests work with minimal changes:
```ts
// Before (Jest)
import { render, screen } from '@testing-library/react' // [!code --]
// After (Vitest)
import { render } from 'vitest-browser-react' // [!code ++]
```
### Key Differences
* Use `await expect.element()` instead of `expect()` for DOM assertions
* Use `vitest/browser` for user interactions instead of `@testing-library/user-event`
* Browser Mode provides real browser environment for accurate testing
## Learn More
* [Browser Mode Documentation](/guide/browser/)
* [Assertion API](/api/browser/assertions)
* [Interactivity API](/api/browser/interactivity)
* [Example Repository](https://github.com/vitest-tests/browser-examples)
---
---
url: /guide/browser/visual-regression-testing.md
---
# Visual Regression Testing
Vitest can run visual regression tests out of the box. It captures screenshots of your UI components and pages, then compares them against reference images to detect unintended visual changes.
Unlike functional tests that verify behavior, visual tests catch styling issues, layout shifts, and rendering problems that might otherwise go unnoticed without thorough manual testing.
## Why visual regression testing?
Visual bugs don’t throw errors, they just look wrong. That’s where visual testing comes in.
* That button still submits the form... but why is it hot pink now?
* The text fits perfectly... until someone views it on mobile
* Everything works great... except those two containers are outside the viewport
* That careful CSS refactor works... but broke the layout on a page no one tests
Visual regression testing acts as a safety net for your UI, automatically catching these visual changes before they reach production.
## Example
Visual regression testing in Vitest can be done through the [`toMatchScreenshot` assertion](/api/browser/assertions#tomatchscreenshot):
```ts
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
test('button renders in default state', async () => {
// render your component
// capture and compare screenshot
await expect(page.getByRole('button')).toMatchScreenshot()
})
```
## Getting started
### Environmental stability
Visual regression tests are **sensitive to environmental differences** because rendering is not perfectly deterministic across environments and depends on multiple factors:
* GPU, drivers, and hardware acceleration
* Operating System
* Font rendering pipelines
* Browser, browser versions, and settings
* Whether the browser is running headless or headed
* Screen scaling, color profiles, and display settings
* ...and occasionally what feels like the phase of the moon
In practice, even seemingly identical environments can occasionally produce subtle rendering differences. For this reason, **visual regression tests are most reliable when run in a standardized and tightly controlled environment**. This is also why [Docker containers](https://playwright.dev/docs/docker), [CI-only visual testing workflows, or cloud services](#visual-testing-for-teams) are strongly recommended.
### Not a replacement for behavior testing
When a visual test fails alongside behavior tests, it's harder to tell what's actually broken or why. Visual failures are also expected during intentional UI work, but a failing unit test usually is not. Keeping them separate means each suite can fail loudly for the right reasons.
It's worth calling out that **`toMatchScreenshot` is not a substitute for proper assertions**.
A test that renders a button and just takes a screenshot is just documenting the current state. There's no way to tell from a screenshot whether users can interact with the button. **Visual tests work best as a complementary layer on top of behavior tests, not a replacement for them**.
Put another way, **visual testing doesn't tell you why something renders the way it does**. It just tells you that something rendered a certain way, or a different way than it did last time.
For example, take a business requirement to sort recent purchases in a table by purchase date. If you're looking only at the visual regression tests, you might notice that the same items from the last test are in a different order. This could be because you just introduced the sorting or because the sorting is broken. Either way, you don't know why the order is different just by looking at the UI. Someone could dismiss the visual diff as noise because the table "looks the same", even though the ordering logic is now broken. Now you have a broken business requirement in production.
### Project structure
Separating your visual suite from other tests gives you cleaner failure signals and a more deliberate update workflow. The recommended setup uses [projects](/guide/projects) with a `[name].vrt.test.[ext]` naming convention to keep them distinct, and runs them in headless mode for consistency. As the browser instance might have a different default size, it also sets a specific viewport size.
```ts [vitest.config.ts]
import { defaultExclude, defineConfig } from 'vitest/config'
const vrtPattern = '**/*.vrt.test.[tj]s?(x)'
export default defineConfig({
test: {
// ...other configurations
projects: [
{
test: {
name: 'unit',
exclude: [vrtPattern, ...defaultExclude],
},
},
{
test: {
name: 'vrt',
browser: {
headless: true,
instances: [
{
browser: '[browser-name]',
viewport: { width: 1280, height: 720 },
},
],
},
include: [vrtPattern],
},
},
],
},
})
```
With this configuration in place, add scripts to launch each project separately:
```json [package.json]
{
"scripts": {
"test:unit": "vitest --project unit",
"test:visual": "vitest --project vrt"
}
}
```
### Creating references
When you run a visual test for the first time, Vitest creates a reference (also called baseline) screenshot and fails the test with the following error message:
```
expect(element).toMatchScreenshot()
No existing reference screenshot found; a new one was created. Review it before running tests again.
Reference screenshot:
tests/__screenshots__/button.vrt.test.ts/button-default-state-chromium-darwin.png
```
This is normal. Check that the screenshot looks right, then run the test again. Vitest will now compare future runs against this baseline.
::: tip
Reference screenshots live in `__screenshots__` folders next to your tests. **Commit them to your repository.**
:::
### Screenshot organization
By default, screenshots are organized as:
```
.
├── __screenshots__
│ └── test-file.vrt.test.ts
│ ├── test-name-chromium-darwin.png
│ ├── test-name-firefox-linux.png
│ └── test-name-webkit-win32.png
└── test-file.vrt.test.ts
```
The naming convention includes:
* **Test name**: either the first argument of the `toMatchScreenshot()` call, or automatically generated from the test's name.
* **Browser name**: depends on the configured browser provider, for example `chrome`, `chromium`, `firefox` or `webkit`.
* **Platform**: `aix`, `darwin`, `freebsd`, `linux`, `openbsd`, `sunos`, or `win32`.
This ensures screenshots from different environments don't overwrite each other.
### Updating references
When you intentionally change your UI, you'll need to update the reference screenshots just as you would update snapshots:
```bash
$ vitest --project vrt --update
```
Review updated screenshots before committing to make sure changes are intentional.
::: warning Stale screenshots
Note that **screenshots for deleted or renamed tests aren't removed automatically**. Clean up the `__screenshots__` folder manually when you remove or rename tests, otherwise stale references will accumulate over time.
:::
### Debugging failed tests
When a visual test fails, Vitest provides three images to help debug:
1. **Reference screenshot**: the expected baseline image
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:
```
expect(element).toMatchScreenshot()
Screenshot does not match the stored reference.
245 pixels (ratio 0.03) differ.
Reference screenshot:
tests/__screenshots__/button.vrt.test.ts/button-chromium-darwin.png
Actual screenshot:
tests/.vitest/attachments/button.vrt.test.ts/button-chromium-darwin-actual.png
Diff image:
tests/.vitest/attachments/button.vrt.test.ts/button-chromium-darwin-diff.png
```
While in UI mode, Vitest shows a tabbed diff view with an A/B slider as shown below.
An example of the visual regression diff UI, showing the "Diff", "Reference", "Actual", and "Slider" tabs, and how the slider reveals unexpected visual changes in a component.
#### Understanding the diff image
* **Red pixels** are areas that differ between reference and actual
* **Yellow pixels** are anti-aliasing differences (when anti-alias is not ignored)
* **Transparent/original** are unchanged areas
:::tip
If the diff is mostly red, something's really wrong. If it's speckled with a few red pixels around text, you probably just need to bump your threshold.
:::
## Configuring the `toMatchScreenshot` assertion
It's possible to configure the `toMatchScreenshot` assertion either globally, by changing its default options, or on a per-test basis.
To change the defaults, you have to change the [Vitest config](/config/browser/expect#tomatchscreenshot):
```ts{6-16} [vitest.config.ts]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
expect: {
toMatchScreenshot: {
comparatorName: 'pixelmatch',
comparatorOptions: {
// 0-1, how different can colors be?
threshold: 0.2,
// 1% of pixels can differ
allowedMismatchedPixelRatio: 0.01,
},
},
},
},
},
})
```
For more fine-grained control, override global settings in specific tests by passing options directly to the assertion:
```ts{2-6}
await expect(element).toMatchScreenshot('button', {
comparatorName: 'pixelmatch',
comparatorOptions: {
// more lax comparison for text-heavy elements
allowedMismatchedPixelRatio: 0.1,
},
})
```
## Third-party comparators
Vitest ships with `pixelmatch` as its built-in comparator. It's fast, compares images pixel-by-pixel, has no native dependencies, and handles the majority of cases well. Perceptual comparators aren't included by default because they bring heavier dependencies and there's no clear single "best one" to pick as different algorithms make different trade-offs, but the comparator API exists precisely to let you plug in whatever fits your needs. This decision may change as the ecosystem matures, though.
For use cases where pixel-level diffing produces excessive noise, a perceptual or structural similarity comparator may be a better fit. These compare images more like a human would, tolerating minor rendering differences while still detecting meaningful visual changes.
There are many algorithms, so these are a useful starting point:
* [`@blazediff/ssim`](https://blazediff.dev/docs/ssim), [SSIM (Structural Similarity Index)](https://en.wikipedia.org/wiki/Structural_similarity_index_measure) implementations for perceptual image quality assessment. It offers standard SSIM, MS-SSIM (Multi-Scale SSIM), and Hitchhiker’s SSIM for various use cases
* [`@blazediff/gmsd`](https://blazediff.dev/docs/gmsd), a single-threaded GMSD (Gradient Magnitude Similarity Deviation) metric for perceptual image quality assessment, good for CI environments
To use one, install and register it:
```ts{5-11,18-46} [vitest.config.ts]
import ssim from '@blazediff/ssim/ssim'
import type { SsimOptionsExtended } from '@blazediff/ssim/ssim'
import { defineConfig } from 'vitest/config'
declare module 'vitest/browser' {
interface ScreenshotComparatorRegistry {
'standard-ssim': SsimOptionsExtended & {
threshold?: number
}
}
}
export default defineConfig({
test: {
browser: {
expect: {
toMatchScreenshot: {
comparators: {
// naive implementation, always check the library's docs
'standard-ssim': (
reference,
actual,
{ createDiff, ...options }
) => {
const diffBuffer = createDiff
? new Uint8Array(reference.data.length)
: undefined
const output = ssim(
reference.data,
actual.data,
diffBuffer,
reference.metadata.width,
reference.metadata.height,
options,
)
const pass = output >= (options.threshold ?? 0.95)
return {
pass,
diff: diffBuffer ?? null,
message: pass ? null : `SSIM score: ${output}.`,
}
},
},
},
},
},
},
})
```
Once registered, the comparator can be referenced by name in your config or on a per-test basis:
:::code-group
```ts{8} [vitest.config.ts]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
expect: {
toMatchScreenshot: {
comparatorName: 'standard-ssim',
},
},
},
},
})
```
```ts{2} [button.vrt.test.tsx]
await expect(button).toMatchScreenshot('button', {
comparatorName: 'standard-ssim',
})
```
:::
## Best practices
### Test specific elements
Unless you explicitly want to test the whole page, prefer capturing specific components to reduce false positives:
```ts
// ❌ Captures entire page; prone to unrelated changes
await expect(page).toMatchScreenshot()
// ✅ Captures only the component under test
await expect(
page.getByRole('article', { name: 'Tote bag' })
).toMatchScreenshot()
```
### Handle dynamic content
Dynamic content like timestamps, user data, or random values will cause tests to fail. Either mock the underlying data sources or mask them using the [`mask` option](https://playwright.dev/docs/api/class-page#page-screenshot-option-mask) in `screenshotOptions` when using the Playwright provider.
```ts{8}
const profile = page.getByRole(
'article',
{ name: 'Gracie\'s profile' },
)
await expect(profile).toMatchScreenshot({
screenshotOptions: {
mask: [profile.getByRole('status')],
},
})
```
### Disable animations
::: tip
When using the Playwright provider, animations are automatically disabled when using the built-in assertion: the `animations` option's value in `screenshotOptions` is set to `"disabled"` by default.
If you prefer to disable all animations to save some execution time, continue reading.
:::
Animations can cause flaky tests. Disable them during testing by injecting a custom CSS snippet using [`setupFiles`](/config/setupfiles) or directly in your tests:
```ts
const stylesheet = document.createElement('style')
stylesheet.textContent = /* css */`
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
`
document.head.appendChild(stylesheet)
```
Alternatively, you can declare the CSS in a custom HTML template by using [`browser.testerHtmlPath`](/config/browser/testerhtmlpath).
### Set appropriate thresholds
Tuning thresholds is tricky. It depends on the content, test environment, what's acceptable for your app, and might also change based on the test.
Vitest does not define a default tolerance for mismatched pixels. The appropriate value depends on your application and environment. The recommendation is to use `allowedMismatchedPixelRatio`, so that the threshold is computed on the size of the screenshot and not a fixed number.
When setting both `allowedMismatchedPixelRatio` and `allowedMismatchedPixels`, Vitest uses whichever limit is stricter.
### Use Git LFS
Store reference screenshots in [Git LFS](https://github.com/git-lfs/git-lfs?tab=readme-ov-file) if you plan to have a large test suite.
## Common issues and solutions
### False positives from font rendering
Font availability and rendering varies significantly between systems. Some possible solutions might be to:
* Use web fonts and wait for them to load:
```ts
// wait for fonts to load
await document.fonts.ready
// continue with your tests
```
* Increase comparison threshold for text-heavy areas:
```ts{6-7}
await expect(
page.getByRole('article', { name: 'How to grow tomatoes' })
).toMatchScreenshot({
comparatorName: 'pixelmatch',
comparatorOptions: {
// 10% of the pixels are allowed to change
allowedMismatchedPixelRatio: 0.1,
},
})
```
* [Consider a shared environment setup](#visual-testing-for-teams) for consistent font rendering.
### Flaky tests or different screenshot sizes
If tests pass and fail randomly, or if screenshots have different dimensions between runs:
* Wait for everything to load, including loading indicators
* Set explicit viewport sizes: `await page.viewport(1920, 1080)`
* Check for responsive behavior at viewport boundaries
* Check for unintended animations or transitions
* Increase test timeout for large screenshots
* [Consider a shared environment setup](#visual-testing-for-teams)
## Visual testing for teams
Even with a controlled local setup, references generated on one machine will often fail on another. This matters as soon as more than one person is running the suite.
Running the visual regression suite in a shared environment solves this problem. There are three ways to do this:
1. **Self-hosted runners** (e.g., Docker images), complex to set up and maintain
2. **Generate references in CI**, which requires some setup
3. **Cloud services**, like [Azure App Testing](https://azure.microsoft.com/en-us/products/app-testing/), built to solve this exact problem, but usually restricted to specific providers and browsers
Options 2 and 3 are the quickest to get running, so those are covered below.
:::: tabs key:shared-environment-vrt
\=== GitHub Actions (CI)
GitHub runners don't have browsers preinstalled. Install them before running tests, using the steps for your provider:
::: tabs key:provider
\== Playwright
[Playwright](https://npmx.dev/package/playwright) makes this easy. Just pin your version and add this step before running tests:
```yaml [.github/workflows/ci.yml]
# ...the rest of the workflow
- name: Install Playwright Browsers
run: npx --no playwright install --with-deps --only-shell
```
\== WebdriverIO
[WebdriverIO](https://npmx.dev/package/webdriverio) installs browsers automatically if none can be found when a test run starts, but it's recommended to decouple the installation process. To help with this, the folks at [@browser-actions](https://github.com/browser-actions) have packaged scripts to install [Chrome](https://github.com/browser-actions/setup-chrome), [Edge](https://github.com/browser-actions/setup-edge), and [Firefox](https://github.com/browser-actions/setup-firefox) in convenient reusable actions:
```yaml [.github/workflows/ci.yml]
# ...the rest of the workflow
- uses: browser-actions/setup-chrome@v1
with:
chrome-version: 120
```
:::
Then in your existing workflow run the visual tests:
```yaml [.github/workflows/ci.yml]
# ...the rest of the workflow
# ...browser setup
- name: Visual Regression Testing
run: npm run test:visual
```
### The update workflow
Running `vitest --update` locally would generate screenshots on your machine, defeating the whole point of a controlled environment. Instead, you need a way to trigger the update in CI where the environment matches the one that runs the tests.
You don't want this to happen automatically on every PR *(chaos!)*. Instead, create a manually-triggered workflow that runs when there are intentional changes to the UI.
The workflow below:
* Only runs on feature branches (never on main)
* Credits the person who triggered it as co-author
* Prevents concurrent runs on the same branch
* Shows a nice summary:
* **When screenshots changed**, it lists what changed
* **When nothing changed**, well, it tells you that too
::: tip
This is just one approach. Some prefer PR comments (`/update-screenshots`), others use labels. Adjust it to fit your workflow.
The important part is having a controlled way to update reference screenshots.
:::
```yaml [.github/workflows/update-screenshots.yml]
name: Update Visual Regression Screenshots
on:
workflow_dispatch: # manual trigger only
env:
AUTHOR_NAME: 'github-actions[bot]'
AUTHOR_EMAIL: '41898282+github-actions[bot]@users.noreply.github.com'
COMMIT_MESSAGE: |
test: update visual regression screenshots
Co-authored-by: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
jobs:
update-screenshots:
runs-on: ubuntu-24.04
# safety first: don't run on main
if: github.ref_name != github.event.repository.default_branch
# one at a time per branch
concurrency:
group: visual-regression-screenshots@${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: write # needs to push changes
steps:
- name: Checkout selected branch
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
# use PAT if triggering other workflows
# token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure Git
run: |
git config --global user.name "${{ env.AUTHOR_NAME }}"
git config --global user.email "${{ env.AUTHOR_EMAIL }}"
# your setup steps here (node, pnpm, whatever)
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx --no playwright install --with-deps --only-shell
- name: Update Visual Regression Screenshots
run: npm run test:visual --update
# check what changed
- name: Check for changes
id: check_changes
run: |
CHANGED_FILES=$(git status --porcelain | awk '{print $2}')
if [ "${CHANGED_FILES:+x}" ]; then
echo "changes=true" >> $GITHUB_OUTPUT
echo "Changes detected"
# save the list for the summary
echo "changed_files<> $GITHUB_OUTPUT
echo "$CHANGED_FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "changed_count=$(echo "$CHANGED_FILES" | wc -l)" >> $GITHUB_OUTPUT
else
echo "changes=false" >> $GITHUB_OUTPUT
echo "No changes detected"
fi
# commit if there are changes
- name: Commit changes
if: steps.check_changes.outputs.changes == 'true'
run: |
git add -A
git commit -m "${{ env.COMMIT_MESSAGE }}"
- name: Push changes
if: steps.check_changes.outputs.changes == 'true'
run: git push origin ${{ github.ref_name }}
# pretty summary for humans
- name: Summary
run: |
if [[ "${{ steps.check_changes.outputs.changes }}" == "true" ]]; then
echo "### 📸 Visual Regression Screenshots Updated" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Successfully updated **${{ steps.check_changes.outputs.changed_count }}** screenshot(s) on \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "#### Changed Files:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.check_changes.outputs.changed_files }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ The updated screenshots have been committed and pushed. Your visual regression baseline is now up to date!" >> $GITHUB_STEP_SUMMARY
else
echo "### ℹ️ No Screenshot Updates Required" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The visual regression test command ran successfully but no screenshots needed updating." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All screenshots are already up to date! 🎉" >> $GITHUB_STEP_SUMMARY
fi
```
\=== Azure App Testing (Cloud service)
With this method, your tests stay local but the browsers run in the cloud. This is built on top of Playwright's remote browser feature and Azure handles all the infrastructure.
Everyone uses the same cloud browsers, so references are consistent regardless of who runs them. Tests work locally, you pay only for what you use, and there's nothing to maintain.
### Configuration
To have Playwright connect to the browsers spawned within the service, you have to update the provider configuration.
```ts{14-28} [vitest.config.ts]
import { env } from 'node:process'
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
// ...other configurations
projects: [
{
test: {
name: 'vrt',
browser: {
provider: playwright({
connectOptions: {
wsEndpoint: `${env.PLAYWRIGHT_SERVICE_URL}?${new URLSearchParams({
'api-version': '2025-09-01',
'os': 'linux', // always use Linux for consistency
// helps identifying runs in the service's dashboard
'runName': `Vitest ${env.CI ? 'CI' : 'local'} run @${new Date().toISOString()}`,
})}`,
exposeNetwork: '',
headers: {
Authorization: `Bearer ${env.PLAYWRIGHT_SERVICE_ACCESS_TOKEN}`,
},
timeout: 30_000,
}
}),
headless: true,
instances: [
{
browser: '[browser-name]',
viewport: { width: 1280, height: 720 },
},
],
},
include: [vrtPattern],
},
},
// ...other projects
],
},
})
```
To create a Playwright Workspace follow the [official guide](https://learn.microsoft.com/en-us/azure/app-testing/playwright-workspaces/quickstart-run-end-to-end-tests?tabs=playwrightcli\&pivots=playwright-test-runner#create-a-workspace).
Once your workspace is created, configure Vitest to use it:
1. **Set the endpoint URL**: following the [official guide](https://learn.microsoft.com/en-us/azure/app-testing/playwright-workspaces/quickstart-run-end-to-end-tests?tabs=playwrightcli\&pivots=playwright-test-runner#configure-the-browser-endpoint), retrieve the URL and set it as the `PLAYWRIGHT_SERVICE_URL` environment variable.
2. **Enable token authentication**: [enable access tokens](https://learn.microsoft.com/en-us/azure/app-testing/playwright-workspaces/how-to-manage-authentication?pivots=playwright-test-runner#enable-authentication-using-access-tokens) for your workspace, then [generate a token](https://learn.microsoft.com/en-us/azure/app-testing/playwright-workspaces/how-to-manage-access-tokens#generate-a-workspace-access-token) and set it as the `PLAYWRIGHT_SERVICE_ACCESS_TOKEN` environment variable.
::: danger Keep that token secret!
Never commit `PLAYWRIGHT_SERVICE_ACCESS_TOKEN` to your repository. Anyone with the token can rack up your bill. Use environment variables locally and secrets in CI.
:::
### Running tests
```bash
# Local development
npm run test:unit # runs locally using your browsers
npm run test:visual # uses cloud browsers
# Update screenshots
npm run test:visual -- --update
```
### CI setup
Add the secrets to your CI configuration:
```yaml
env:
PLAYWRIGHT_SERVICE_URL: ${{ vars.PLAYWRIGHT_SERVICE_URL }}
PLAYWRIGHT_SERVICE_ACCESS_TOKEN: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_TOKEN }}
```
Then run your tests like normal. The service handles the browser infrastructure.
::::
### Picking the right option
All approaches work. The real question is what pain points matter most to you and your team.
If you're comfortable with containerization, a self-hosted Docker setup gives you a controlled environment without any external dependencies or costs. The downside is maintenance as you own the setup, the browser versions, and any breakage.
CI runs work with any browser provider and give you full control, but screenshots can only be generated in CI. If someone runs `vitest --update` locally and commits the result, those references will likely fail on the next CI run. This is preventable by guarding the command behind a CI environment check.
A cloud service makes sense if you want developers to be able to run and update visual tests locally without risking mismatched references. It becomes even more useful when designers are involved in reviewing changes, or when the push-wait-check-fix-push cycle becomes a real bottleneck.
Still on the fence? Start with the CI workflow. You can always move to a container or cloud service later if it becomes a pain point.
## Going deeper
### How Vitest ensures screenshot stability
Visual regression tests rely on screenshots remaining stable across runs. In practice, pages are not instantly stable: images load asynchronously, animations finish at different times, fonts render, and layouts settle. To mitigate this, Vitest uses a "Stable Screenshot Detection" strategy:
1. It takes an initial screenshot (or uses the reference screenshot if available) as baseline
2. It takes another screenshot and compares it with the baseline
* If the screenshots match, the page is stable and testing continues
* If they differ, Vitest uses the newest screenshot as the baseline and repeats
3. This continues until stability is achieved or the timeout is reached
This ensures that transient visual changes (like loading spinners or animations) don't cause false positives. If something never stops animating, though, you'll hit the timeout, so consider [disabling animations during testing](#disable-animations).
If a stable screenshot is captured after one or more retries and a reference screenshot exists, Vitest performs a final comparison with the reference using `createDiff: true`. This will generate a diff image if they don't match.
During stability detection, Vitest calls comparators with `createDiff: false` since it only needs to know if screenshots match. This keeps the detection process fast.
---
---
url: /guide/browser/trace-view.md
---
# Trace View 5.0.0
`browser.traceView` records browser interactions as DOM snapshots and lets you replay them step by step in Vitest's built-in trace viewer. It is useful when the live browser view is not enough: you can inspect earlier tests, failed retries, screenshots, assertions, and user actions after the browser has already moved on.
Trace view is additive to the current browser testing workflow. Enabling it does not force a single debugging mode. You can use it with the normal local browser UI, with a headless browser and Vitest UI, or with the HTML reporter in CI.
::: tip Trace view, browser UI, and HTML reports
The normal local browser mode opens the [browser UI](/config/browser/ui), where tests run in a visible iframe. This is useful while developing, but the iframe only shows the current browser state. When another test runs, the previous rendered state is gone.
`browser.traceView` keeps a replayable record for each test. In local browser UI mode, the trace viewer appears alongside the existing live view so you can keep using the browser UI while also inspecting recorded steps.
For static output, add the [HTML reporter](/guide/reporters#html-reporter). The same trace viewer can then be opened from the generated report, which is useful for run-mode and CI failures.
:::
::: details Looking for Playwright traces?
This page now documents Vitest's built-in `browser.traceView` feature. The previous `browser.trace` guide for Playwright traces moved to [Playwright Traces](./playwright-traces).
:::
## Quick Start
Enable trace view with the [`browser.traceView`](/config/browser/traceview) option:
::: code-group
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
traceView: true,
},
},
})
```
```bash [CLI]
vitest --browser.traceView
```
:::
When `browser.traceView` is enabled, tests with recorded traces can be opened in the trace viewer from the [browser UI](/config/browser/ui), [Vitest UI](/guide/ui), and [HTML reporter](/guide/reporters#html-reporter). The viewer has two resizable panes:
* **Step list** (left) — every recorded action, assertion, mark, and lifecycle entry, with name, timing, selector, and source location. Failed actions and assertions are highlighted in red.
* **DOM snapshot** (right) — a reconstruction of the page at the selected step. The interacted element is highlighted in blue.
Selecting a step also opens its source location in the Editor tab when that location is available.
Example replay uses [Vuetify's](https://github.com/vuetifyjs/vuetify) `VDateInput` component.
## Common Setups
`browser.traceView` records traces. The browser mode, UI, and reporter options determine where you inspect them.
| Goal | Configuration | Result |
| --- | --- | --- |
| Add trace replay to the normal local browser UI | `vitest --browser.traceView` | Uses the default local headed browser UI and adds trace replay for recorded tests. |
| Debug locally with a headless browser | `vitest --browser.traceView --browser.headless --ui` | The browser runs headless, while Vitest UI shows recorded trace steps and snapshots. |
| Debug locally with a visible browser window and Vitest UI | `vitest --browser.traceView --browser.headless=false --browser.ui=false --ui` | Vitest UI shows recorded trace steps and snapshots, while tests run in a separate headed browser window. |
| Generate a static report for CI or run mode | `vitest run --browser.traceView --reporter=html` | The HTML report includes the trace viewer for recorded tests. |
## Relation to Playwright Traces
`browser.traceView` and [`browser.trace`](/config/browser/trace) are independent features:
| | `browser.traceView` | `browser.trace` |
| ---------------------- | --------------------------------------------------------- | ---------------------------------------------- |
| Provider support | All providers (playwright, webdriverio, preview) | Playwright only |
| Viewer | Browser UI / Vitest UI / HTML reporter | Playwright Trace Viewer / trace.playwright.dev |
| Format | [rrweb](https://github.com/rrweb-io/rrweb) DOM snapshots | Playwright `.trace.zip` |
| Requires external tool | No | Yes (`npx playwright show-trace`) |
You can enable both at the same time. See [Playwright Traces](./playwright-traces) for the `browser.trace` workflow.
## Recorded Steps
Trace entries are recorded automatically for:
* `expect.element(...)` assertions
* Interactive actions like `click`, `dblClick`, `tripleClick`, `fill`, `clear`, `type`, `hover`, `selectOptions`, `upload`, `dragAndDrop`, `tab`, `keyboard`, `wheel`, and screenshots
* Test runner lifecycle event (e.g. `vitest:onAfterRetryTask` is recorded after each test and retry run)
Each entry captures the DOM state at that point, along with timing information, the selector, and the source location that triggered it.
In Vitest UI, trace entries are streamed as the test runs, so you can inspect recorded steps before the test finishes. Long-running actions, `expect.element(...)` assertions, and callback `page.mark()` entries appear as in-progress steps first, then update with their final status and duration.
## Custom Trace Entries
You can insert your own named entries with `page.mark()` and `locator.mark()`:
```ts
import { page } from 'vitest/browser'
await page.mark('content rendered')
await page.getByRole('button', { name: 'Sign in' }).mark('sign in button')
```
You can also pass a callback to `page.mark()`. Note that grouping is not currently supported — each inner action is recorded individually, and the mark entry appears at the end:
```ts
await page.mark('sign in flow', async () => {
await page.getByRole('textbox', { name: 'Email' }).fill('john@example.com')
await page.getByRole('textbox', { name: 'Password' }).fill('secret')
await page.getByRole('button', { name: 'Sign in' }).click()
})
```
Use [`vi.defineHelper()`](/api/vi#vi-defineHelper) to make entries from reusable helpers point to the call site rather than the helper's internals:
```ts
import { vi } from 'vitest'
import { page } from 'vitest/browser'
const renderContent = vi.defineHelper(async (html: string) => {
document.body.innerHTML = html
await page.elementLocator(document.body).mark('render')
})
test('shows button', async () => {
await renderContent('') // trace entry points here
})
```
## Retries and Repeats
Each attempt — retry or repeat — is recorded as a separate trace. When a test has multiple attempts, the viewer opens the most recent one by default. You can switch between attempts in the Report tab.
## Snapshot Fidelity
By default, trace view captures the DOM tree, attributes, form values, same-origin readable CSS, element scroll positions, viewport size, and window scroll position. Images and canvas pixels are not inlined by default.
Stylesheets are captured through the browser's CSSOM. Readable `