← Recipes

阅读中文版本

Add Visual Regression Checks with Playwright Screenshots

Screenshot stable viewports with deterministic content, compare against committed baselines with a tolerance, and review diffs in the pull request.

~30 mintestinge2eplaywright

Task

Catch unintended visual changes automatically while keeping intentional restyling reviewable as pixel diffs.

When to use this

A CSS refactor shifted every button 3 pixels. No functional test noticed, and the feedback arrived as screenshots from an annoyed customer.

Prerequisites

  • Playwright with screenshot capability
  • Deterministic test data or seeded content
  • A review workflow for diff images

Steps

1. Screenshot a stable viewport with deterministic data.

test("pricing page matches baseline", async ({ page }) => {
  await page.goto("/pricing");
  await page.getByTestId("plan-standard").waitFor();
  expect(await page.screenshot({ fullPage: false })).toMatchSnapshot("pricing.png", {
    maxDiffPixelRatio: 0.01,
  });
});

Expected output: The screenshot compares against the committed baseline with a one percent pixel tolerance for font rendering variance.

2. Freeze nondeterminism before capturing.

await page.addInitScript(() => {
  Date.now = () => new Date("2026-10-22T12:00:00Z").getTime();
});
await page.route("**/banners/*", route => route.fulfill({ status: 200, body: "" }));

Expected output: Frozen clocks and stubbed rotating banners remove the two largest sources of false diffs.

3. Scope screenshots to the component under test.

const card = page.getByTestId("pricing-card-pro");
expect(await card.screenshot()).toMatchSnapshot("card-pro.png");

Expected output: Component-level baselines stay valid when unrelated page regions change, keeping the suite low-churn.

4. Review intentional changes as diffs.

npx playwright test --update-snapshots && git diff --stat -- "*.png" | tail -3

Expected output: Restyling updates baselines in the PR, and reviewers see exactly which pixels moved before approving.

5. Gate on unreviewed baseline changes.

if (git diff --name-only origin/main | grep -q "\.png$"); then
  echo "snapshot changed without --update-snapshots review"; exit 1
fi

Expected output: A snapshot diff that appears without the explicit update flag fails CI, preventing silent baseline drift.

Troubleshooting

Problem

Diffs fire from font rendering differences

Solution

Pin the fonts in the test environment and use a small pixel-ratio tolerance; never disable checks to silence the noise.

Problem

Baselines churn every design tweak

Solution

Screenshot at component granularity so unrelated changes do not invalidate page-level baselines, and batch updates deliberately.

Problem

Animations pollute every capture

Solution

Disable animations and transitions in the test browser options before screenshotting.

Limitations

  • Pixel comparison flags changes it cannot interpret; accessibility and semantics need separate checks.
  • Baseline images are architecture- and font-specific; regenerate per platform deliberately.

Pixels can be a contract too

Functional assertions cannot see a 3-pixel shift, and customers can. Visual regression closes the gap: deterministic screenshots compared against committed baselines turn accidental layout changes into test failures and intentional restyling into a reviewable pixel diff. The craft is eliminating false positives, because a noisy visual suite gets disabled within a week.

const card = page.getByTestId("pricing-card-pro");
expect(await card.screenshot()).toMatchSnapshot("card-pro.png", {
  maxDiffPixelRatio: 0.01,
});

Determinism before pixels

Every nondeterministic input, a live clock, a rotating banner, an animation frame, becomes a false diff eventually. Freeze the clock, stub rotating content, and disable animations before capture. The rule of thumb: if a screenshot can change without a code change, fix that before writing any baseline.

Component granularity beats page granularity

A page-level baseline is invalidated by any change anywhere, so every minor nav tweak re-records the entire page. Component-level screenshots localize both the change and the churn: a button restyle re-records the button, and the pricing card baseline from last month still guards the pricing card today.