Playwright's toHaveScreenshot() assertion captures a screenshot and compares it pixel-by-pixel against a committed baseline, failing when the rendered UI drifts beyond a configured threshold. It's built in — no third-party visual service required for basic coverage.
Functional assertions can't catch a broken layout, a wrong color, or an overlapping element that still has the right text. Visual testing fills that gap. On first run, toHaveScreenshot() writes a baseline; later runs diff against it and produce an actual/expected/diff triplet on mismatch. Because rendering varies by OS and browser, baselines are stored per platform/project, and CI should generate them on the same OS it runs on (often via a Linux container or the official Playwright Docker image) to avoid font/anti-aliasing noise. You reduce false positives by masking dynamic regions (timestamps, avatars), setting maxDiffPixels/threshold, and disabling animations. Update baselines intentionally with --update-snapshots when a change is legitimate.
A CSS refactor accidentally pushed the checkout total off-screen on mobile widths. Every functional test still passed (the text was present), but the mobile visual snapshot failed with a clear diff highlighting the misplaced total — catching a bug that assertions alone would have shipped.
// First run creates the baseline; later runs diff against it
await expect(page).toHaveScreenshot('checkout.png', {
maxDiffPixels: 100,
mask: [page.getByTestId('order-timestamp')], // ignore dynamic text
animations: 'disabled',
});
// Component-level snapshot
await expect(page.getByRole('dialog')).toHaveScreenshot('dialog.png');
# Regenerate baselines after an intended UI change
npx playwright test --update-snapshotsScenario: A newly added visual test passes on a developer's Mac but fails on Linux CI with thousands of pixel diffs, even though the UI looks identical. What's wrong, and how do you make visual testing reliable across environments?