Visual Regression Testing
Catch UI regressions with screenshot comparison and snapshot thresholds.
Screenshot Assertions
expect(page).toHaveScreenshot() and expect(locator).toHaveScreenshot() compare against golden baseline on disk. First run creates baseline; subsequent runs diff pixel-by-pixel.
Configure snapshotPathTemplate for organized baseline folder structure per project/browser.
await expect(page).toHaveScreenshot("dashboard.png");
await expect(page.getByTestId("chart")).toHaveScreenshot();Thresholds and Stability
maxDiffPixels and maxDiffPixelRatio tolerate anti-aliasing and font rendering variance. mask clips dynamic regions—clock, ads, animated GIF.
Disable animations in test env CSS for stable captures: prefers-reduced-motion or utility class.
- Run visual tests same viewport and device project consistently
- WebKit and Chromium baselines differ—separate snapshots per project
- Update snapshots deliberately: npx playwright test --update-snapshots
await expect(page).toHaveScreenshot({
mask: [page.getByTestId("timestamp")],
maxDiffPixelRatio: 0.02,
});Full Page vs Component
Full page screenshots catch layout shifts; component screenshots isolate widgets. Combine with component testing for fast visual coverage.
Scroll fullPage: true for long pages—watch file size growth.
await page.goto("/marketing");
await expect(page).toHaveScreenshot({ fullPage: true });CI Visual Testing
Store baselines in repo LFS if large. CI fails on diff; upload diff image artifact for reviewer. Docker consistent fonts reduce flakiness vs macOS local baselines.
Some teams use Percy or Chromatic external services—Playwright native snapshots zero vendor cost.
- Review diff images in PR before approving snapshot update
- Never bulk update snapshots without visual review
- Separate visual spec job optional for faster unit+ E2E PR gate
When Visual Tests Help
Charts, maps, complex CSS grids, and marketing pages benefit. Pure logic forms better suited to role/text assertions.
Visual tests complement not replace functional E2E—button can look correct and still fail click handler.
// Combine functional + visual
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Success")).toBeVisible();
await expect(page).toHaveScreenshot("success-state.png");