Visual bugs are real bugs. A layout shift, a colour regression, a truncated label — these reach users and damage trust, yet they're invisible to functional tests. Snapshot testing fills this gap.

How Playwright Visual Testing Works

Playwright captures a screenshot of a component or page and compares it pixel-by-pixel against a stored baseline. If the diff exceeds a threshold, the test fails.

test('homepage hero looks correct', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('hero.png', { maxDiffPixels: 100 });
});

On first run, the screenshot is saved as the baseline. Subsequent runs compare against it.

Setting Baselines

Run once with --update-snapshots to generate your initial baselines. Commit these to source control — they're the source of truth for what the UI should look like.

npx playwright test --update-snapshots

Handling Dynamic Content

Dynamic content — dates, user names, animated elements — will cause false failures. Mask them:

await expect(page).toHaveScreenshot('dashboard.png', {
  mask: [page.getByTestId('user-greeting'), page.getByTestId('live-clock')]
});

CI Integration

Visual tests are slow and should run on a dedicated step, not blocking every commit:

- name: Visual Regression Tests
  run: npx playwright test tests/visual/
  continue-on-error: true  # warn, don't block initially

As confidence grows, move this to a blocking gate.

Component-Level vs Page-Level

Page-level snapshots are brittle — any content change anywhere fails the test. Prefer component-level snapshots that isolate what you're testing.

Conclusion

Visual regression testing catches a category of bugs that no amount of functional testing will find. Set it up once, commit your baselines, and let CI guard your UI automatically.