Skip to content

Vitest 5.0 is out!

September 4th, 2026

Vitest 5 Announcement Cover Image

The next Vitest major is here

Today, we are thrilled to announce Vitest 5!

Quick links:

If you've not used Vitest before, we suggest reading the Getting Started and Features guides first.

We extend our gratitude to the over 790 contributors to Vitest Core and to the maintainers and contributors of Vitest integrations, tools, and translations who have helped us develop this new major release. We encourage you to get involved and help us improve Vitest for the entire ecosystem. Learn more at our Contributing Guide.

To get started, we suggest helping triage issues, review PRs, send failing tests PRs based on open issues, and support others in Discussions and Vitest Land's help forum. If you'd like to talk to us, join our Discord community and say hi on the #contributing channel.

For the latest news about the Vitest ecosystem and Vitest core, follow us on Bluesky or Mastodon.

To stay updated, keep an eye on the VoidZero blog and subscribe to the newsletter.

Performance Improvements

Performance was the main focus of this release. To measure it, we built vitest-dev/benchmarks: a set of generated reference apps, from a 5-file utility package to an enterprise monolith with 1,280 modules and a barrel-file-heavy app with 817 modules. Every app runs across pools (forks, threads, vmForks, vmThreads), environments (node, jsdom, happy-dom, and Browser Mode), with and without isolation, so we can see how each change behaves on realistic projects instead of micro-benchmarks.

Here is a selection of cells from the comparison between Vitest 4.1.10 and Vitest 5.0 (Apple M4, 10 cores, Node 24, whole-process wall clock of vitest run, median of 3 runs):

AppConfigurationVitest 4.1Vitest 5.0Change
micro-utils (5 test files)vmThreads, jsdom0.61s0.56s−8%
node-library (40 test files)forks, isolated0.86s0.75s−13%
deps-heavyvmThreads1.59s0.74s−53%
react-spa (92 modules)vmThreads, jsdom1.25s1.07s−15%
react-spa (92 modules)Browser Mode, Chromium2.40s2.01s−16%
vue-spa (37 components)Browser Mode, Chromium1.94s1.58s−18%
design-system (80 components)vmThreads, jsdom2.09s1.72s−18%
barrel-hell (817 modules)forks, isolated, fsModuleCache1.33s1.08s−18%
enterprise-monolith (1,280 modules)forks, isolated7.24s5.83s−19%
long-haul (80 jsdom files)vmForks, happy-dom5.43s4.06s−25%
cpu-bound (30 test files)threads, 100% workers0.91s0.83s−8%

The biggest wins are in the vm pools, in Browser Mode, and in large isolated suites. Cells that were already dominated by the environment setup, like forks with jsdom and isolation, stay within ±3% of Vitest 4.1. The full result set for every cell is in the benchmarks repository.

Some of the changes behind these numbers:

  • Inline projects share the Vite server. Projects defined in test.projects that don't change the Vite config now reuse the Vite server of the config that declares them, so shared files are transformed once. See sharedViteServer.
  • File system module cache is stable. The fsModuleCache option (previously experimental.fsModuleCache) persists transformed modules on disk, so they are reused across reruns and separate Vitest processes. Plugins can participate in the cache key with defineCacheKeyGenerator.
  • Fewer round trips between the main process and workers. Warm modules are served to workers in one round trip.
  • Faster vm pools. vmThreads and vmForks reuse compiled code across contexts and prewarm the module graph. They also support require(esm) now.
  • Faster Browser Mode. Vitest prebundles its own runtime, prewarms the browser while the Vite server starts, opens browser sessions adaptively instead of maxWorkers sessions upfront, and cuts per-file round trips.
  • Smaller install. Vitest now bundles its own dependencies, which reduces the number of packages in node_modules and the time spent resolving them.
  • Faster coverage. The v8 provider merges reports with bounded memory and precompiled globs, both providers send less data over RPC, and istanbul moved to the maintained @vitest/istanbuljs packages. The coverage tables in the benchmarks repository list every app.

The duration breakdown in the reporter output now shows percentages, so it's easier to see where the time goes:

Duration  3.76s (environment 79%, import 13%, transform 6%, tests 1%, setup 1%)

Trace View

Vitest 5 adds a built-in Trace View for Browser Mode. When browser.traceView is enabled, Vitest records every interaction, assertion, and page.mark as a DOM snapshot and lets you replay the test step by step after the browser has already moved on. The viewer is available in the browser UI, in Vitest UI, and in the HTML reporter, so it works for local debugging and for CI failures.

Select a step to see the reconstructed page at that moment with the interacted element highlighted, and Vitest opens the source location in the editor panel. Failed actions and assertions are highlighted in red. Trace view also supports keyboard navigation and live updates in watch mode.

ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    browser: {
      traceView: true,
    },
  },
})
bash
vitest --browser.traceView

Unlike Playwright Traces, trace view does not depend on the provider and does not require a separate viewer.

Nested Projects and Config Inheritance

Inline projects now inherit the root config by default, including Vite options like plugins and resolve.alias. In Vitest 4, you had to set extends: true on every project to get this behavior:

vitest.config.ts
ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    projects: [
      {
        extends: true, 
        test: {
          name: 'unit',
          include: ['**/*.unit.test.ts'],
        },
      },
    ],
  },
})

A config file referenced in test.projects can now declare its own projects. Such a config acts as a container, exactly like the root config, and provides nested projects named app (unit), app (e2e), and so on. This makes it possible to reference a package that already defines its own projects without duplicating them at the root:

packages/app/vitest.config.ts
ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    projects: ['./packages/*/vitest.config.ts'],
  },
})

The --project filter is aware of the hierarchy, and it now has a -p shorthand:

bash
vitest -p app

vi.when

Defining different return values for different arguments used to require a manual mockImplementation with argument checks. The new vi.when API defines per-argument behaviors on a spy. Arguments are matched with deep equality and support asymmetric matchers like expect.any():

ts
import { expect, test, vi } from 'vitest'

test('returns user data', async () => {
  const findById = vi.fn()

  vi.when(findById)
    .calledWith(1)
    .thenResolve({ id: 1, name: 'Ella' })
    .calledWith(2)
    .thenResolve({ id: 2, name: 'Gracie' })
    .calledWith(expect.any(Number))
    .thenReject(new Error('not found'))

  await expect(findById(1)).resolves.toEqual({ id: 1, name: 'Ella' })
  await expect(findById(3)).rejects.toThrow('not found')
})

Behaviors can be limited with thenReturnOnce or a times option, and the new toHaveBeenExhausted assertion checks that every registered behavior was consumed. Read more in the Conditional Mocking recipe.

Benchmarking Rewrite

The benchmarking API was rewritten. bench is no longer a top-level import; it is a test-context fixture available inside regular test() calls in benchmark files. This gives benchmarks access to everything the test runner offers: fixtures, lifecycle hooks, retries, filtering, and assertions.

parse.bench.ts
ts
import { expect, test } from 'vitest'

test('compare parsers', async ({ bench }) => {
  const result = await bench.compare(
    bench('JSON.parse', () => {
      JSON.parse('{"key":"value"}')
    }),
    bench('custom parser', () => {
      customParse('{"key":"value"}')
    }),
  )

  expect(result.get('JSON.parse')).toBeFasterThan(result.get('custom parser'))
})

Results can be stored with writeResult and replayed with bench.from() to compare against a baseline, and the built-in Tinybench provider can be replaced with a custom benchmark provider. Benchmark output is now part of the default and json reporters. See the Benchmarking guide for the full API.

Locator Errors Show the ARIA Tree

When a locator cannot find an element in Browser Mode, Vitest now prints the ARIA snapshot of the searched subtree next to the HTML output. The accessibility tree is usually much shorter than the raw HTML and shows exactly the roles and names that getByRole and getByLabelText match against. The output is controlled by the new browser.locators.errorFormat option:

ts
export default defineConfig({
  test: {
    browser: {
      locators: {
        errorFormat: 'aria', // 'html' | 'aria' | 'all'
      },
    },
  },
})

Locators are also strict by default: locators.exact is enabled, so getByText('Item') no longer matches Item 1 by accident.

Mocking Temporal

Fake timers now mock the Temporal API alongside Date, thanks to the @sinonjs/fake-timers v15.4 update. This applies both to vi.useFakeTimers() and to vi.setSystemTime() used without fake timers:

ts
vi.setSystemTime(0)
Temporal.Now.instant().epochMilliseconds // 0

Temporal is part of the default set of faked APIs. To avoid faking it, add it to toNotFake in the config or when invoking vi.setSystemTime().

Stricter Assertions

Asynchronous assertions like resolves, rejects, and toMatchFileSnapshot now fail the test when they are not awaited. Before, Vitest awaited them at the end of the test and only printed a warning. The test still passed even though the assertion never ran at the point where it was written.

ts
test('unawaited assertion', async () => {
  expect(promise).resolves.toBe(1) 
  await expect(promise).resolves.toBe(1) 
})

expect.poll now rejects when it does not settle within timeout, and the callback receives an AbortSignal so you can cancel in-flight work:

ts
await expect.poll(async ({ signal }) => {
  const response = await fetch('/api/status', { signal })
  return response.status
}, { timeout: 1000 }).toBe(200)

Assertion types now expose both the return type and the received type. When you extend matchers, the Matchers interface now takes the return type as its first parameter:

ts
import 'vitest'

declare module 'vitest' {
  interface Matchers<T = any> { 
    toBeFoo: () => void
  } 
  interface Matchers<R, T> { 
    toBeFoo: () => R
  } 
}

R reflects how the matcher is used: void when called synchronously, Promise<void> through .resolves, .rejects, expect.poll, or expect.element. T is the type of the received value, so an expected argument can be typed the same as the value under test:

ts
declare module 'vitest' {
  interface Matchers<R, T> {
    toEqualTyped: (expected: T) => R
  }
}

expect(1).toEqualTyped(2) // ✅
expect(1).toEqualTyped('2') // ❌ type error

The same change applies to code that refers to assertion types directly:

ts
Assertion<string>
Assertion<void, string>
Assertion<Promise<void>, string> // asynchronous assertion

Custom matchers also get access to the underlying Chai assertion object.

clearMocks is Enabled by Default

clearMocks now defaults to true. Vitest calls vi.clearAllMocks() before every test, so a mock no longer carries call history from one test into the next while the implementations stay intact. This removes one of the most common sources of order-dependent tests. To keep the previous behavior, set clearMocks: false.

Reporters Updates

Reporters and other integrations now write their output into a single .vitest directory at the project root: the html, json, and junit reporters, failure screenshots, and new traces all use it by default. This reduces the number of entries you need to add to .gitignore to one.

Third-party reporters can use the same convention through the new vitest.createReport(scope) API, which returns a Report limited to its own .vitest/<scope> directory.

The HTML reporter can also produce a self-contained report with the singleFile option. Vitest inlines the UI assets, metadata, and test attachments into one index.html, which is easy to upload as a CI artifact:

ts
export default defineConfig({
  test: {
    reporters: [
      ['html', { singleFile: true }],
    ],
  },
})

Other Improvements

  • The new --repeats CLI option repeats every test a given number of times regardless of the result, which is useful for hunting flaky tests.
  • injectCjsGlobals makes it possible to disable the injection of module, exports, require, __filename, and __dirname into ES modules.
  • coverage.autoAttachSubprocess tracks the coverage of node:child_process and node:worker_threads spawned during the test run with the v8 provider.
  • coverage.thresholds.perFile accepts an object, and thresholds.autoUpdate receives the previous threshold as an argument.
  • The json reporter accepts a filterMeta option, and the junit reporter supports jest-junit-compatible naming options.
  • TestCase.logs() exposes the console output recorded during a test to reporters and the advanced API.
  • Test titles and inspected values use pretty-format, and test.for/test.each title placeholders support non-ASCII characters.
  • vitest --merge-reports supports non-sharded runs across multiple environments.
  • Coverage switched to the @vitest/istanbuljs packages, a maintained fork of the istanbul-lib-* family.

Breaking Changes

Vitest 5 requires Vite >= 6.4.0 and Node.js >= 22.12.0. Vitest 5 has several breaking changes that could affect you, so we advise reviewing the detailed Migration Guide before upgrading.

The complete list of changes is at the Vitest 5 Changelog.

Acknowledgments

Vitest 5 is the result of countless hours of work by the Vitest team and our contributors. None of it would be possible without the individuals and companies that sponsor Vitest. Vladimir and Hiroshi work on Vite and Vitest full-time at VoidZero, and Chromatic gives Ari the time to keep pushing Vitest forward. A big thank you to everyone supporting us through GitHub Sponsors and Open Collective.