Vitest 4.0 is out!
October 22, 2025
The next Vitest major is here
Today, we are thrilled to announce Vitest 4!
Quick links:
- Docs
- Translations: 简体中文
- Migration Guide
- GitHub Changelog
If you've not used Vitest before, we suggest reading the Getting Started and Features guides first.
We extend our gratitude to the over 640 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.
Browser Mode is Stable
With this release we are removing the experimental
tag from Browser Mode. To make it possible, we had to introduce some changes to the public API.
To define a provider, you now need to install a separate package: @vitest/browser-playwright
, @vitest/browser-webdriverio
, or @vitest/browser-preview
. This makes it simpler to work with custom options and doesn't require adding /// <reference
comments anymore.
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
/// <reference path="@vitest/browser/providers/playwright" />
export default defineConfig({
test: {
browser: {
provider: 'playwright',
provider: playwright({
launchOptions: {
slowMo: 100,
},
}),
instances: [
{
browser: 'chromium',
launch: {
slowMo: 100,
},
},
],
},
},
})
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-webdriverio'
/// <reference path="@vitest/browser/providers/webdriverio" />
export default defineConfig({
test: {
browser: {
provider: 'webdriverio',
provider: webdriverio({
capabilities: {
browserVersion: '82',
},
}),
instances: [
{
browser: 'chrome',
capabilities: {
browserVersion: '82',
},
},
],
},
},
})
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-preview'
export default defineConfig({
test: {
browser: {
provider: 'preview',
provider: preview(),
instances: [
{ browser: 'chrome' },
],
},
},
})
The context is no longer imported from @vitest/browser/context
(but it will keep working until the next major version for better compatibility with tools that did not update yet), now just import from vitest/browser
:
import { page } from '@vitest/browser/context'
import { page } from 'vitest/browser'
test('example', async () => {
await page.getByRole('button').click()
})
With these changes, the @vitest/browser
package can be removed from your dependencies. It is now included in every provider package automatically.
Visual Regression Testing
Vitest 4 adds support for Visual Regression testing in Browser Mode. We will continue to iterate on this feature to improve the experience.
Visual regression testing in Vitest can be done through the toMatchScreenshot
assertion:
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
test('hero section looks correct', async () => {
// ...the rest of the test
// capture and compare screenshot
await expect(page.getByTestId('hero')).toMatchScreenshot('hero-section')
})
Vitest captures screenshots of your UI components and pages, then compares them against reference images to detect unintended visual changes.
Alongside this feature, Vitest also introduces a toBeInViewport
matcher. It allows you to check if an element is currently in viewport with IntersectionObserver API.
// A specific element is in viewport.
await expect.element(page.getByText('Welcome')).toBeInViewport()
// 50% of a specific element should be in viewport
await expect.element(page.getByText('To')).toBeInViewport({ ratio: 0.5 })
Playwright Traces Support
Vitest 4 supports generating Playwright Traces. To enable tracing, you need to set the trace
option in the test.browser
configuration or pass down --browser.trace=on
option (off
, on-first-retry
, on-all-retries
, retain-on-failure
are also available).
The traces are available in reporters as annotations. For example, in the HTML reporter, you can find the link to the trace file in the test details. To open the trace file, you can use the Playwright Trace Viewer.
Locator Improvements
The frameLocator
method returns a FrameLocator
instance that can be used to find elements inside the iframe. Vitest now supports a new page.frameLocator
API (only with playwright
provider).
const frame = page.frameLocator(
page.getByTestId('iframe')
)
await frame.getByText('Hello World').click() // ✅
await frame.click() // ❌ Not available
Every locator now exposes a length
property, allowing them to be used with toHaveLength
matcher automatically:
await expect.element(page.getByText('Item')).toHaveLength(3)
Improved Debugging
The vscode extension now supports "Debug Test" button when running browser tests.
If you prefer configuring the debug options yourself, you can start Vitest with the --inspect
flag (available with playwright
and webdriverio
) and connect to DevTools manually. In this case Vitest will also disable the new trackUnhandledErrors
option automatically.
Type-Aware Hooks
When using test.extend
with lifecycle hooks like beforeEach
and afterEach
, you can now reference them directly on the returned test
object:
import { test as baseTest } from 'vitest'
const test = baseTest.extend<{
todos: number[]
}>({
todos: async ({}, use) => {
await use([])
},
})
// Unlike global hooks, these hooks are aware of the extended context
test.beforeEach(({ todos }) => {
todos.push(1)
})
test.afterEach(({ todos }) => {
console.log(todos)
})
expect.assert
Vitest has always exported Chai's assert
, but sometimes using it was inconvenient because many modules have the same export.
Now Vitest exposes the same method on expect
for an easy access. This is especially useful if you need to narrow down the type, since expect.to*
methods do not support that:
interface Cat {
__type: 'Cat'
mew(): void
}
interface Dog {
__type: 'Dog'
bark(): void
}
type Animal = Cat | Dog
const animal: Animal = { __type: 'Dog', bark: () => {} }
expect.assert(animal.__type === 'Dog')
// does not show a type error!
expect(animal.bark()).toBeUndefined()
expect.schemaMatching
Vitest 4 introduces a new asymmetric matcher called expect.schemaMatching
. It accepts a Standard Schema v1 object and validates values against it, passing the assertion when the value conforms to the schema.
As a reminder, asymmetric matchers can be used in all expect
matchers that check equality, including toEqual
, toStrictEqual
, toMatchObject
, toContainEqual
, toThrowError
, toHaveBeenCalledWith
, toHaveReturnedWith
and toHaveBeenResolvedWith
.
import { expect, test } from 'vitest'
import { z } from 'zod'
import * as v from 'valibot'
import { type } from 'arktype'
test('email validation', () => {
const user = { email: 'john@example.com' }
// using Zod
expect(user).toEqual({
email: expect.schemaMatching(z.string().email()),
})
// using Valibot
expect(user).toEqual({
email: expect.schemaMatching(v.pipe(v.string(), v.email()))
})
// using ArkType
expect(user).toEqual({
email: expect.schemaMatching(type('string.email')),
})
})
Reporter Updates
The basic
reporter was removed. You can use the default
reporter with summary: false
instead:
export default defineConfig({
test: {
reporters: [
['default', { summary: false }],
],
},
})
The default
reporter now always prints tests in a tree if there is only one test file running. If you want to always see tests printed as a tree, you can use a new tree
reporter.
The verbose
reporter now always prints tests one by one when they are finished. Previously, this was done only in CI, and locally verbose
would behave mostly like a default
reporter. If you prefer to keep the old behaviour, you can conditionally use the verbose
reporter only in CI by updating the config:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
reporter: process.env.CI ? 'verbose' : 'default',
},
})
New API Methods
Vitest 4 comes with new advanced public API methods:
experimental_parseSpecifications
allows you to parse a test file without running it.watcher
exposes methods that can be used when you disable the default Vitest watcher.enableCoverage
anddisableCoverage
allow you to enable and disable coverage dynamically.getSeed
returns the seed value, if tests run at random.getGlobalTestNamePattern
returns the current test name pattern.waitForTestRunEnd
returns a promise that resolves when all tests have finished running.
Breaking changes
Vitest 4 has a few 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 4 Changelog.
Acknowledgments
Vitest 4 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 StackBlitz. A shout-out to NuxtLabs, Zammad, and sponsors on Vitest's GitHub Sponsors and Vitest's Open Collective.