Skip to main content
CodeRocket
Testingmediumvisual

Use visual regression testing

rule · visual-regression

Visual regression testing screenshots your app and compares against approved baselines, failing when pixels change unexpectedly.

Code Example

JavaScript
// tests/visual/homepage.spec.js
import { test, expect } from '@playwright/test'
 
test('homepage looks correct', async ({ page }) => {
  await page.goto('/')
  await page.waitForLoadState('networkidle')
 
  // Full page screenshot
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixelRatio: 0.01 // Allow 1% pixel difference (anti-aliasing, etc.)
  })
})
 
test('button states', async ({ page }) => {
  await page.goto('/components/buttons')
 
  // Component-level screenshot
  const button = page.locator('.button--primary')
  await expect(button).toHaveScreenshot('button-default.png')
 
  await button.hover()
  await expect(button).toHaveScreenshot('button-hover.png')
})

Why It Matters

Functional tests verify behavior but don't see your page. A CSS refactor might pass all unit and integration tests while completely breaking the layout — text overlapping images, buttons invisible, wrong colors. Visual regression testing acts as a safety net that reviews your actual rendered output, catching changes that no amount of JavaScript testing can detect.

Updating Baselines

Shell
# First run creates baselines
npx playwright test --update-snapshots
 
# Subsequent runs compare against baselines
npx playwright test
 
# Update specific test's baseline
npx playwright test homepage.spec.js --update-snapshots

Storybook + Chromatic

For component libraries, Storybook + Chromatic provides the most streamlined visual testing workflow:

JavaScript
// Button.stories.js
export default {
  title: 'Components/Button',
  component: Button,
}
 
export const Primary = {
  args: { variant: 'primary', children: 'Click me' }
}
 
export const Disabled = {
  args: { variant: 'primary', disabled: true, children: 'Disabled' }
}
 
export const Loading = {
  args: { variant: 'primary', loading: true, children: 'Loading' }
}
YAML
# CI: .github/workflows/visual.yml
- name: Publish to Chromatic
  uses: chromaui/action@v1
  with:
    projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
    onlyChanged: true # Only test changed components

Handling Dynamic Content

JavaScript
test('dashboard with dynamic content', async ({ page }) => {
  await page.goto('/dashboard')
 
  // Mask dynamic values before screenshotting
  await page.addStyleTag({
    content: `
      .timestamp, .chart-data, .user-avatar { visibility: hidden; }
    `
  })
 
  await expect(page).toHaveScreenshot('dashboard-layout.png')
})

CI Configuration

YAML
# GitHub Actions
- name: Run visual tests
  run: npx playwright test tests/visual/
  env:
    CI: true
 
- name: Upload diff artifacts on failure
  if: failure()
  uses: actions/upload-artifact@v3
  with:
    name: visual-diff
    path: test-results/

Standards

  • Use these references as the standard for how the test or monitoring strategy should behave in the shipped workflow.
  • Check the implementation against Playwright Docs before treating the rule as satisfied.
  • Check the implementation against Testing Library Guiding Principles before treating the rule as satisfied.

Verification

Automated Checks

  • Run the relevant test or CI step locally and confirm it fails when the rule is violated.

Manual Checks

  • Ensure the automation blocks regressions instead of only printing warnings.
  • Cover at least one representative high-risk flow, component, or route.
  • Keep thresholds or assertions in version control so changes remain reviewable.