Claude Code Plugins

Community-maintained marketplace

Feedback

visual-regression

@anton-abyzov/specweave
11
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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.

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"