visual-regression
Visual regression testing expert using Playwright snapshots, Percy, Chromatic, BackstopJS, and pixel-diff analysis. Covers baseline management, responsive testing, cross-browser visual testing, component visual testing, and CI integration. Activates for visual regression, screenshot testing, visual diff, Percy, Chromatic, BackstopJS, pixel comparison, snapshot testing, visual testing, CSS regression, UI looks different, layout changed, style regression, component looks wrong, responsive breakpoints, cross-browser differences, screenshot comparison, baseline update, approve changes, Storybook visual tests, design QA, pixel perfect, UI drift, visual bugs.
$ Installer
git clone https://github.com/anton-abyzov/specweave /tmp/specweave && cp -r /tmp/specweave/plugins/specweave-ui/skills/visual-regression ~/.claude/skills/specweave// tip: Run this command in your terminal to install the skill
name: visual-regression description: Visual regression testing expert using Playwright snapshots, Percy, Chromatic, BackstopJS, and pixel-diff analysis. Covers baseline management, responsive testing, cross-browser visual testing, component visual testing, and CI integration. Activates for visual regression, screenshot testing, visual diff, Percy, Chromatic, BackstopJS, pixel comparison, snapshot testing, visual testing, CSS regression, UI looks different, layout changed, style regression, component looks wrong, responsive breakpoints, cross-browser differences, screenshot comparison, baseline update, approve changes, Storybook visual tests, design QA, pixel perfect, UI drift, visual bugs.
Visual Regression Testing Skill
Expert in visual regression testing - automated detection of unintended visual changes in web applications using screenshot comparison, pixel diffing, and visual testing frameworks.
Why Visual Regression Testing?
Problems It Solves:
- CSS changes breaking layout unexpectedly
- Responsive design regressions (mobile/tablet/desktop)
- Cross-browser rendering differences
- Component library changes affecting consumers
- UI regressions that functional tests miss
Example Scenario:
Developer changes global CSS: `.container { padding: 10px }`
↓
Accidentally breaks checkout page layout
↓
Functional E2E tests pass (buttons still clickable)
↓
Visual regression test catches layout shift
Core Tools
1. Playwright Visual Snapshots (Built-in)
Why Playwright?
- No third-party service required (free)
- Fast (parallel execution)
- Built-in automatic masking (hide dynamic content)
- Cross-browser support (Chromium, Firefox, WebKit)
Basic Snapshot Test
import { test, expect } from '@playwright/test';
test('homepage should match visual baseline', async ({ page }) => {
await page.goto('https://example.com');
// Take full-page screenshot and compare to baseline
await expect(page).toHaveScreenshot('homepage.png');
});
First Run (create baseline):
npx playwright test --update-snapshots
# Creates: tests/__screenshots__/homepage.spec.ts/homepage-chromium-darwin.png
Subsequent Runs (compare to baseline):
npx playwright test
# Compares current screenshot to baseline
# Fails if difference exceeds threshold
Element-Level Snapshots
test('button should match visual baseline', async ({ page }) => {
await page.goto('/buttons');
const submitButton = page.locator('[data-testid="submit-button"]');
await expect(submitButton).toHaveScreenshot('submit-button.png');
});
Configurable Thresholds
// playwright.config.ts
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 100, // Allow max 100 pixels to differ
// OR
maxDiffPixelRatio: 0.01, // Allow 1% of pixels to differ
},
},
});
Masking Dynamic Content
test('dashboard with dynamic data', async ({ page }) => {
await page.goto('/dashboard');
// Mask elements that change frequently (timestamps, user IDs)
await expect(page).toHaveScreenshot({
mask: [
page.locator('.timestamp'),
page.locator('.user-avatar'),
page.locator('[data-testid="ad-banner"]'),
],
});
});
Responsive Testing (Multiple Viewports)
const viewports = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1920, height: 1080 },
];
for (const viewport of viewports) {
test(`homepage on ${viewport.name}`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('https://example.com');
await expect(page).toHaveScreenshot(`homepage-${viewport.name}.png`);
});
}
2. Percy (Cloud-Based Visual Testing)
Why Percy?
- Smart diffing (ignores anti-aliasing differences)
- Review UI (approve/reject changes)
- Integrates with GitHub PRs
- Parallel testing across browsers
- Automatic baseline management
Setup
npm install --save-dev @percy/playwright
// tests/visual.spec.ts
import { test } from '@playwright/test';
import percySnapshot from '@percy/playwright';
test('homepage visual test', async ({ page }) => {
await page.goto('https://example.com');
// Percy captures screenshot and compares to baseline
await percySnapshot(page, 'Homepage');
});
# Run tests with Percy
PERCY_TOKEN=your_token npx percy exec -- npx playwright test
Percy Configuration
# .percy.yml
version: 2
snapshot:
widths:
- 375 # Mobile
- 768 # Tablet
- 1280 # Desktop
min-height: 1024
percy-css: |
/* Hide dynamic elements */
.timestamp { visibility: hidden; }
.ad-banner { display: none; }
Percy in CI (GitHub Actions)
name: Visual Tests
on: [pull_request]
jobs:
percy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npx playwright install --with-deps
- name: Run Percy tests
run: npx percy exec -- npx playwright test
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
3. Chromatic (Storybook Visual Testing)
Why Chromatic?
- Designed for component libraries (Storybook integration)
- Captures all component states automatically
- UI review workflow (approve/reject)
- Detects accessibility issues
- Version control for design system
Setup (Storybook + Chromatic)
npm install --save-dev chromatic
npx chromatic --project-token=your_token
// .storybook/main.js
module.exports = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-essentials'],
};
// Button.stories.tsx
import { Button } from './Button';
export default {
title: 'Components/Button',
component: Button,
};
export const Primary = () => <Button variant="primary">Click me</Button>;
export const Disabled = () => <Button disabled>Disabled</Button>;
export const Loading = () => <Button loading>Loading...</Button>;
# Chromatic captures all stories automatically
npx chromatic --project-token=your_token
Chromatic in CI
name: Chromatic
on: push
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Required for Chromatic
- uses: actions/setup-node@v3
- run: npm ci
- run: npx chromatic --project-token=${{ secrets.CHROMATIC_TOKEN }}
4. BackstopJS (Configuration-Based)
Why BackstopJS?
- No code required (JSON configuration)
- Local execution (no cloud service)
- Interactive reports
- CSS selector-based scenarios
Configuration
{
"id": "myapp_visual_tests",
"viewports": [
{ "label": "phone", "width": 375, "height": 667 },
{ "label": "tablet", "width": 768, "height": 1024 },
{ "label": "desktop", "width": 1920, "height": 1080 }
],
"scenarios": [
{
"label": "Homepage",
"url": "https://example.com",
"selectors": ["document"],
"delay": 500
},
{
"label": "Login Form",
"url": "https://example.com/login",
"selectors": [".login-form"],
"hideSelectors": [".banner-ad"],
"delay": 1000
}
],
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference",
"bitmaps_test": "backstop_data/bitmaps_test",
"html_report": "backstop_data/html_report"
}
}
# Create baseline
backstop reference
# Run test (compare to baseline)
backstop test
# Update baseline (approve changes)
backstop approve
Testing Strategies
1. Component-Level Visual Testing
Use Case: Design system components (buttons, inputs, modals)
// Component snapshots
test.describe('Button component', () => {
test('primary variant', async ({ page }) => {
await page.goto('/storybook?path=/story/button--primary');
await expect(page.locator('.button')).toHaveScreenshot('button-primary.png');
});
test('disabled state', async ({ page }) => {
await page.goto('/storybook?path=/story/button--disabled');
await expect(page.locator('.button')).toHaveScreenshot('button-disabled.png');
});
test('hover state', async ({ page }) => {
await page.goto('/storybook?path=/story/button--primary');
const button = page.locator('.button');
await button.hover();
await expect(button).toHaveScreenshot('button-hover.png');
});
});
2. Page-Level Visual Testing
Use Case: Full pages (homepage, checkout, profile)
test('checkout page visual baseline', async ({ page }) => {
await page.goto('/checkout');
// Wait for page to fully load
await page.waitForLoadState('networkidle');
// Mask dynamic content
await expect(page).toHaveScreenshot('checkout.png', {
mask: [page.locator('.cart-timestamp'), page.locator('.promo-banner')],
fullPage: true, // Capture entire page (scrolling)
});
});
3. Interaction-Based Visual Testing
Use Case: Modals, dropdowns, tooltips (require interaction)
test('modal visual test', async ({ page }) => {
await page.goto('/');
// Open modal
await page.click('[data-testid="open-modal"]');
await page.waitForSelector('.modal');
// Capture modal screenshot
await expect(page.locator('.modal')).toHaveScreenshot('modal-open.png');
// Test error state
await page.fill('input[name="email"]', 'invalid');
await page.click('button[type="submit"]');
await expect(page.locator('.modal')).toHaveScreenshot('modal-error.png');
});
4. Cross-Browser Visual Testing
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
# Run tests across all browsers
npx playwright test
# Generates separate baselines per browser:
# - homepage-chromium-darwin.png
# - homepage-firefox-darwin.png
# - homepage-webkit-darwin.png
Best Practices
1. Stabilize Before Capturing
Problem: Animations, lazy loading, fonts cause flaky tests.
// ❌ BAD: Capture immediately
await page.goto('/');
await expect(page).toHaveScreenshot();
// ✅ GOOD: Wait for stability
await page.goto('/');
await page.waitForLoadState('networkidle'); // Wait for network idle
await page.waitForSelector('.main-content'); // Wait for key element
await page.evaluate(() => document.fonts.ready); // Wait for fonts
// Disable animations for consistent screenshots
await page.addStyleTag({
content: `
*, *::before, *::after {
animation-duration: 0s !important;
transition-duration: 0s !important;
}
`,
});
await expect(page).toHaveScreenshot();
2. Mask Dynamic Content
await expect(page).toHaveScreenshot({
mask: [
page.locator('.timestamp'), // Changes every second
page.locator('.user-id'), // Different per user
page.locator('[data-dynamic="true"]'), // Marked as dynamic
page.locator('video'), // Video frames vary
],
});
3. Use Meaningful Names
// ❌ BAD: Generic names
await expect(page).toHaveScreenshot('test1.png');
// ✅ GOOD: Descriptive names
await expect(page).toHaveScreenshot('homepage-logged-in-user.png');
await expect(page).toHaveScreenshot('checkout-empty-cart-error.png');
4. Test Critical Paths Only
Visual regression tests are expensive (slow, storage). Prioritize:
// ✅ High Priority (critical user flows)
- Homepage (first impression)
- Checkout flow (revenue-critical)
- Login/signup (user acquisition)
- Product details (conversion)
// ⚠️ Medium Priority (important but not critical)
- Profile settings
- Search results
- Category pages
// ❌ Low Priority (skip or sample)
- Admin dashboards (internal users)
- Footer (rarely changes)
- Legal pages
5. Baseline Management Strategy
When to Update Baselines:
- ✅ Intentional design changes (approved by design team)
- ✅ Component library upgrades (reviewed)
- ✅ Browser updates (expected differences)
- ❌ Unintentional changes (investigate first!)
# Review diff report BEFORE approving
npx playwright test --update-snapshots # Use carefully!
# Better: Update selectively
npx playwright test homepage.spec.ts --update-snapshots
Debugging Visual Diffs
1. Review Diff Report
Playwright generates HTML report with side-by-side comparison:
npx playwright test
# On failure, opens: playwright-report/index.html
# Shows: Expected | Actual | Diff (highlighted pixels)
2. Adjust Thresholds
// Tolerate minor differences (anti-aliasing, font rendering)
await expect(page).toHaveScreenshot({
maxDiffPixelRatio: 0.02, // 2% tolerance
});
3. Ignore Specific Regions
// Ignore regions that legitimately differ
await expect(page).toHaveScreenshot({
mask: [page.locator('.animated-banner')],
clip: { x: 0, y: 0, width: 800, height: 600 }, // Capture specific area
});
CI/CD Integration
1. GitHub Actions (Playwright Snapshots)
name: Visual Regression Tests
on:
pull_request:
branches: [main]
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npx playwright install --with-deps
- name: Run visual tests
run: npx playwright test
- name: Upload diff report
if: failure()
uses: actions/upload-artifact@v3
with:
name: visual-diff-report
path: playwright-report/
2. Baseline Storage Strategies
Option 1: Git LFS (Large File Storage)
- Store baselines in Git (versioned with code)
- Use Git LFS to avoid bloating repository
- Automatic sync across developers
# .gitattributes
*.png filter=lfs diff=lfs merge=lfs -text
git lfs install
git add tests/__screenshots__/*.png
git commit -m "Add visual baselines"
Option 2: Cloud Storage (S3, GCS)
- Store baselines in cloud bucket
- Download in CI before test
- Faster CI (no Git LFS checkout)
- name: Download baselines
run: aws s3 sync s3://my-bucket/baselines tests/__screenshots__/
Option 3: Percy/Chromatic (Managed)
- Baselines stored in service (no Git needed)
- Automatic baseline management
- UI for reviewing changes
3. Handling Baseline Drift
Problem: Developer A updates baselines, Developer B's tests fail.
Solution 1: Require baseline review
# PR merge rules
- Require approval for changes in tests/__screenshots__/
Solution 2: Auto-update in CI
- name: Update baselines if approved
if: contains(github.event.pull_request.labels.*.name, 'update-baselines')
run: |
npx playwright test --update-snapshots
git config user.name "GitHub Actions"
git add tests/__screenshots__/
git commit -m "Update visual baselines"
git push
Common Pitfalls
1. Flaky Tests Due to Animations
❌ Bad:
await page.goto('/'); // Page has CSS animations
await expect(page).toHaveScreenshot(); // Fails randomly (mid-animation)
✅ Good:
await page.goto('/');
await page.addStyleTag({ content: '* { animation: none !important; }' });
await expect(page).toHaveScreenshot();
2. Font Loading Issues
❌ Bad:
await page.goto('/'); // Fonts loading async
await expect(page).toHaveScreenshot(); // Sometimes uses fallback font
✅ Good:
await page.goto('/');
await page.evaluate(() => document.fonts.ready); // Wait for fonts
await expect(page).toHaveScreenshot();
3. Testing Everything (Slow CI)
❌ Bad: 500 visual tests (30 min CI time) ✅ Good: 50 critical visual tests (5 min CI time)
Optimize:
// Run visual tests only on visual changes
if (changedFiles.some(file => file.endsWith('.css'))) {
runVisualTests();
}
4. Platform Differences (macOS vs Linux)
Problem: Screenshots differ between macOS (local) and Linux (CI).
Solution: Use Docker for local development
# Local development with Docker
docker run -it --rm -v $(pwd):/work -w /work mcr.microsoft.com/playwright:v1.40.0-focal npx playwright test
Advanced Techniques
1. Visual Regression for Emails
test('email template visual test', async ({ page }) => {
const emailHtml = await generateEmailTemplate({ userName: 'John', orderTotal: '$99.99' });
await page.setContent(emailHtml);
await expect(page).toHaveScreenshot('order-confirmation-email.png');
});
2. PDF Visual Testing
test('invoice PDF visual test', async ({ page }) => {
await page.goto('/invoice/123');
const pdfBuffer = await page.pdf({ format: 'A4' });
// Convert PDF to image and compare
const pdfImage = await pdfToImage(pdfBuffer);
expect(pdfImage).toMatchSnapshot('invoice.png');
});
3. A/B Test Visual Variants
test('A/B test variant visual comparison', async ({ page }) => {
// Test control variant
await page.goto('/?variant=control');
await expect(page).toHaveScreenshot('homepage-control.png');
// Test experiment variant
await page.goto('/?variant=experiment');
await expect(page).toHaveScreenshot('homepage-experiment.png');
// Manual review to ensure both look good
});
Resources
Activation Keywords
Ask me about:
- "How to set up visual regression testing"
- "Playwright screenshot testing"
- "Percy vs Chromatic comparison"
- "Visual testing for components"
- "How to fix flaky visual tests"
- "Managing visual baselines in CI"
- "Cross-browser visual testing"
- "Screenshot comparison best practices"
- "Visual regression CI integration"
Repository
