Skip to content

Vitest 4.1 is out!

March 12, 2026

Vitest 4.1 Announcement Cover Image

The next Vitest minor is here

Today, we are thrilled to announce Vitest 4.1 packed with new exciting features!

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 713 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.

Vite 8 Support

This release adds support for the new Vite 8 version. Additionally, Vitest now uses the installed vite version instead of downloading a separate dependency, if possible. This makes issues like type inconsistencies in your config file obsolete.

Test Tags

Tags let you label tests to organize them into groups. Once tagged, you can filter tests by tag or apply shared options — like a longer timeout or automatic retries — to every test with a given tag.

To use tags, define them in your configuration file. Each tag requires a name and can optionally include test options that apply to every test marked with that tag. For the full list of available options, see tags.

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

export default defineConfig({
  test: {
    tags: [
      {
        name: 'db',
        description: 'Tests for database queries.',
        timeout: 60_000,
      },
      {
        name: 'flaky',
        description: 'Flaky CI tests.',
        retry: process.env.CI ? 3 : 0,
      },
    ],
  },
})

With this configuration, you can apply flaky and db tags to your tests:

ts
test('flaky database test', { tags: ['flaky', 'db'] }, () => {
  // ...
})

The test has a timeout of 60 seconds and will be retried 3 times on CI because these options were specified in the configuration file for db and flaky tags.

Inspired by pytest, Vitest supports a custom syntax for filtering tags:

  • and or && to include both expressions
  • or or || to include at least one expression
  • not or ! to exclude the expression
  • * to match any number of characters (0 or more)
  • () to group expressions and override precedence

Here are some common filtering patterns:

shell
# Run only unit tests
vitest --tags-filter="unit"

# Run tests that are both frontend AND fast
vitest --tags-filter="frontend and fast"

# Run frontend tests that are not flaky
vitest --tags-filter="frontend && !flaky"

# Run tests matching a wildcard pattern
vitest --tags-filter="api/*"

Experimental viteModuleRunner: false

By default, Vitest runs all code inside Vite's module runner — a permissive sandbox that provides import.meta.env, require, __dirname, __filename, and applies Vite plugins and aliases. While this makes getting started easy, it can hide real issues: your tests may pass in the sandbox but fail in production because the runtime behavior differs from native Node.js.

Vitest 4.1 introduces experimental.viteModuleRunner, which lets you disable the module runner entirely and run tests with native import instead:

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

export default defineConfig({
  test: {
    experimental: {
      viteModuleRunner: false,
    },
  },
})

With this flag, no file transforms are applied — your test files, source code, and setup files are executed by Node.js directly. This means faster startup, closer-to-production behavior, and issues like incorrect __dirname injection or silently passing imports of nonexistent exports are caught early.

If you are using Node.js 22.18+ or 23.6+, TypeScript is stripped natively — no extra configuration needed.

Mocking with vi.mock and vi.hoisted is supported via the Node.js Module Loader API (requires Node.js 22.15+). Note that import.meta.env, Vite plugins, aliases, and the istanbul coverage provider are not available in this mode.

Consider this option if you run server-side or script-like tests that don't need Vite transforms. For jsdom/happy-dom tests, we still recommend the default module runner or browser mode.

Read more in the experimental.viteModuleRunner docs.

Configure UI Browser Window

Vitest 4.1 introduces browser.detailsPanelPosition, letting you choose where the details panel appears in Browser UI.

This is especially useful on smaller screens, where switching to a bottom panel leaves more horizontal space for your app:

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

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      detailsPanelPosition: 'bottom', // or 'right'
    },
  },
})

You can also switch this directly from the UI via the new layout toggle button.

Enhanced Browser Trace View

Vitest 4.1 brings major improvements to the Playwright Trace Viewer integration in browser mode. Browser interactions like click, fill, and expect.element are now automatically grouped in the trace timeline and linked back to the exact line in your test file.

Framework libraries are also integrating with the trace. For example, vitest-browser-react's render() utility now automatically appears in the trace with rendered element highlighted.

For custom annotations, the new page.mark and locator.mark APIs let you add your own markers to the trace:

ts
import { page } from 'vitest/browser'

await page.mark('before sign in')
await page.getByRole('button', { name: 'Sign in' }).click()
await page.mark('after sign in')

You can also group a whole flow under one named entry:

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()
})

Read more in the Trace View guide.

Type-Inference in test.extend - New Builder Pattern

Vitest 4.1 introduces a new test.extend pattern that supports type inference. You can return a value from the factory instead of calling the use function — TypeScript infers the type of each fixture from its return value, so you don't need to declare types manually.

ts
import { test as baseTest } from 'vitest'

export const test = baseTest
  // Simple value - type is inferred as { port: number; host: string }
  .extend('config', { port: 3000, host: 'localhost' })
  // Function fixture - type is inferred from return value
  .extend('server', async ({ config }) => {
    // TypeScript knows config is { port: number; host: string }
    return `http://${config.host}:${config.port}`
  })

For fixtures that need setup or cleanup logic, use a function. The onCleanup callback registers teardown logic that runs after the fixture's scope ends:

ts
import { test as baseTest } from 'vitest'

export const test = baseTest
  .extend('tempFile', async ({}, { onCleanup }) => {
    const filePath = `/tmp/test-${Date.now()}.txt`
    await fs.writeFile(filePath, 'test data')

    // Register cleanup - runs after test completes
    onCleanup(() => fs.unlink(filePath))

    return filePath
  })

In addition to this, Vitest now passes down file and worker contexts to beforeAll, afterAll and aroundAll hooks:

ts
import { test as baseTest } from 'vitest'

const test = baseTest
  .extend('config', { scope: 'file' }, () => loadConfig())
  .extend('db', { scope: 'file' }, ({ config }) => createDatabase(config.port))

test.beforeAll(async ({ db }) => {
  await db.migrateUsers()
})

test.afterAll(async ({ db }) => {
  await db.deleteUsers()
})

WARNING

This change could be considered breaking. Previously Vitest passed down undocumented Suite as the first argument. The team decided that the usage was small enough to not disrupt the ecosystem.

New aroundAll and aroundEach Hooks

The new aroundEach hook registers a callback function that wraps around each test within the current suite. The callback receives a runTest function that must be called to run the test. The aroundAll hook works similarly, but is called for every suite, not every test.

You should use aroundEach when your test needs to run inside a context that wraps around it, such as:

  • Wrapping tests in AsyncLocalStorage context
  • Wrapping tests with tracing spans
  • Database transactions
ts
import { test as baseTest } from 'vitest'

const test = baseTest
  .extend('db', async ({}, { onCleanup }) => {
    // db is created before `aroundEach` hook
    const db = await createTestDatabase()
    onCleanup(() => db.close())
    return db
  })

test.aroundEach(async (runTest, { db }) => {
  await db.transaction(runTest)
})

test('insert user', async ({ db }) => {
  // called inside a transaction
  await db.insert({ name: 'Alice' })
})

Helper for Better Stack Traces

When a test fails inside a shared utility function, the stack trace usually points to the line inside that helper — not where it was called. This makes it harder to find which test actually failed, especially when the same helper is used across many tests.

vi.defineHelper wraps a function so that Vitest removes its internals from the stack trace and points the error back to the call site instead:

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

const assertPair = vi.defineHelper((a, b) => {
  expect(a).toEqual(b) // 🙅‍♂️ error code block will NOT point to here
})

test('example', () => {
  assertPair('left', 'right') // 🙆 but point to here
})

This is especially useful for custom assertion libraries and reusable test utilities where the call site is more meaningful than the implementation.

--detect-async-leaks to Catch Leaks

Leaked timers, handles, and unresolved async resources can make test suites flaky and hard to debug. Vitest 4.1 adds detectAsyncLeaks to help track these issues.

You can enable it via CLI:

sh
vitest --detect-async-leaks

Or in config:

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

export default defineConfig({
  test: {
    detectAsyncLeaks: true,
  },
})

When enabled, Vitest uses node:async_hooks to report leaked async resources with source locations. Since this adds runtime overhead, it is best used while debugging.

vscode Improvements

The official vscode extension received a large number of fixes and new features:

  • The extension no longer keeps a running process in the background unless you explicitly enable continuous run manually or via a new config option watchOnStartup. This reduces memory usage and eliminates the maximumConfigs config option.
  • The new "Run Related Tests" command runs tests that import the currently open file.
  • The new "Toggle Continuous Run" action is now available when clicking on the gutter icon.
  • The extension now supports Deno runtime.
  • The extension cancels the test run sooner after clicking "Stop", when possible.
  • The extension displays the module load time inline next to each import statement, if you are using Vitest 4.1.

GitHub Actions Job Summary

The built-in github-actions reporter now automatically generates a Job Summary with an overview of your test results. The summary includes test file and test case statistics, and highlights flaky tests that required retries — with permalink URLs linking test names directly to the relevant source lines on GitHub.

The summary is enabled by default when running in GitHub Actions and writes to the path specified by $GITHUB_STEP_SUMMARY. No configuration is needed in most cases. To disable it or customize the output path:

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

export default defineConfig({
  test: {
    reporters: [
      ['github-actions', {
        jobSummary: {
          enabled: false, // or set `outputPath` to customize where the summary is written
        },
      }],
    ],
  },
})

New agent Reporter to Reduce Token Usage

As AI coding agents become a common way to run tests, Vitest 4.1 introduces the agent reporter — a minimal output mode designed to reduce token usage. It only displays failed tests and their errors, suppressing passed test output and console logs from passing tests.

Vitest automatically enables this reporter when it detects it's running inside an AI coding agent. The detection is powered by std-env, which recognizes popular agent environments out of the box. You can also set the AI_AGENT=copilot (or any name) environment variable explicitly. No configuration needed — just run Vitest as usual:

sh
AI_AGENT=copilot vitest

If you configure custom reporters, the automatic detection is skipped, so add 'agent' to the list manually if you want both.

New mockThrow API

Previously, making a mock throw required wrapping the error in a function: mockImplementation(() => { throw new Error(...) }). The new mockThrow and mockThrowOnce methods make this more concise and readable:

ts
const myMockFn = vi.fn()
myMockFn.mockThrow(new Error('error message'))
myMockFn() // throws Error<'error message'>

Strict Mode in WebdriverIO and Preview

Locating elements is now strict by default in webdriverio and preview, matching Playwright behavior.

If a locator resolves to multiple elements, Vitest throws a "strict mode violation" instead of silently picking one. This helps catch ambiguous queries early:

ts
const button = page.getByRole('button')

await button.click() // throws if multiple buttons match
await button.click({ strict: false }) // opt out and return first match

Chai-style Mocking Assertions

Vitest already supports chai-style assertions like eql, throw, and be. This release extends that support to mock assertions, making it easier to migrate from Sinon-based test suites without rewriting every expectation:

ts
import { expect, vi } from 'vitest'

const fn = vi.fn()

fn('example')

expect(fn).to.have.been.called // expect(fn).toHaveBeenCalled()
expect(fn).to.have.been.calledWith('example') // expect(fn).toHaveBeenCalledWith('example')
expect(fn).to.have.returned // expect(fn).toHaveReturned()
expect(fn).to.have.callCount(1) // expect(fn).toHaveBeenCalledTimes(1)

Coverage ignore start/stop Ignore Hints

You can now completely ignore specific lines from code coverage using ignore start/stop comments. In Vitest v3, this was supported by the v8 provider, but not in v4.0 due to underlying dependency changes.

Due to the community's request, we've now implemented it back ourselves and extended the support to both v8 and istanbul providers.

ts
/* istanbul ignore start -- @preserve */
if (parameter) { 
  console.log('Ignored') 
} 
else { 
  console.log('Ignored') 
} 
/* istanbul ignore stop -- @preserve */

console.log('Included')

/* v8 ignore start -- @preserve */
if (parameter) { 
  console.log('Ignored') 
} 
else { 
  console.log('Ignored') 
} 
/* v8 ignore stop -- @preserve */

console.log('Included')

See Coverage | Ignoring Code for more examples.

Coverage For Changed Files Only

If you want to get code coverage only for the modified files, you can use coverage.changed to limit the file inclusion.

Compared to the regular --changed flag, --coverage.changed allows you to still run all test files, but limit the coverage reporting only to the changed files. This allows you to exclude unchanged files from coverage that --changed would otherwise include.

Coverage in HTML Reporter and Subpath Deployments

Coverage HTML viewing now works reliably across UI mode, HTML reporter, and browser mode — including when deployed under a subpath. For custom coverage reporters, the new coverage.htmlDir option can be used to integrate their HTML output.

Acknowledgments

Vitest 4.1 is the result of countless hours by the Vitest team and our contributors. We appreciate the individuals and companies sponsoring Vitest development. Vladimir and Hiroshi are part of the VoidZero Team and are able to work on Vite and Vitest full-time, and Ari can invest more time in Vitest thanks to support from Chromatic. A big shout-out to Zammad, and sponsors on Vitest's GitHub Sponsors and Vitest's Open Collective.