Skip to content
Blog

Testing Your Frontend: Vitest, Playwright, and Visual Regressions

A leveled-up frontend test strategy for 2026: unit and component tests in Vitest 4, end-to-end flows in Playwright, and pixel-comparison visual regressions — with a CI-ready workflow.

Published on August 11, 2026

AI Assistant

Most frontend test suites have a blind spot. Unit tests cover logic, end-to-end tests cover happy paths — but neither catches the regression that actually bites users: a button that moved 2px and clipped, a font-size change that broke a layout, an image that stopped loading and collapsed a card. That is the case for a layered strategy: fast unit/component tests where behavior lives, end-to-end tests where the browser does, and visual regression tests where pixels do.

In this post, you will learn how to structure that stack on a Vite project: component tests with Vitest 4’s now-stable Browser Mode, user-journey tests with Playwright Test, and pixel-comparison visual checks with both tools’ built-in screenshot assertions. Key technologies: Vitest 4.0, Playwright 1.62, and CI-friendly snapshot workflows.

Prerequisites

  • A Vite + React (or any framework) project with npm.
  • Node.js 20+.
  • Familiarity with Jest/Vitest-style describe/it/expect.
  • A CI provider in mind (GitHub Actions works for all examples here).

Why three layers — and why not more

Each layer tests a different failure mode at a different speed:

  • Vitest component tests run in milliseconds, in memory, in the same environment the component code runs in. They verify state transitions, props, and event handlers.
  • Playwright end-to-end tests drive a real browser through full user journeys — navigation, forms, API integration, multi-page flows — in seconds.
  • Visual regression tests compare rendered pixels against a committed baseline and catch what neither other layer can: layout drift, missing assets, unexpected font rendering.

You could bolt on Cypress or Storybook too, but the Vitest + Playwright pair covers the full spectrum with two runtime environments and no duplicated test harnesses.

Layer 1: Component tests with Vitest 4

Vitest 4.0, released in December 2025, made Browser Mode stable and added built-in visual regression testing. Component tests run your components in a real browser context (via Playwright or WebdriverIO as the provider) instead of a jsdom mock:

// src/components/Counter.test.tsx
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-react';
import { Counter } from './Counter';

test('increments when clicked', async () => {
  const screen = render(<Counter initial={0} />);
  await screen.getByRole('button', { name: 'increment' }).click();
  await expect.element(screen.getByText('1')).toBeVisible();
});

Browser Mode gives you real CSS, real layout, and real event semantics — getByRole, toBeVisible, and auto-retrying queries that until recently existed only in Playwright. Configure it in vitest.config.ts:

import { defineConfig } from 'vitest/config';

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

Layer 2: End-to-end with Playwright Test

Playwright Test gives each test a fresh browser context (full isolation, near-zero overhead), auto-waits for elements to be actionable, and uses web-first assertions that retry until conditions are met. The canonical setup:

npm init playwright@latest

A realistic user journey — search filters the product list — looks like this:

// e2e/catalog.spec.ts
import { test, expect } from '@playwright/test';

test('filters products by search', async ({ page }) => {
  await page.goto('http://localhost:4321/catalog');
  await page.getByPlaceholder('Search products').fill('drone');
  await expect(page.getByRole('listitem')).toHaveCount(3);
  await expect(page.getByText('Cargo Drone X2')).toBeVisible();
});

Playwright’s auto-waiting means no waitForTimeout(1000) hacks — the assertions poll the browser until the condition holds. Run it with npx playwright test; browsers install with npx playwright install.

When e2e overlaps with unit

A common trap is duplicating component behavior in e2e tests. The split to keep: unit/component tests own behavior (state, handlers, props); e2e tests own integration (nav, routing, real network, cross-page state). If you find yourself waiting for a spinner in both, you have the wrong split.

Layer 3: Visual regression testing

With Playwright

Playwright’s toHaveScreenshot() captures a screenshot on first run (writing the baseline) and compares on every subsequent run using pixelmatch:

import { test, expect } from '@playwright/test';

test('product card renders consistently', async ({ page }) => {
  await page.goto('http://localhost:4321/catalog');
  const card = page.locator('.product-card').first();
  await expect(card).toHaveScreenshot('product-card.png', {
    maxDiffPixels: 100,
    animations: 'disabled',
  });
});

Options that matter in practice: maxDiffPixels (an absolute budget, easier to reason about than a ratio for a small element) and animations: 'disabled' so spinners and transitions don’t cause false positives. Snapshots live in a -snapshots directory next to the test file and must be committed to git; review them like a code diff. Update deliberately after an intentional change with npx playwright test --update-snapshots.

With Vitest 4

Vitest 4 ships equivalent capability inside Browser Mode via toMatchScreenshot. The clever part is its stable screenshot detection: it takes a screenshot, compares it to the previous one, and only declares the page stable when two consecutive captures match — so async images and late animations are handled automatically:

test('button renders in default state', async () => {
  const screen = render(<SubmitButton label="Save" />);
  await expect.element(screen.getByRole('button')).toMatchScreenshot();
});

Tune the tolerance globally in the browser config:

test: {
  browser: {
    expect: {
      toMatchScreenshot: {
        comparator: 'pixelmatch',
        comparatorOptions: {
          threshold: 0.2,
          allowedMismatchedPixelRatio: 0.01,
        },
      },
    },
  },
},

allowedMismatchedPixelRatio scales the tolerance to the size of the element rather than a fixed pixel count.

The CI workflow: where visual tests actually survive

Visual snapshots are only trustworthy if they were generated in the same environment that runs them. That means: pin browser versions, generate baselines in CI, and never let a local --update silently rewrite references. A GitHub Actions workflow that does this:

name: frontend-tests
on: [push, 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 --with-deps --only-shell
      - run: npm run test:unit        # vitest run
      - run: npm run test:e2e         # playwright test
      - run: npm run test:visual      # --update, guarded to this job

The --only-shell flag installs only the browser binaries needed for tests. When a visual baseline legitimately changes, regenerate it through a manual CI job that runs test:visual --update and commits the diff — same environment, same fonts, no surprises.

Putting It All Together

A complete, runnable version of this stack — the Vitest component suite, the Playwright e2e suite, two visual-regression suites, and this GitHub Actions workflow — is curated here: https://gist.github.com/redlinesoft/frontend-testing-vitest-playwright-visual

Expected output from one CI run:

vitest:     24 passed    (component suite, ~14s)
playwright:  6 passed    (e2e suite, chromium/firefox/webkit, ~48s)
visual:      9 passed, 1 diff detected
  e2e/product-card.png          unchanged
  vitest/submit-button.png      unchanged
  e2e/checkout-summary.png      MISMATCH (41 diff pixels)  -> review or update

One caught regression that the unit suite could never have seen — a checkout summary where a padding change pushed the total below the fold.

Conclusion & Next Steps

You now have a three-layer strategy: Vitest 4 Browser Mode for fast component behavior, Playwright for full user journeys, and pixel-comparison visual regressions in both runners with a CI workflow that makes baselines trustworthy. Next steps: add toBeInViewport assertions from Vitest 4 to your component suite; split your e2e suite into web projects per browser (Chromium/Firefox/WebKit) and compare which surfaces drift; and review Playwright’s stylePath option, which lets you strip volatile elements before a screenshot so your baselines stay stable for years.

References / Sources