Visual Regression Testing
Tools Comparison
| Tool | Approach | Integrates With | Best For |
|---|---|---|---|
| Chromatic | Storybook-based component snapshots | Storybook | Component library, design system |
| Percy (BrowserStack) | Full-page + component snapshots | Selenium, Playwright, Cypress | Full app visual testing |
| Playwright Snapshots | Built-in screenshot comparison | Playwright | Free, self-hosted, page-level |
| Storyshots | Jest snapshot of Storybook stories | Jest + Storybook | Component-level regressions |
| Loki | Storybook screenshot comparison (open source) | Storybook, Docker | Self-hosted Chromatic alternative |
| reg-suit | Git diff-based image comparison | Any screenshot tool | Custom 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
| Step | Action |
|---|---|
| 1. First run | Generate baseline screenshots, commit to repo |
| 2. PR run | Compare screenshots; fail if diff exceeds threshold |
| 3. Review diffs | Developer reviews visual diff in PR comment / Chromatic UI |
| 4. Intentional change | Run --update-snapshots, commit new baselines |
| 5. Unintentional change | Fix CSS regression, re-run, confirm pass |
| Stability | Mask dynamic content (timestamps, ads, animations) |
| Stability | Use fixed viewport and font rendering; run in Docker for determinism |