Visual Regression Testing

Tools Comparison

ToolApproachIntegrates WithBest For
ChromaticStorybook-based component snapshotsStorybookComponent library, design system
Percy (BrowserStack)Full-page + component snapshotsSelenium, Playwright, CypressFull app visual testing
Playwright SnapshotsBuilt-in screenshot comparisonPlaywrightFree, self-hosted, page-level
StoryshotsJest snapshot of Storybook storiesJest + StorybookComponent-level regressions
LokiStorybook screenshot comparison (open source)Storybook, DockerSelf-hosted Chromatic alternative
reg-suitGit diff-based image comparisonAny screenshot toolCustom pipelines, flexible storage

Playwright Visual Tests

import { test, expect } from '@playwright/test'; test('homepage visual regression', async ({ page }) => { await page.goto('/'); await page.waitForLoadState('networkidle'); // Full page screenshot comparison await expect(page).toHaveScreenshot('homepage.png', { fullPage: true, threshold: 0.2, // 0.2% pixel difference allowed maxDiffPixels: 100, // absolute max diff pixels }); }); test('button states', async ({ page }) => { await page.goto('/components'); const button = page.getByRole('button', { name: 'Submit' }); // Default state await expect(button).toHaveScreenshot('button-default.png'); // Hover state await button.hover(); await expect(button).toHaveScreenshot('button-hover.png'); // Focused state await button.focus(); await expect(button).toHaveScreenshot('button-focused.png'); }); // playwright.config.ts // export default { // snapshotPathTemplate: '{testDir}/snapshots/{testFilePath}/{arg}{ext}', // use: { // // Mask dynamic content to prevent false positives // mask: [page.locator('.timestamp'), page.locator('.ad-banner')], // } // }; # Update baseline snapshots npx playwright test --update-snapshots

CI Workflow for Visual Tests

StepAction
1. First runGenerate baseline screenshots, commit to repo
2. PR runCompare screenshots; fail if diff exceeds threshold
3. Review diffsDeveloper reviews visual diff in PR comment / Chromatic UI
4. Intentional changeRun --update-snapshots, commit new baselines
5. Unintentional changeFix CSS regression, re-run, confirm pass
StabilityMask dynamic content (timestamps, ads, animations)
StabilityUse fixed viewport and font rendering; run in Docker for determinism