---
title: "Vitest 4: Browser Mode is stable, visual regression and Playwright traces · vuejs.school"
description: "Vitest 4.0 makes Browser Mode stable, adds screenshot assertions and Playwright traces, and drops Node 18 and Vite 5. Here is what changes for Vue tests."
canonical_url: "https://vuejs.school/community/blog/vitest-4-browser-mode-is-stable"
meta:
  "og:description": "Vitest 4.0 makes Browser Mode stable, adds screenshot assertions and Playwright traces, and drops Node 18 and Vite 5. Here is what changes for Vue tests."
  "og:image": "https://vuejs.school/_og/d/a_~Vml0ZXN0IDQ6IEJyb3dzZXIgTW9kZSBpcyBzdGFibGUsIHZpc3VhbCByZWdyZXNzaW9uIGFuZCBQbGF5d3JpZ2h0IHRyYWNlcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Vml0ZXN0IDQ6IEJyb3dzZXIgTW9kZSBpcyBzdGFibGUsIHZpc3VhbCByZWdyZXNzaW9uIGFuZCBQbGF5d3JpZ2h0IHRyYWNlcw%2Cdescription_~Vml0ZXN0IDQuMCBtYWtlcyBCcm93c2VyIE1vZGUgc3RhYmxlLCBhZGRzIHNjcmVlbnNob3QgYXNzZXJ0aW9ucyBhbmQgUGxheXdyaWdodCB0cmFjZXMsIGFuZCBkcm9wcyBOb2RlIDE4IGFuZCBWaXRlIDUuIEhlcmUgaXMgd2hhdCBjaGFuZ2VzIGZvciBWdWUgdGVzdHMu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA4IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92aXRlc3QtNC1icm93c2VyLW1vZGUtaXMtc3RhYmxlIg%2Cs_r-3ldB5gkBIZiBYG.png"
  "og:image:alt": "Vitest 4: Browser Mode is stable, visual regression and Playwright traces · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Vitest 4: Browser Mode is stable, visual regression and Playwright traces · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~Vml0ZXN0IDQ6IEJyb3dzZXIgTW9kZSBpcyBzdGFibGUsIHZpc3VhbCByZWdyZXNzaW9uIGFuZCBQbGF5d3JpZ2h0IHRyYWNlcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Vml0ZXN0IDQ6IEJyb3dzZXIgTW9kZSBpcyBzdGFibGUsIHZpc3VhbCByZWdyZXNzaW9uIGFuZCBQbGF5d3JpZ2h0IHRyYWNlcw%2Cdescription_~Vml0ZXN0IDQuMCBtYWtlcyBCcm93c2VyIE1vZGUgc3RhYmxlLCBhZGRzIHNjcmVlbnNob3QgYXNzZXJ0aW9ucyBhbmQgUGxheXdyaWdodCB0cmFjZXMsIGFuZCBkcm9wcyBOb2RlIDE4IGFuZCBWaXRlIDUuIEhlcmUgaXMgd2hhdCBjaGFuZ2VzIGZvciBWdWUgdGVzdHMu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA4IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92aXRlc3QtNC1icm93c2VyLW1vZGUtaXMtc3RhYmxlIg%2Cs_r-3ldB5gkBIZiBYG.png"
  "twitter:image:alt": "Vitest 4: Browser Mode is stable, visual regression and Playwright traces · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## What shipped

- Browser Mode is stable. The providers moved into their own packages: `@vitest/browser-playwright`, `@vitest/browser-webdriverio` and `@vitest/browser-preview`. Each one brings `@vitest/browser` with it, so that package can leave your `devDependencies`. The browser context is imported from `vitest/browser` now, not `@vitest/browser/context`; the old path still works until the next major.
- Visual regression testing. A new `toMatchScreenshot` assertion photographs an element and compares it with a reference image stored next to the test. A `toBeInViewport` matcher arrives with it.
- Playwright traces. Set `trace` under `test.browser`, or pass `--browser.trace=on`, and Vitest records a trace you can open in the Playwright trace viewer.
- `page.frameLocator` (Playwright only) finds elements inside an iframe, and every locator has a `length` property, so `toHaveLength` works on a locator.
- `expect.schemaMatching` validates a value against any Standard Schema object, such as a Zod schema, and `expect.assert` exposes Chai's `assert` for narrowing types.
- Reporters: the `basic` reporter is gone, `default` prints a tree only when a single file runs, a new `tree` reporter always does, and `verbose` prints each test as it finishes rather than only in CI.
- `beforeEach` and `afterEach` can be called on the object returned by `test.extend`, so they see the extended context types.

## Why test a component in a real browser

Most Vue component tests run in Node with happy-dom or jsdom, two libraries that imitate a browser's document model. They are fast and they are enough for "renders the list, emits on click". They do not do layout. An element has no width, `scrollIntoView` does nothing, focus and CSS behave approximately, and a library that measures the DOM (a virtual scroller, a chart, a date picker) often fails in ways the real thing never would.

Browser Mode starts your Vite dev server, opens a real browser through Playwright and runs the test file inside it. The component renders with its real CSS, measures real sizes and receives real clicks. The price is speed: a browser test takes longer to start than a Node test. The split that works is to keep composable and store tests in Node and move the component tests that depend on layout, focus, scrolling or third-party DOM code into the browser.

## A Vue component test in Browser Mode

Install the runner, the Playwright provider, the Vue renderer and Playwright itself. `@vitest/browser-playwright` lists `playwright` as a peer dependency, which is why it is on the line, and the second command downloads Chromium. The config also uses `@vitejs/plugin-vue`, which a Vite Vue project already has.

```
npm install -D vitest@4 @vitest/browser-playwright@4 vitest-browser-vue@2 playwright
npx playwright install chromium
```

The configuration names a provider and at least one browser instance:

*vitest.config.ts*

```
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { playwright } from '@vitest/browser-playwright'

export default defineConfig({
  plugins: [vue()],
  test: {
    setupFiles: ['vitest-browser-vue'],
    browser: {
      enabled: true,
      provider: playwright(),
      headless: true,
      instances: [{ browser: 'chromium' }],
    },
  },
})
```

`provider: playwright()` is a function call, not a string; that is one of the breaking changes below. `instances` lists the browsers to run in, one entry each. `headless: true` keeps the browser window closed, which you want in CI; drop it locally to watch the tests run.

The `setupFiles` entry loads `vitest-browser-vue` before every test file. It adds `page.render`, and it unmounts the previous component before each test starts rather than after it ends, so the last render stays on screen for you to look at.

Here is a component and its test. The test file ends in `.browser.test.ts`, the pattern that split matches:

*src/components/Counter.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const props = defineProps<{ initialCount?: number }>()
const count = ref(props.initialCount ?? 0)
</script>

<template>
  <p>Count is {{ count }}</p>
  <button type="button" @click="count++">Increment</button>
</template>
```

*src/components/Counter.browser.test.ts*

```
import { expect, test } from 'vitest'
import { render } from 'vitest-browser-vue'
import Counter from './Counter.vue'

test('increments when the button is clicked', async () => {
  const screen = render(Counter, { props: { initialCount: 1 } })

  await screen.getByRole('button', { name: 'Increment' }).click()

  await expect.element(screen.getByText('Count is 2')).toBeVisible()
})
```

`render` mounts the component and returns a `screen` with locators: `getByRole`, `getByText`, `getByLabelText` and the rest. It is synchronous in 2.0. The click is a real click, so it is awaited. `expect.element` retries the assertion until it passes or times out, which removes every `await nextTick()` you would write in a Node test. `vitest-browser-vue` 2.0.0 was published on the same day as Vitest 4 and requires it; underneath it uses Vue Test Utils, and its `config.global` export is where you set global plugins or mocks such as a `$t` function.

## Compare screenshots and record traces

A screenshot assertion takes a locator and a name. This test assumes a `Card` component whose root element is an `<article>`:

*src/components/Card.browser.test.ts*

```
import { expect, test } from 'vitest'
import { render } from 'vitest-browser-vue'
import Card from './Card.vue'

test('matches the reference image', async () => {
  const screen = render(Card, { props: { title: 'Hello' } })

  await expect(screen.getByRole('article')).toMatchScreenshot('card-default')
})
```

Reference images live in a `__screenshots__` folder beside the test file, one subfolder per test file, with the browser and platform in the file name. Commit them. The first run has nothing to compare with: Vitest saves the reference and fails the test, so check the image and run again. When a later comparison fails, the actual image and a diff are written to `.vitest-attachments`, and `vitest --update` replaces the references once you have checked the change is intended.

Vitest sets no default for how many pixels may differ. The [visual regression guide](https://v4.vitest.dev/guide/browser/visual-regression-testing) sets 1% in its example under `test.browser.expect.toMatchScreenshot`: `comparatorName: 'pixelmatch'` with `comparatorOptions: { allowedMismatchedPixelRatio: 0.01 }`. It also strongly recommends generating and comparing screenshots in the same environment, such as a Docker container in CI, because fonts and GPUs differ between machines.

Traces are a Playwright-only feature. Add `trace: 'retain-on-failure'` under `test.browser` (the other values are `on`, `off`, `on-first-retry` and `on-all-retries`), and a failed test leaves a `.trace.zip` in a `__traces__` folder next to the test. Open it with `npx playwright show-trace <file>` to step through every action with a DOM snapshot, network log and console output at each step. For a flaky browser test this replaces guessing.

## Breaking changes for an existing suite

The provider option changed shape. A string no longer works:

*vitest.config.ts**Breaks***

```
export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: 'playwright',
      instances: [{ browser: 'chromium' }],
    },
  },
})
```

*vitest.config.ts**Works***

```
import { playwright } from '@vitest/browser-playwright'

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: playwright(),
      instances: [{ browser: 'chromium' }],
    },
  },
})
```

The factory takes options, for example `playwright({ launchOptions: { slowMo: 100 } })`, and the `preview` provider is no longer the default, so a config without a provider must now name one.

Outside Browser Mode, these are the changes most likely to touch a Vue project. In the config file:

- The `workspace` option is removed in favour of `projects`, and a `projects` entry can no longer point to a file that exports an array. A `test.workspace` option stops the run with an error, but a `vitest.workspace.ts` that Vitest 3 found on its own is now skipped without a warning, and every test runs under the root config. Move its contents into `test.projects` in `vitest.config.ts`.
- The default `exclude` only skips `node_modules` and `.git`. If you kept tests out of `dist` or `cypress` by relying on the old defaults, add `test.dir` or your own `exclude`.
- V8 coverage uses AST-based remapping, and the guide says to expect the numbers to move. `coverage.all` and `coverage.extensions` are removed: the report includes only files loaded during the run unless you set `coverage.include`, so set it to `['src/**']`.
- `maxThreads` and `maxForks` become `maxWorkers`, `poolOptions` is removed and its options move to the top level, and `singleThread: true` becomes `maxWorkers: 1, isolate: false`.

In test code:

- Passing options as the third argument, `it('saves', fn, { timeout: 10_000 })`, was a warning in Vitest 3 and is an error now. Put the object second. A plain number still works as the last argument.
- `vi.restoreAllMocks()`, and the `restoreMocks: true` config option, now only put back the originals of spies created with `vi.spyOn`. They no longer clear any mock's call history or reset `vi.fn()` implementations. If you relied on that, add `clearMocks: true` (history) or `mockReset: true` (history and implementations) to the config.
- A mock used as a constructor must be implemented with `function` or `class`. With an arrow function, `new` throws a `TypeError` that ends in `is not a constructor`, after a warning that the mock did not use `function` or `class`.
- `vi.fn().getMockName()` returns `vi.fn()`, so a snapshot that contained `[MockFunction spy]` now prints `[MockFunction]`.

## How to upgrade

Get the suite green on Vitest 3 first, so that every failure afterwards belongs to the upgrade, then install Vitest 4 and its coverage package:

```
npx vitest --run
npm install -D vitest@4 @vitest/coverage-v8@4
```

Then:

1. Add `@vitest/ui@4` if you use the UI.
2. For Browser Mode, uninstall `@vitest/browser`, install `@vitest/browser-playwright@4`, and change the provider to the factory.
3. If you have a `vitest.workspace.ts`, move it into `test.projects` now.
4. Run the suite and fix configuration errors before test failures. The provider string, a `test.workspace` option and third-argument options all stop with a clear message.
5. Compare the coverage report with the one from Vitest 3 and adjust thresholds rather than chasing the old figures.

Upgrade now if you are on Vite 6 or 7 and Node 20 or newer: the breaking changes are listed above, and `expect.element` in a real browser is a better way to test anything that touches layout. If you are still on Vite 5, wait and upgrade Vite first.

**Check it stuck**3 questions from the Testing Library and browser mode quiz.

[**Practice now**](https://vuejs.school/learn/testing/testing-library-and-browser-mode?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 6 min read**Vitest 5: what changed for Vue tests**](https://vuejs.school/community/blog/vitest-5-what-changed-for-vue-tests) [Release notes · 6 min read**Vitest 5 release candidate: what's coming for Vue tests**](https://vuejs.school/community/blog/vitest-5-rc-whats-coming)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/vitest-4-browser-mode-is-stable#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-10-23","description":"Vitest 4.0 makes Browser Mode stable, adds screenshot assertions and Playwright traces, and drops Node 18 and Vite 5. Here is what changes for Vue tests.","name":"Vitest 4: Browser Mode is stable, visual regression and Playwright traces","url":"https://vuejs.school/community/blog/vitest-4-browser-mode-is-stable","breadcrumb":{"@id":"https://vuejs.school/community/blog/vitest-4-browser-mode-is-stable#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/vitest-4-browser-mode-is-stable"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/vitest-4-browser-mode-is-stable#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-10-23","description":"Vitest 4.0 makes Browser Mode stable, adds screenshot assertions and Playwright traces, and drops Node 18 and Vite 5. Here is what changes for Vue tests.","headline":"Vitest 4: Browser Mode is stable, visual regression and Playwright traces","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~Vml0ZXN0IDQ6IEJyb3dzZXIgTW9kZSBpcyBzdGFibGUsIHZpc3VhbCByZWdyZXNzaW9uIGFuZCBQbGF5d3JpZ2h0IHRyYWNlcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Vml0ZXN0IDQ6IEJyb3dzZXIgTW9kZSBpcyBzdGFibGUsIHZpc3VhbCByZWdyZXNzaW9uIGFuZCBQbGF5d3JpZ2h0IHRyYWNlcw%2Cdescription_~Vml0ZXN0IDQuMCBtYWtlcyBCcm93c2VyIE1vZGUgc3RhYmxlLCBhZGRzIHNjcmVlbnNob3QgYXNzZXJ0aW9ucyBhbmQgUGxheXdyaWdodCB0cmFjZXMsIGFuZCBkcm9wcyBOb2RlIDE4IGFuZCBWaXRlIDUuIEhlcmUgaXMgd2hhdCBjaGFuZ2VzIGZvciBWdWUgdGVzdHMu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA4IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92aXRlc3QtNC1icm93c2VyLW1vZGUtaXMtc3RhYmxlIg%2Cs_r-3ldB5gkBIZiBYG.png","@type":["Article","BlogPosting"],"articleSection":["Release notes"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/vitest-4-browser-mode-is-stable#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/vitest-4-browser-mode-is-stable#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/vitest-4-browser-mode-is-stable#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Vitest 4: Browser Mode is stable, visual regression and Playwright traces","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~Vml0ZXN0IDQ6IEJyb3dzZXIgTW9kZSBpcyBzdGFibGUsIHZpc3VhbCByZWdyZXNzaW9uIGFuZCBQbGF5d3JpZ2h0IHRyYWNlcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Vml0ZXN0IDQ6IEJyb3dzZXIgTW9kZSBpcyBzdGFibGUsIHZpc3VhbCByZWdyZXNzaW9uIGFuZCBQbGF5d3JpZ2h0IHRyYWNlcw%2Cdescription_~Vml0ZXN0IDQuMCBtYWtlcyBCcm93c2VyIE1vZGUgc3RhYmxlLCBhZGRzIHNjcmVlbnNob3QgYXNzZXJ0aW9ucyBhbmQgUGxheXdyaWdodCB0cmFjZXMsIGFuZCBkcm9wcyBOb2RlIDE4IGFuZCBWaXRlIDUuIEhlcmUgaXMgd2hhdCBjaGFuZ2VzIGZvciBWdWUgdGVzdHMu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA4IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92aXRlc3QtNC1icm93c2VyLW1vZGUtaXMtc3RhYmxlIg%2Cs_r-3ldB5gkBIZiBYG.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~Vml0ZXN0IDQ6IEJyb3dzZXIgTW9kZSBpcyBzdGFibGUsIHZpc3VhbCByZWdyZXNzaW9uIGFuZCBQbGF5d3JpZ2h0IHRyYWNlcyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Vml0ZXN0IDQ6IEJyb3dzZXIgTW9kZSBpcyBzdGFibGUsIHZpc3VhbCByZWdyZXNzaW9uIGFuZCBQbGF5d3JpZ2h0IHRyYWNlcw%2Cdescription_~Vml0ZXN0IDQuMCBtYWtlcyBCcm93c2VyIE1vZGUgc3RhYmxlLCBhZGRzIHNjcmVlbnNob3QgYXNzZXJ0aW9ucyBhbmQgUGxheXdyaWdodCB0cmFjZXMsIGFuZCBkcm9wcyBOb2RlIDE4IGFuZCBWaXRlIDUuIEhlcmUgaXMgd2hhdCBjaGFuZ2VzIGZvciBWdWUgdGVzdHMu%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA4IG1pbiByZWFk%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy92aXRlc3QtNC1icm93c2VyLW1vZGUtaXMtc3RhYmxlIg%2Cs_r-3ldB5gkBIZiBYG.png"}]}
```