SoftwareTestPilot
Module 08 · Lesson 1intermediate 11 min read Playwright

Visual Regression Testing with Playwright

Catch every unintended pixel change without third-party services. Learn baselines, masking, thresholds and how to keep visual tests stable across every machine that runs them.

Quick answer

Call await expect(page).toHaveScreenshot(). First run saves a PNG baseline in __snapshots__/; subsequent runs compare pixel-for-pixel and fail on diffs. Mask timestamps and update baselines with --update-snapshots.

1. Your first visual test

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

test('landing page looks correct', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('landing.png');
});

test('pricing card matches design', async ({ page }) => {
  await page.goto('/pricing');
  const card = page.getByTestId('plan-pro');
  await expect(card).toHaveScreenshot('pricing-pro.png');
});

2. Mask everything that changes

await expect(page).toHaveScreenshot({
  mask: [
    page.getByTestId('timestamp'),
    page.getByTestId('user-avatar'),
    page.locator('.live-price'),
  ],
  maskColor: '#ff00ff',   // default is pink
});

3. Tuning thresholds

OptionMeaningWhen to use
maxDiffPixels: 0Exact matchComponent snapshots
maxDiffPixels: 100Allow up to N different pixelsSmall antialiasing noise
maxDiffPixelRatio: 0.02Allow 2% of pixels to differFull-page shots with fonts
threshold: 0.2Per-pixel colour toleranceRarely — prefer masks

4. Making snapshots stable across machines

// playwright.config.ts
export default defineConfig({
  use: {
    // Freeze animations so snapshots aren't captured mid-transition
    launchOptions: { args: ['--force-prefers-reduced-motion'] },
  },
  expect: {
    toHaveScreenshot: { animations: 'disabled', caret: 'hide', scale: 'css' },
  },
});
Run in the official Docker image
Local diffs vanish when both dev and CI use mcr.microsoft.com/playwright:v{VERSION}-jammy. Font hinting and GPU output are now identical.

5. Updating baselines

# Re-record after a legitimate UI change
npx playwright test --update-snapshots

# Update only one project or one file
npx playwright test tests/landing.spec.ts --update-snapshots --project=chromium

# Then commit the __snapshots__ diff and review it in the PR

6. Common mistakes

Snapshotting the whole page with dynamic data

You will chase flakes forever. Snapshot components, or mask everything dynamic.

Updating baselines blindly

--update-snapshots without reviewing the diff hides real regressions. Treat baseline PRs as code review.

Different OS locally vs CI

Fonts render differently on macOS, Windows and Linux. Standardise on Playwright's Docker image.

7. Hands-on task (20 minutes)

  1. 1

    Snapshot a card

    Pick one component on your app. Add a toHaveScreenshot test. Commit the baseline.

  2. 2

    Introduce a change

    Change a colour or padding. Watch the test fail with a nice side-by-side diff in the HTML report.

  3. 3

    Mask a timestamp

    Add a live timestamp near the card. Fix the flaky test by masking that locator.

Ready to ship the whole suite: CI/CD with GitHub Actions.

Frequently asked questions

1.How does Playwright visual regression work?
`await expect(page).toHaveScreenshot()` captures a PNG, compares it pixel-by-pixel to the saved baseline, and fails if the diff exceeds the threshold. The first run creates the baseline; subsequent runs enforce it.
2.How do I update baselines after a legitimate UI change?
Run `npx playwright test --update-snapshots`. Review the diff before committing — treat baseline updates like production code changes.
3.Why do my visual tests fail only in CI?
Font rendering, GPU antialiasing and OS-level pixel differences. Fix by running visual tests in a Docker container that matches CI, or by using Playwright's official Docker image for both local and CI runs.
4.How do I mask a dynamic region (avatar, timestamp)?
Pass `mask` with the locators to blur: `await expect(page).toHaveScreenshot({ mask: [page.getByTestId('timestamp')] });`. Playwright paints a pink rectangle over that area before comparing.
5.What threshold should I use?
Start with the default (`maxDiffPixels: 0`). Loosen only when you know exactly why — e.g. `maxDiffPixelRatio: 0.02` for pages with subtle antialiasing. Never disable comparison entirely.
6.Component snapshots or full-page?
Prefer component snapshots (`await expect(page.getByTestId('card')).toHaveScreenshot()`). They're smaller, faster and less flaky than full-page shots.

Related lessons