| name | visual-testing |
| description | Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration. |
Visual Testing
Visual regression testing — detecting unintended UI changes automatically.
When to Activate
- Setting up visual regression testing for a UI project
- Choosing between Chromatic, Percy, and Playwright screenshots
- Configuring baseline management (when to update, when to flag)
- Debugging flaky visual tests (antialiasing, font loading, animations)
- Integrating visual tests into CI/CD pipeline
- Preventing cross-platform baseline drift caused by macOS vs. Linux font rendering differences
- Masking dynamic content (timestamps, user avatars, random banners) to eliminate false positives
Concept: Visual Regression Testing
Visual regression testing captures screenshots and compares them against a baseline — flagging any visual change for review.
Baseline (approved) ──────────────── Current screenshot
│ │
└──────── pixel diff ─────────────────┘
│
Difference > threshold?
YES → Fail test / flag for review
NO → Pass
Types of comparison:
| Approach | Tool | Accuracy | Noise |
|---|
| Pixel-exact | Playwright, BackstopJS | High | High (fonts, AA, rendering) |
| AI-based | Chromatic, Percy | Ignores irrelevant diffs | Low |
| Perceptual hash | Custom | Medium | Medium |
Tool Comparison
| Tool | Type | Storybook | Multi-browser | Pricing |
|---|
| Chromatic | Cloud (AI-based) | ✅ First-class | ✅ | Free up to 5k snapshots/mo |
| Percy (BrowserStack) | Cloud (AI-based) | ✅ | ✅ | Free up to 5k/mo |
Playwright toHaveScreenshot | In-repo (pixel-diff) | ⚠️ Via test-runner | ✅ | Free (open source) |
| BackstopJS | Self-hosted (pixel-diff) | ⚠️ Manual setup | ✅ (Chromium) | Free |
Decision guide:
- Has Storybook + wants UI review workflow → Chromatic
- Has Storybook + wants multi-browser + cloud → Percy
- No Storybook + already uses Playwright → Playwright
toHaveScreenshot
- On-prem / no cloud allowed → BackstopJS
Chromatic (Storybook)
Setup
npm install --save-dev chromatic
npx chromatic --project-token=<your-token>
GitHub Actions
name: Chromatic
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
onlyChanged: true
exitZeroOnChanges: false
autoAcceptChanges:
PR Workflow
On PRs, Chromatic shows a visual diff UI:
- Accept — this change was intentional, update baseline
- Deny — this is a regression, must be fixed
npx chromatic --project-token=<token> --auto-accept-changes
npx chromatic --project-token=<token> --only-changed
Playwright toHaveScreenshot
Setup
npm install --save-dev @playwright/test
npx playwright install
Writing Visual Tests
import { test, expect } from '@playwright/test';
test.describe('Homepage', () => {
test('renders correctly on desktop', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot('homepage-desktop.png', {
mask: [
page.locator('[data-testid="last-updated"]'),
page.locator('.user-avatar'),
],
maxDiffPixelRatio: 0.01,
});
});
test('renders product card correctly', async ({ page }) => {
await page.goto('/products');
await page.waitForSelector('[data-testid="product-card"]');
const card = page.().();
(card).(, {
: ,
});
});
(, ({ page }) => {
page.({ : , : });
page.();
page.();
(page).();
});
});
playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/visual',
snapshotDir: './tests/visual/__snapshots__',
updateSnapshots: 'none',
use: {
baseURL: 'http://localhost:3000',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 13'] },
},
],
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI,
},
});
Update Baselines
npx playwright test --update-snapshots
npx playwright test homepage --update-snapshots
npx playwright test
CI Integration
name: Visual Regression
on:
push:
branches: [main]
pull_request:
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Run visual tests
run: npx playwright test tests/visual/
- name: Upload diff artifacts on failure
if: failure()
BackstopJS (Open Source)
npm install --save-dev backstopjs
npx backstop init
{
"id": "my-app",
"viewports": [
{ "label": "desktop", "width": 1280, "height": 900 },
{ "label": "mobile", "width": 375, "height": 812 }
],
"scenarios": [
{
"label": "Homepage",
"url": "http://localhost:3000",
"delay": 500,
"hideSelectors": [".timestamp",
npx backstop reference
npx backstop test
npx backstop approve
Flakiness Prevention
Visual tests are the most flaky test type. Key strategies:
1. Wait for Stable State
await page.goto('/');
await expect(page).toHaveScreenshot();
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(200);
await expect(page).toHaveScreenshot({ animations: 'disabled' });
2. Mask Dynamic Content
await expect(page).toHaveScreenshot({
mask: [
page.locator('[data-testid="current-time"]'),
page.locator('.random-promo-banner'),
page.locator('[data-testid="session-id"]'),
],
});
3. Disable Animations
use: {
launchOptions: {
args: ['--force-prefers-reduced-motion'],
},
}
await page.addStyleTag({
content: `
*, *::before, *::after {
animation-duration: 0s !important;
transition-duration: 0s !important;
}
`,
});
await expect(page).toHaveScreenshot({ animations: 'disabled' });
4. Platform Consistency
docker run --rm -v $(pwd):/work mcr.microsoft.com/playwright:v1.44.0-jammy bash -c \
"cd /work && npm ci && npx playwright test --update-snapshots"
5. Tolerances
await expect(page).toHaveScreenshot({
maxDiffPixelRatio: 0.01,
maxDiffPixels: 50,
threshold: 0.2,
});
Baseline Management Best Practices
DO commit baselines to git — they're your visual contract
DO update baselines intentionally (not automatically in CI)
DO run visual tests in the same Docker image as CI
DO organize by component/page, not randomly
DON'T auto-update baselines on every push
DON'T skip visual tests to "fix" CI
DON'T generate baselines on developer machines if CI uses different fonts/OS
npx playwright test --update-snapshots
git add tests/visual/__snapshots__/
git commit -m "chore(visual): update baselines for new button style"
Reference
storybook-patterns — Storybook CSF3, play functions, Chromatic CI integration
e2e-testing — Playwright E2E tests (functional, not visual)