Skip to content

Vitest 5.1 is out! ​

November 2, 2026

Vitest 5.1 Announcement Cover Image

The next Vitest minor is here ​

Today, we are thrilled to announce Vitest 5.1!

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 840 contributors to Vitest Core and to the maintainers and contributors of Vitest integrations, tools, and translations who have helped us develop this new 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.

Run Only Affected Tests with --since-last-run ​

The --changed flag compares your files with a Git reference. It needs a repository, and a change stops being "changed" as soon as you commit it. The new experimental --since-last-run flag compares the files with the last run of each test instead:

sh
vitest run --since-last-run

The first run with the flag runs every test and records the files each test loaded, including dynamic imports, mocks, setup files, and snapshot files. Every later run checks the modification time of those files and runs only the tests whose files changed or were deleted since their last run. A test file that failed in its last run also runs again until it passes.

Each run updates the records, so the next run selects only the tests affected by the edits made in between. Git is not involved, so the flag works the same in a fresh container, in a temporary worktree, or in a directory that is not a repository. This makes it a good fit for AI coding agents that run tests after every edit:

AGENTS.md
md
- After a change, run `vitest run --since-last-run` to test only what the change affects.
- Before you finish, run `vitest run` once for the whole suite.

The flag is built on experimental.recordDependencies and the new mtime VCS provider. Custom VCS providers now also receive a resolver that returns every file the test files of the run depend on. See Running Only Affected Tests to choose between --changed and --since-last-run.

Interactive Snapshot Review ​

When snapshots fail in watch mode, the u key updates all of them at once. Vitest 5.1 adds an interactive review, inspired by Insta's review command, so you can check each change before you accept it. Press i, and Vitest shows the failed snapshots one at a time:

snapshot-review.ansi
Reviewing [1/3]:
Source: src/user-card.test.ts:7:25 (renders the full name)
Snapshot: renders the full name 1
────────────────────────────────────────────────────────────────────────────────
- Expected
+ Received
────────────┬───────────────────────────────────────────────────────────────────
    1     1 │  {
    2       │-   "name": "Ada",
          2 │+   "name": "Ada Lovelace",
    3     3 │    "role": "admin",
    4     4 │  }
────────────┴───────────────────────────────────────────────────────────────────

  a accept  keep the new snapshot
  r reject  retain the old snapshot

Press a to accept the new snapshot or r to keep the old one. After the last snapshot, Vitest writes the accepted snapshots and reruns the test files that contain them. The review covers inline snapshots, .snap files, and file snapshots, including ARIA snapshots in Browser Mode.

Trace View on Failure by Default ​

Trace View, introduced in Vitest 5.0 as an experimental feature, is now stable and enabled by default in a new 'failure-only' mode. In this mode, Vitest keeps the trace only for tests that fail. A passing test records its steps in the browser, but it never takes a DOM snapshot or sends anything to the server, so the mode adds no measurable cost to passing tests.

When a browser test fails, the error report now ends with the steps that led to the failure:

Error report
 FAIL src/sign-in.test.ts > signs in
Error: expect(element).toHaveTextContent()

Expected element to have text content:
  Welcome
Received:
  Sign in

 ❯ src/sign-in.test.ts:11:51
      9|   })
     10|   await page.getByRole('button', { name: 'Sign in' }).click()
     11|   await expect.element(page.getByRole('heading')).toHaveTextContent('Welcome')
       |                                                   ^
     12| })

Caused by: Error: Matcher did not succeed in time.

Trace:
  step                       locator                                   start  duration  location
  fill form                  -                                          +1ms      58ms  src/sign-in.test.ts:6:14
  ├── fill                   getByLabel('Email')                        +2ms      31ms  src/sign-in.test.ts:7:40
  └── fill                   getByLabel('Password')                    +34ms      24ms  src/sign-in.test.ts:8:43
  click                      getByRole('button', { name: 'Sign in' })  +61ms      18ms  src/sign-in.test.ts:10:55
  toHaveTextContent [ERROR]  getByRole('heading')                      +80ms    1003ms  src/sign-in.test.ts:11:16

The same trace, with a snapshot of the page at the moment of failure, is available in the browser UI, in Vitest UI, and in the HTML reporter. To record every test with a snapshot at each step, set the mode to 'on':

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

export default defineConfig({
  test: {
    browser: {
      traceView: 'on', // 'failure-only' (default) | 'on' | 'off'
    },
  },
})

browser.traceView.enabled is deprecated in favor of browser.traceView.mode.

The trace viewer also captures the accessibility tree of the page with each snapshot. A selector in the viewer header switches between the DOM and Aria views. The Aria view shows the roles and accessible names that locators like getByRole() match against, in the same format as ARIA snapshots.

Trace Timeline in Node Tests ​

The step timeline is not limited to Browser Mode. With the new experimental experimental.traceTimeline option, Vitest records the expect.poll, vi.waitFor, and vi.waitUntil steps of each test and prints them under the error when the test fails. This shows what the test waited on and how long each wait took, which helps with flaky tests that wait on servers, queues, or file watchers.

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

export default defineConfig({
  test: {
    experimental: {
      traceTimeline: true,
    },
  },
})
Error report
 FAIL src/search.test.ts > finds new user in search
AssertionError: expected [] to have a length of 1 but got +0

- Expected
+ Received

- 1
+ 0

 ❯ src/search.test.ts:8:50
      6|   await api.post('/users', { name: 'alice' })
      7|   await vi.waitUntil(() => db.users.findByName('alice'), { message: 'user saved' })
      8|   await expect.poll(() => search.query('alice')).toHaveLength(1)
       |                                                  ^
      9| })

Caused by: Error: Matcher did not succeed in time.

Trace:
  step                           start  duration  location
  vi.waitFor()                    +2ms     120ms  src/search.test.ts:5:12
  user saved (vi.waitUntil)     +171ms      42ms  src/search.test.ts:7:12
  expect.poll().toHaveLength()  +214ms    1001ms  src/search.test.ts:8:50

Vitest keeps the steps only for failed attempts, so the option adds almost no cost to passing tests.

vi.waitFor and vi.waitUntil now accept a message option, like expect.poll already does. Vitest adds the message to the start of the error, and the message names the step in the timeline, as with user saved (vi.waitUntil) in the report above:

ts
await vi.waitFor(() => server.ping(), { message: 'server started' })
// Error: server started: connect ECONNREFUSED

Rerun Failures with --last-failed ​

Watch mode can already rerun failed tests with the f key. The new --last-failed flag does the same across separate processes: it reads the failures of the last run from the results cache and runs only those tests.

sh
vitest run --last-failed

# combine with other filters
vitest run --last-failed -t login

Vitest selects the tests by their full names, so adding or moving other tests in the file does not change the selection. If a file failed outside of a test, for example in a beforeAll hook or during collection, the whole file runs. Custom integrations can use the same filter through the new TestSpecification.testNames property.

Faster Module Mocking in Browser Mode ​

To mock a module in Browser Mode, Vitest intercepted the network requests of the page (with Playwright's page.route, or a Vite plugin for the other providers) and served the mocked code instead. Every request in the page paid for the interception, and the cost grew with the number of modules a test file imports.

Vitest 5.1 serves mocks through an import map instead. When a test file calls vi.mock, Vitest adds an import map rule that points the module URL to its mocked version. The browser loads the mocked module itself, so no request is intercepted, and the HTTP cache stays enabled.

On a public benchmark with 500 test files, 962 modules, and 1,883 mocks per suite (Chromium, 9 workers, median of 3 runs):

SuiteVitest 5.0.3Vitest 5.1Change
No mocks31.7s26.6s−16%
Mocks56.4s27.2s−52%
Mixed44.0s27.1s−38%

The new browser.importMapMocks option is experimental. It is enabled when the browser supports multiple import maps per document (Chromium 133+, Safari 18.4+, and Firefox 150+ with the Playwright provider). In other browsers, and when isolate is disabled, Vitest intercepts requests as before.

An import map cannot change a module that the page already imported, so Vitest now reports an error when a module was imported before vi.mock registered it, instead of silently using the original module. A mock factory can also only provide the exports that the original module declares. If this breaks your tests, set browser.importMapMocks to false and let us know.

Pointer API ​

The new experimental userEvent.pointer method performs a sequence of low-level pointer actions. It uses the user-event pointer syntax, so you can press, move, and release the pointer separately, for example to drag an element to an exact position:

ts
import { test } from 'vitest'
import { page, userEvent } from 'vitest/browser'

test('drags the card into the column', async () => {
  const card = page.getByRole('listitem', { name: 'Fix the login form' })
  const column = page.getByRole('list', { name: 'Done' })

  await userEvent.pointer([
    { target: card, keys: '[MouseLeft>]' },
    { target: column, coords: { x: 10, y: 10 }, keys: '[/MouseLeft]' },
  ])
})

Fake Timers for node:timers ​

Fake timers replace the global setTimeout and similar functions, but code that imports them from node:timers or node:timers/promises kept the real timers. With the new fakeNodeBuiltins option, Vitest also fakes these modules, including named imports and require calls:

ts
import { setTimeout as sleep } from 'node:timers/promises'
import { expect, test, vi } from 'vitest'

test('resolves after the delay', async () => {
  vi.useFakeTimers({ fakeNodeBuiltins: true })

  const resolved = vi.fn()
  sleep(1000).then(resolved)

  await vi.advanceTimersByTimeAsync(1000)
  expect(resolved).toHaveBeenCalled()
})

TIP

fakeNodeBuiltins is disabled by default. We plan to enable it by default in the next major version. To prepare for it, set fakeTimers.fakeNodeBuiltins to true in your config now.

vi.setSystemTime now also accepts a Temporal.Instant or a Temporal.ZonedDateTime. A ZonedDateTime sets the mocked time, but it does not change the time zone of the environment:

ts
vi.setSystemTime(Temporal.Instant.from('2026-11-02T09:00:00Z'))

In Browser Mode, locator actions like click and fill, and expect.element, now advance fake timers while they wait for the element, the same way expect.poll and vi.waitFor do. Before, an action timed out if the element appeared only after a faked setTimeout.

Results of Each Attempt ​

A test runs more than once when it has retry or repeats. Until now, TestCase.result() reported only the final state and merged the errors of all runs. The new TestCase.attempts() method returns the state, errors, and timing of each run separately, so a reporter can find tests that passed only after a retry:

flaky-reporter.ts
ts
import type { Reporter } from 'vitest/node'

export default {
  onTestCaseResult(testCase) {
    const attempts = testCase.attempts()

    if (testCase.result().state === 'passed' && attempts.length > 1) {
      console.warn(`${testCase.fullName} passed after ${attempts.length} attempts`)
    }
  },
} satisfies Reporter

The default reporter also uses this information. A test that fails in several attempts now prints one header with the attempts, instead of one identical header per attempt:

 FAIL  basic.test.js > always fails (initial run, 1st retry, 2nd retry)
AssertionError: expected 1 to be 2 // Object.is equality

Coverage Improvements ​

The HTML coverage report has new light and dark themes that match the Vitest style. The report follows your OS color preference, and the theme picker in the report keeps your choice. Inside Vitest UI, the report uses the theme of the UI. To keep the original Istanbul style, set theme: 'legacy' in the reporter options:

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

export default defineConfig({
  test: {
    coverage: {
      reporter: [['html', { theme: 'legacy' }]], // 'light' | 'dark' | 'legacy'
    },
  },
})

The istanbul provider is much faster. The @vitest/istanbuljs packages no longer use Babel: they parse the code with Oxc through Rolldown and insert the coverage counters with magic-string. This makes instrumentation 5 to 10 times faster and uses less memory. For example, the instrumentation of the 479 source files of the Vitest repository went from 3.7s to 0.38s. The instrumented code also no longer includes the input source maps, which reduces the code sent to the workers by about 26%.

The v8 provider has a new coverage.ignoreNode option. It ignores code by its AST node, which is useful for code that a compiler or a plugin generates and that cannot have an ignore hint, like the cache branches that the React Compiler adds to every component:

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

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      ignoreNode: (node, type) =>
        type === 'statement'
        && node.type === 'ExpressionStatement'
        && node.expression.type === 'CallExpression'
        && node.expression.callee.type === 'Identifier'
        && node.expression.callee.name === '__generatedHelper',
    },
  },
})

Performance Improvements ​

  • Reused workers with isolate: false. Workers now cache the resolution of setup files and their mocks, and they no longer request a new snapshot of warm modules for every test file. On a benchmark with 800 test files, the resolution cache saves about 10%, and the warm modules change saves up to 18% when every test file imports a large shared module tree.
  • Typed arrays in toEqual. Vitest compares typed arrays in one loop. A toEqual of two 2 MiB arrays went from about 1.5s to 125ms.
  • Trace View in Firefox. Vitest resolves only the first usable stack frame of each step. With the 'on' mode, a test with 200 expect.element calls went from 5.7s to 0.33s in Firefox.
  • Browser Mode under load. The WebSocket server now handles messages of all browsers in turn, so a busy browser no longer delays the actions of other browsers until they time out.

Other Changes ​

  • The default port in Browser Mode is now 23315 instead of 63315. Windows can reserve the old port when Hyper-V, WSL2, or Docker Desktop runs, and Vitest then failed to start. If your setup depends on the old port, set api.port.
  • api.port: 0 now lets the OS choose a free port. This requires Vite 8.2.1 or newer.
  • If a browser test file cannot connect to the server, the run now fails with an error instead of waiting forever.

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

Acknowledgments ​

Vitest 5.1 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. Vitest also sponsors Raul's work as a member of the Vitest team through the Cloudflare fund. A big thank you to everyone supporting us through GitHub Sponsors and Open Collective.