Switch Language
Toggle Theme

Vitest Component Testing in Practice: Browser Mode and Playwright Integration

Honestly, I hit a major roadblock the first time I tested a Canvas component.

It was 2 AM. Staring at the green “PASS” on my test report, I confidently pushed my code. The next day, my colleague opened the page in a real browser—Canvas wasn’t rendering at all. But the tests passed?!

Later I realized: jsdom is just a “fake” browser. It simulates DOM APIs, but Canvas rendering behavior, computed CSS styles, Web Components lifecycle—none of these work properly. After six months of unit testing, I’d only been scratching the surface.

That’s why Vitest introduced Browser Mode in version 3.0—running tests directly in real browsers. Unlike jsdom simulating DOM in Node.js, Browser Mode launches Chromium/Firefox/Safari, renders your components for real, and uses Playwright’s API for interactions. Tests pass? Then they really pass.

In this article, I’ll walk you through configuring Browser Mode from scratch, testing React/Vue components, and setting up CI coverage gates. This is the third article in the Vitest testing guide series—previous articles covered unit test configuration and TDD workflows. Now we’ll complete the component testing piece.

Why Do You Need Browser Mode?

You might wonder: isn’t jsdom enough?

Honestly, for pure logic components—like a calculator or form validator—jsdom is perfectly fine. It’s a DOM simulator in Node.js, blazing fast and simple to configure. The unit tests in my previous articles all ran in jsdom, testing reactive data and event triggers without issues.

But jsdom falls short in these scenarios:

  • Canvas Drawing: jsdom has Canvas APIs, but doesn’t actually render anything. You can test that ctx.fillRect() was called, but can you test if the drawn shape is correct? Nope.
  • Computed CSS Styles: getComputedStyle() returns empty objects in jsdom. In real browsers, element width is affected by parent containers, padding, borders—jsdom can’t calculate any of that.
  • Web Components: Custom elements’ connectedCallback and disconnectedCallback exist in jsdom, but lifecycle timing differs from real browsers.
  • Async Rendering: Animation frames, requestIdleCallback, IntersectionObserver—these APIs are either missing or incompletely implemented in jsdom.

Here’s a trap I fell into. Last year, a project had a component using CSS animations for button expand/collapse. In jsdom tests, transitionend events never fired—because there’s no real transition. Tests mocked the event, but then the animation duration changed in the real browser. Tests still “passed,” but the component was broken.

Browser Mode solves these problems. It renders components in real browsers. You write test code, it launches Chromium (or Firefox/Safari), mounts the component to the page, and uses Playwright’s API to click, type, and wait. What happens in the real browser is what gets tested.

Here’s a notable stat: Vitest 3.0’s Browser Mode shares Chromium contexts, launching the browser only once with all tests sharing the same instance. The official docs say it’s 30% faster than traditional Playwright E2E. Testing 50 components? No waiting for browser restarts.

There’s also debate about the testing pyramid for component testing. The traditional pyramid: unit tests at the base, E2E at the top. But Vue’s official blog has a counter-argument—alexop.dev’s inverted testing pyramid: 70% integration tests, 20% unit tests, 10% E2E. The reasoning: components themselves are integration units, combining templates, styles, and logic. jsdom unit tests only cover logic; integration tests verify overall behavior. Browser Mode fills this gap—more realistic than jsdom, lighter than Playwright E2E.

Browser Mode Configuration in Practice

Configuring Browser Mode isn’t actually that complex. But there are a few gotchas I’ve hit—let me share them upfront.

Installing Dependencies

First, install Vitest and the Browser Mode provider. Playwright is the official recommendation:

npm install -D vitest @vitest/browser-playwright

Playwright installs Chromium, Firefox, and WebKit browsers. If you only want Chromium (sufficient for most cases), specify during installation:

npx playwright install chromium

This step is a bit slow—Chromium is about 170MB. Once downloaded, you’re basically ready.

vitest.config.ts Configuration

The config file is simple, but watch for one detail:

import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'

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

The instances configuration determines which browser runs your tests. For cross-browser compatibility testing, add Firefox and WebKit:

instances: [
  { browser: 'chromium' },
  { browser: 'firefox' },
  { browser: 'webkit' },  // Safari
]

I usually stick with Chromium—multi-browser testing is slow, and most frontend bugs are caught in Chromium. Safari compatibility issues, I cover with Playwright E2E on critical paths.

Headless Mode vs UI Mode

This choice is subtle.

  • Headless Mode: Browser runs without a window, tests execute in the background. Good for CI environments, fast, but you can’t see the rendering process.
  • UI Mode: Browser opens a window, you can see components render, get clicked, accept input. Great for debugging during development, very useful when writing tests.

I use UI Mode during development, running:

npx vitest --browser.ui

Vitest opens a test workbench—test list on the left, browser window on the right. Click a test file, the browser renders the component, and you can watch the test code execute. Button not responding? Debug directly in the browser.

For CI environments, use Headless Mode with one config line:

browser: {
  provider: playwright(),
  enabled: true,
  headless: true,  // Force headless in CI
  instances: [{ browser: 'chromium' }],
}

Test File Naming Convention

The official recommendation is using .browser.test.ts for Browser Mode test files, distinguishing them from regular .test.ts files. Benefits I’ve found:

  • You can run jsdom unit tests and Browser Mode component tests simultaneously without mixing them.
  • When CI mysteriously fails, the filename tells you it’s a browser test—clear troubleshooting direction.

Vitest doesn’t enforce this naming. .test.ts works too. The key is specifying which directory contains Browser Mode tests in your config, or just run all tests in Browser Mode. I prefer separating them—jsdom for unit tests, Browser Mode for component tests.

React/Vue Component Testing in Practice

This is the core use case for Browser Mode. Honestly, the syntax differs a bit from traditional Testing Library, but it’s intuitive once you get used to it.

React Component Testing

First, install the React adapter:

npm install -D @vitest/browser-react

Then write tests. Say you have a Counter component where clicking a button increments the count:

// Counter.browser.test.ts
import { page } from '@vitest/browser/context'
import { userEvent } from '@vitest/browser/context'
import Counter from './Counter'

test('button click increments count', async () => {
  // Render component to browser
  await page.mount(<Counter />)

  // Find button element
  const button = page.getByRole('button', { name: 'Count: 0' })

  // Click button
  await userEvent.click(button)

  // Verify text change
  await expect.element(button).toHaveTextContent('Count: 1')
})

Compare with Testing Library syntax: Testing Library uses render(), Browser Mode uses page.mount(). Testing Library uses screen.getByRole(), Browser Mode uses page.getByRole(). The API is similar, just that page is Vitest Browser Mode’s context object.

One detail: await expect.element(button). This is Vitest’s Web Testing API—it automatically waits for element state changes. No manual await waitFor() needed; the test framework handles async waiting for you. Cleaner than Testing Library.

Vue Component Testing

Vue syntax is similar, but needs the Vue adapter:

npm install -D @vitest/browser-vue

Testing a Vue Counter component:

// Counter.browser.test.ts
import { page } from '@vitest/browser/context'
import { userEvent } from '@vitest/browser/context'
import Counter from './Counter.vue'

test('button click increments count', async () => {
  // Render Vue component
  await page.mount(Counter)

  // Find button, click, verify
  const button = page.getByRole('button', { name: 'Count: 0' })
  await userEvent.click(button)
  await expect.element(button).toHaveTextContent('Count: 1')
})

Vue 2 uses @vue/test-utils syntax, which Browser Mode doesn’t directly support. But for Vue 3 projects, @vitest/browser-vue is sufficient.

A Real-World Example

In my project, I had a drag-and-drop sorting component using the HTML5 Drag & Drop API. In jsdom, dragstart and drop events can’t be simulated—you have to mock them, testing only “event was fired,” but is the sorting logic correct? Unknown.

With Browser Mode, the test is straightforward:

test('drag and drop sorting', async () => {
  await page.mount(<SortableList items={['A', 'B', 'C']} />)

  const itemA = page.getByText('A')
  const itemC = page.getByText('C')

  // Drag A after C
  await userEvent.dragTo(itemA, itemC)

  // Verify order change
  const items = page.getByRole('listitem')
  await expect.element(items.nth(2)).toHaveTextContent('A')
})

In a real browser, Drag & Drop API actually fires, sorting logic actually executes, element order actually changes. Test passed? Then it really works.

That’s the value of Browser Mode—it tests real behavior, not simulated behavior.

Playwright vs Browser Mode Selection Guide

You might be confused: Browser Mode and Playwright, aren’t they both browser testing? What’s the difference?

Simply put: Browser Mode tests components, Playwright tests flows.

Core Differences

FeatureBrowser ModePlaywright
Test ScopeSingle component isolationMulti-page flow testing
Execution Speed~200ms/test2-5s/test
Startup CostShared browser instanceIndependent startup per test
Configuration ComplexityLow, integrated in VitestHigh, separate project config
Use CasesRapid iteration during developmentCritical path verification before release

Browser Mode is positioned for component testing—it mounts components to the browser but tests only that component’s behavior. Playwright is for E2E—it opens your complete application, navigating, logging in, submitting forms, testing entire flows.

An analogy: Browser Mode is like a surgeon using a microscope to examine individual cells. Playwright is like a doctor doing a full body scan. Each has its purpose; neither replaces the other.

Combined Strategy

Here’s how I use them in real projects:

  • Browser Mode: Test all UI components—buttons, forms, cards, modals. Write during development, commit with code. Tests are fast, feedback is instant.
  • Playwright E2E: Test 3-5 critical paths—view homepage after login, view results after search, view order after submission. Run only before release or during daily CI builds.

Benefits of this combination:

  1. Browser Mode catches most UI bugs during development.
  2. Playwright catches cross-page integration bugs before release.
  3. Maintenance is manageable—50 component tests, 5 E2E tests, not so many tests that CI slows down.

When to Choose Which?

Simple criteria:

  • Choose Browser Mode: You’re testing individual component UI behavior—clicks, inputs, rendering, styles. When component code changes, tests need instant feedback.
  • Choose Playwright: You’re testing multi-page flows—login -> navigate -> operate -> verify. Or testing cross-system integration—frontend + backend API + database.

Example: Testing a date picker component—date range limits, disabled dates, format display—use Browser Mode. Testing user selecting a date on a booking page, submitting an order, navigating to payment page—use Playwright.

One more point: Browser Mode only tests frontend; Playwright can test full-stack. If your application has backend APIs, Playwright E2E can test frontend + backend integration. Browser Mode only tests frontend components; backend must be mocked.

CI Environment Coverage Gates

Coverage configuration is the final piece in CI environments. Honestly, I didn’t prioritize this either—until a code refactor dropped coverage from 80% to 60%, causing a production bug. Only then did I realize the importance of coverage gates.

Coverage Configuration

Add coverage thresholds to vitest.config.ts:

test: {
  coverage: {
    provider: 'v8',  // or 'istanbul'
    reporter: ['text', 'json', 'html'],
    thresholds: {
      lines: 80,
      functions: 80,
      branches: 75,
      statements: 80
    }
  }
}

What threshold should you set? My experience:

  • New Projects: Start at 50%, gradually increase. Setting it too high initially creates pressure.
  • Mature Projects: 80% is reasonable. Core modules can be higher—90% or 95%.
  • Don’t Chase 100%: Edge branches, exception handling—some scenarios can’t be tested. Forcing tests wastes time.

Run coverage tests:

npx vitest run --coverage

Coverage below threshold? Vitest reports an error, CI build fails. That’s the gate—code below threshold can’t merge to main.

GitHub Actions Integration

Add two steps to your CI workflow: run tests + report coverage.

# .github/workflows/test.yml
name: Test

on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci
      - run: npx playwright install chromium --with-deps

      - name: Run tests with coverage
        run: npx vitest run --coverage

      - name: Report coverage
        uses: davelosert/vitest-coverage-report-action@v2
        with:
          json-summary-path: './coverage/coverage-summary.json'

This Action automatically displays coverage changes in PR comments:

  • Coverage change percentage (e.g., from 80% to 79%, shows -1%)
  • Which files had coverage drops
  • Which new code is uncovered

PR authors see it immediately: “Oh, my new code doesn’t have enough coverage, I need to add tests.”

CI Considerations

Browser Mode has a few gotchas in CI environments:

  1. Playwright Browser Installation: Add --with-deps flag, otherwise Chromium won’t start.
  2. Headless Mode: Force headless: true in config—CI environments have no display.
  3. Timeout Settings: Browser Mode tests are slower than jsdom. Increase CI timeout threshold—I set 30 seconds.
  4. Parallelism Control: Browser Mode shares browser instances. Don’t set parallel test count too high—I limit to maxWorkers: 4.

My complete CI workflow configuration:

- name: Run browser tests
  run: npx vitest run --coverage --browser.headless
  env:
    CI: true

--browser.headless ensures the browser doesn’t open windows. CI: true environment variable lets Vitest automatically adjust some behaviors (like disabling color output).

Summary

After all this, the core message is simple: jsdom can’t test real browser behavior—Browser Mode can. Canvas, computed CSS styles, Web Components, drag-and-drop, animations—these scenarios require Browser Mode.

Configuration isn’t hard. Install @vitest/browser-playwright, modify a few lines in vitest.config.ts, and you’re running. React and Vue component testing APIs are similar to Testing Library—easy to learn.

But Browser Mode isn’t a silver bullet. It tests individual components; Playwright tests flows. Use them together: Browser Mode for all UI components during development, Playwright E2E for critical paths before release. This gives comprehensive test coverage without slowing down CI.

Coverage gates are the final piece. Set a threshold, and PRs below threshold can’t merge—preventing coverage from slowly degrading. Add coverage-report-action in GitHub Actions, and PR comments automatically show coverage changes—problems are visible at a glance.

If you haven’t tried Browser Mode yet, start with simple components. A button component, an input component—configure Browser Mode first, verify the setup works, then gradually expand to complex components. You’ll hit some bumps, but once you’ve worked through them and gotten comfortable writing tests, development efficiency really improves.

Configure Vitest Browser Mode for component testing

Set up Browser Mode from scratch, run React/Vue component tests, and integrate CI coverage gates

⏱️ Estimated time: 20 min

  1. 1

    Step1: Install Playwright provider

    Run npm install -D vitest @vitest/browser-playwright to install dependencies, then execute npx playwright install chromium to download the browser.
  2. 2

    Step2: Configure vitest.config.ts

    Set provider: playwright(), enabled: true, and instances: [{ browser: 'chromium' }] in test.browser. Add headless: true for CI environments.
  3. 3

    Step3: Write component tests

    Use page.mount() to render components, page.getByRole() to query elements, userEvent.click() to simulate interactions, and expect.element() to assert results.
  4. 4

    Step4: Configure coverage gates

    Set threshold values in vitest.config.ts coverage.thresholds (e.g., lines: 80), and integrate vitest-coverage-report-action in GitHub Actions to display PR coverage changes.

FAQ

What's the difference between Browser Mode and jsdom?
jsdom simulates DOM in Node.js and cannot test Canvas rendering, computed CSS styles, or Web Components lifecycle. Browser Mode renders components in real browsers, testing actual behavior.
How to choose between Browser Mode and Playwright E2E?
Browser Mode is ideal for individual component testing (200ms/test) with instant feedback during development. Playwright suits multi-page flow testing (2-5s/test) to verify critical paths before release. Use both together.
What coverage threshold should I set?
Start at 50% for new projects, 80% is reasonable for mature projects. Core modules can be set to 90%. Don't chase 100%—some edge cases are impractical to test.
Does Browser Mode support Vue 2?
No. Vue 2 requires @vue/test-utils. Vue 3 projects can use @vitest/browser-vue directly.
What should I note about Browser Mode in CI environments?
Add the --with-deps flag when installing Playwright, configure headless: true, increase timeout thresholds (30 seconds), and limit parallel test workers (maxWorkers: 4).

10 min read · Published on: May 17, 2026 · Modified on: May 17, 2026

Comments

Sign in with GitHub to leave a comment