Skip to main content

Visual Testing with Playwright Snapshots

Playwright includes built-in screenshot comparison via toHaveScreenshot(). It's free, stores snapshots in your repository, and integrates naturally with your existing Playwright tests.

Setting Up Snapshots

No additional configuration is required beyond a standard Playwright setup. The snapshot assertion is available out of the box:

import { test, expect } from '@playwright/test'

test('homepage looks correct', async ({ page }) => {
  await page.goto('/')
  await expect(page).toHaveScreenshot('homepage.png')
})

On the first run, Playwright saves a baseline screenshot. On subsequent runs, it compares the current screenshot against the baseline and fails if they differ beyond a threshold.

Generating Baselines

Run your tests with --update-snapshots to generate or update baselines:

npx playwright test --update-snapshots

This creates snapshot files in a __snapshots__ directory next to your test file. Commit these files to your repository — they are the source of truth for what the UI should look like.

Handling Acceptable Differences

Screenshots can differ legitimately — different operating systems render fonts slightly differently, antialiasing varies, and animations may be at different frames. Playwright handles this with a pixel threshold:

await expect(page).toHaveScreenshot('homepage.png', {
  maxDiffPixelRatio: 0.01 // Allow up to 1% pixel difference
})

You can also mask dynamic regions (timestamps, user avatars, ads) that legitimately change between runs:

await expect(page).toHaveScreenshot('dashboard.png', {
  mask: [page.locator('.timestamp'), page.locator('.user-avatar')]
})

Component-Level Snapshots

Full-page screenshots are noisy — a change anywhere on the page causes a failure. Component-level snapshots are more focused and easier to maintain:

test('button component variants', async ({ page }) => {
  await page.goto('/components/button')
  
  const primaryButton = page.getByRole('button', { name: 'Primary' })
  await expect(primaryButton).toHaveScreenshot('button-primary.png')
  
  const secondaryButton = page.getByRole('button', { name: 'Secondary' })
  await expect(secondaryButton).toHaveScreenshot('button-secondary.png')
})

Snapshot Testing Strategy

Test what matters visually: Not every page needs snapshot tests. Prioritise pages and components where visual correctness is critical — landing pages, marketing components, design system components.

Keep snapshots in CI: Run snapshot tests in CI to catch regressions before merge. Developers update snapshots locally when making intentional changes.

Review snapshot diffs carefully: When a snapshot test fails, look at the diff image before deciding whether to update the baseline. A small diff might be a real regression; a large diff might be an intentional redesign.