Q30 of 42 · Playwright

How would you architect a Playwright suite for cross-browser visual regression?

PlaywrightSeniorplaywrightvisual-regressionsnapshotscross-browsersenior

Short answer

Short answer: Use `expect(page).toHaveScreenshot()` with per-project baselines (Playwright auto-keys baselines by project name). Mask volatile elements, freeze time, disable animations. Store baselines in git (small set) or S3 (large set). Update intentionally via `--update-snapshots` in a dedicated PR-affecting workflow.

Detail

Playwright has built-in visual regression — no plugin needed. The architecture:

Step 1: Configure projects for the browsers you want visual coverage on:

projects: [
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
  { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
]

Step 2: Snapshots are auto-keyed by project:

await expect(page).toHaveScreenshot('home.png', { maxDiffPixels: 50 });

Generates home-chromium.png, home-firefox.png, home-webkit.png. Each project compares against its own baseline.

Step 3: Stabilise the page before snapshotting:

await page.clock.setFixedTime(new Date('2026-01-01'));   // freeze time-derived UI
await page.addStyleTag({ content: '* { animation: none !important; transition: none !important; }' });
await page.waitForLoadState('networkidle');               // wait for everything to load
await expect(page.getByTestId('spinner')).not.toBeVisible();

Step 4: Mask volatile elements that genuinely change per run:

await expect(page).toHaveScreenshot('home.png', {
  mask: [
    page.getByTestId('relative-time'),
    page.getByTestId('user-avatar'),
  ],
});

Step 5: Storage strategy:

  • Git: easy reviews (diffs show in PRs), but baselines bloat the repo. Use git LFS once you exceed ~50MB.
  • S3 / object storage: scales better, but PR review of baseline changes is harder. Tools like Reg-Suit or homegrown scripts handle the upload/diff.

Step 6: Update workflow:

# PR fails because of an intentional UI change
npx playwright test --update-snapshots
git add tests/__screenshots__
git commit -m "Update visual baselines for new pricing card"

The new baselines ship in the same PR as the UI change, with the diff in code review.

Step 7: CI gating:

  • Run visuals on chromium-only on PR (fast feedback).
  • Full cross-browser visual on PR-to-main or nightly.
  • Block merge on visual diffs above tolerance.

Watch-outs:

  • Cross-browser will diverge. Even a stable page renders pixel-differently across engines (font hinting, scrollbar width). Set maxDiffPixels per project; webkit usually needs the highest tolerance.
  • CI vs local mismatch. Different OSs render fonts differently. Standardise on a Docker base image (mcr.microsoft.com/playwright:v...) so local-vs-CI diffs go away.
  • Don't snapshot every page. 50 well-chosen snapshots beat 500 bot-generated ones.

// WHAT INTERVIEWERS LOOK FOR

The auto-key-by-project pattern, stabilisation steps (clock/animations/networkidle), `--update-snapshots` workflow, and the local-vs-CI Docker discipline.

// COMMON PITFALL

Snapshotting volatile UI without freezing time / animations / data — every PR has unrelated diffs and the team stops trusting visuals.