Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
testing-patterns/visual-regression.md
1# Visual Regression Testing23## Table of Contents451. [Quick Reference](#quick-reference)62. [Patterns](#patterns)73. [Decision Guide](#decision-guide)84. [Anti-Patterns](#anti-patterns)95. [Troubleshooting](#troubleshooting)1011> **When to use**: Detecting unintended visual changes—layout shifts, style regressions, broken responsive designs—that functional assertions miss.1213## Quick Reference1415```typescript16// Element screenshot17await expect(page.getByTestId('product-card')).toHaveScreenshot();1819// Full page screenshot20await expect(page).toHaveScreenshot('landing-hero.png');2122// Threshold for minor pixel variance23await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.01 });2425// Mask volatile content26await expect(page).toHaveScreenshot({27mask: [page.getByTestId('clock'), page.getByRole('img', { name: 'User photo' })],28});2930// Disable CSS animations31await expect(page).toHaveScreenshot({ animations: 'disabled' });3233// Update baselines34npx playwright test --update-snapshots35```3637## Patterns3839### Masking Volatile Content4041**Use when**: Page contains timestamps, avatars, ad slots, relative dates, random images, or A/B variants.4243The `mask` option overlays a solid box over specified locators before capturing.4445```typescript46test('analytics panel with masked dynamic elements', async ({ page }) => {47await page.goto('/analytics');4849await expect(page).toHaveScreenshot('analytics.png', {50mask: [51page.getByTestId('last-updated'),52page.getByTestId('profile-avatar'),53page.getByTestId('active-users'),54page.locator('.promo-banner'),55],56maskColor: '#FF00FF',57});58});5960test('activity stream with relative times', async ({ page }) => {61await page.goto('/activity');6263await expect(page).toHaveScreenshot('activity.png', {64mask: [page.locator('time[datetime]')],65});66});67```6869**Alternative: freeze content with JavaScript** when masking affects layout:7071```typescript72test('freeze timestamps before capture', async ({ page }) => {73await page.goto('/analytics');7475await page.evaluate(() => {76document.querySelectorAll('[data-testid="time-display"]').forEach((el) => {77el.textContent = 'Jan 1, 2025 12:00 PM';78});79});8081await expect(page).toHaveScreenshot('analytics-frozen.png');82});83```8485### Disabling Animations8687**Use when**: Always. CSS animations and transitions are the primary cause of flaky visual diffs.8889```typescript90test('renders without animation interference', async ({ page }) => {91await page.goto('/');9293await expect(page).toHaveScreenshot('home.png', {94animations: 'disabled',95});96});97```9899**Set globally** in config:100101```typescript102// playwright.config.ts103export default defineConfig({104expect: {105toHaveScreenshot: {106animations: 'disabled',107},108},109});110```111112When `animations: 'disabled'` is set, Playwright injects CSS forcing animation/transition duration to 0s, waits for running animations to finish, then captures.113114For JavaScript-driven animations (GSAP, Framer Motion), wait for stability:115116```typescript117test('page with JS animations', async ({ page }) => {118await page.goto('/animated-hero');119120const heroBanner = page.getByTestId('hero-banner');121await heroBanner.waitFor({ state: 'visible' });122123// Wait for animation to complete by checking for stable state124await expect(heroBanner).not.toHaveClass(/animating/);125126await expect(page).toHaveScreenshot('hero.png', {127animations: 'disabled',128});129});130```131132### Configuring Thresholds133134**Use when**: Minor rendering differences from anti-aliasing, font hinting, or sub-pixel rendering cause false failures.135136| Option | Controls | Typical Value |137|---|---|---|138| `maxDiffPixels` | Absolute pixel count that can differ | `100` for pages, `10` for components |139| `maxDiffPixelRatio` | Fraction of total pixels (0-1) | `0.01` (1%) for pages |140| `threshold` | Per-pixel color tolerance (0-1) | `0.2` for most UIs, `0.1` for design systems |141142```typescript143test('control panel allows minor variance', async ({ page }) => {144await page.goto('/control-panel');145146await expect(page).toHaveScreenshot('control-panel.png', {147maxDiffPixelRatio: 0.01,148});149});150151test('brand logo renders pixel-perfect', async ({ page }) => {152await page.goto('/brand');153154await expect(page.getByTestId('brand-logo')).toHaveScreenshot('brand-logo.png', {155maxDiffPixels: 0,156threshold: 0,157});158});159160test('graph allows anti-aliasing differences', async ({ page }) => {161await page.goto('/reports');162163await expect(page.getByTestId('sales-graph')).toHaveScreenshot('sales-graph.png', {164threshold: 0.3,165maxDiffPixels: 200,166});167});168```169170**Global thresholds** in config:171172```typescript173// playwright.config.ts174export default defineConfig({175expect: {176toHaveScreenshot: {177maxDiffPixelRatio: 0.01,178threshold: 0.2,179animations: 'disabled',180},181},182});183```184185### CI Configuration186187**Use when**: Running visual tests in CI. Consistent rendering is critical—the same test must produce identical screenshots every time.188189**The problem**: Font rendering and anti-aliasing differ across operating systems. macOS snapshots won't match Linux.190191**The solution**: Run visual tests in Docker using the official Playwright container. Generate and update snapshots from the same container.192193**GitHub Actions with Docker**194195```yaml196# .github/workflows/visual-tests.yml197name: Visual Regression Tests198on: [push, pull_request]199200jobs:201visual-tests:202runs-on: ubuntu-latest203container:204image: mcr.microsoft.com/playwright:v1.48.0-noble205steps:206- uses: actions/checkout@v4207208- uses: actions/setup-node@v4209with:210node-version: lts/*211cache: npm212213- run: npm ci214215- name: Run visual tests216run: npx playwright test --project=visual217env:218HOME: /root219220- uses: actions/upload-artifact@v4221if: failure()222with:223name: visual-test-report224path: playwright-report/225retention-days: 14226```227228**Updating snapshots locally using Docker**:229230```bash231docker run --rm -v $(pwd):/work -w /work \232mcr.microsoft.com/playwright:v1.48.0-noble \233npx playwright test --update-snapshots --project=visual234```235236**Add script to `package.json`**:237238```json239{240"scripts": {241"test:visual": "npx playwright test --project=visual",242"test:visual:update": "docker run --rm -v $(pwd):/work -w /work mcr.microsoft.com/playwright:v1.48.0-noble npx playwright test --update-snapshots --project=visual"243}244}245```246247**Platform-agnostic snapshots** (requires Docker for generation):248249```typescript250// playwright.config.ts251export default defineConfig({252snapshotPathTemplate: '{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{ext}',253projects: [254{255name: 'visual',256testMatch: '**/*.visual.spec.ts',257use: { ...devices['Desktop Chrome'] },258},259],260});261```262263### Full Page vs Element Screenshots264265**Use when**: Deciding scope. Full page catches layout shifts. Element screenshots isolate components and are more stable.266267```typescript268test('full page captures layout shifts', async ({ page }) => {269await page.goto('/');270271// Visible viewport272await expect(page).toHaveScreenshot('home-viewport.png');273274// Entire scrollable page275await expect(page).toHaveScreenshot('home-full.png', {276fullPage: true,277});278});279280test('element screenshot isolates component', async ({ page }) => {281await page.goto('/catalog');282283await expect(page.getByRole('table')).toHaveScreenshot('catalog-table.png');284await expect(page.getByTestId('featured-item')).toHaveScreenshot('featured-item.png');285});286```287288**Rule of thumb**: Element screenshots for independently changing components. Full page screenshots for key layouts where spacing matters.289290### Responsive Visual Testing291292**Use when**: Application has responsive breakpoints requiring verification at different viewport sizes.293294```typescript295const breakpoints = [296{ name: 'phone', width: 375, height: 812 },297{ name: 'tablet', width: 768, height: 1024 },298{ name: 'desktop', width: 1440, height: 900 },299];300301for (const bp of breakpoints) {302test(`landing at ${bp.name} (${bp.width}x${bp.height})`, async ({ page }) => {303await page.setViewportSize({ width: bp.width, height: bp.height });304await page.goto('/');305306await expect(page).toHaveScreenshot(`landing-${bp.name}.png`, {307animations: 'disabled',308fullPage: true,309});310});311}312```313314**Alternative: use projects for responsive testing**:315316```typescript317// playwright.config.ts318export default defineConfig({319projects: [320{321name: 'desktop',322testMatch: '**/*.visual.spec.ts',323use: {324...devices['Desktop Chrome'],325viewport: { width: 1440, height: 900 },326},327},328{329name: 'tablet',330testMatch: '**/*.visual.spec.ts',331use: { ...devices['iPad (gen 7)'] },332},333{334name: 'mobile',335testMatch: '**/*.visual.spec.ts',336use: { ...devices['iPhone 14'] },337},338],339});340```341342### Component Visual Testing343344**Use when**: Testing individual UI components in isolation—buttons, cards, forms, modals. Faster and more stable than full-page screenshots.345346```typescript347test.describe('Button visual states', () => {348test('primary button', async ({ page }) => {349await page.goto('/storybook/iframe.html?id=button--primary');350const btn = page.getByRole('button');351await expect(btn).toHaveScreenshot('btn-primary.png', {352animations: 'disabled',353});354});355356test('primary button hover', async ({ page }) => {357await page.goto('/storybook/iframe.html?id=button--primary');358const btn = page.getByRole('button');359await btn.hover();360await expect(btn).toHaveScreenshot('btn-primary-hover.png', {361animations: 'disabled',362});363});364365test('button sizes', async ({ page }) => {366for (const size of ['small', 'medium', 'large']) {367await page.goto(`/storybook/iframe.html?id=button--${size}`);368const btn = page.getByRole('button');369await expect(btn).toHaveScreenshot(`btn-${size}.png`, {370animations: 'disabled',371});372}373});374});375```376377**Using a dedicated test harness** instead of Storybook:378379```typescript380test.describe('Card component', () => {381test.beforeEach(async ({ page }) => {382await page.goto('/test-harness/card');383});384385test('default state', async ({ page }) => {386await expect(page.getByTestId('card')).toHaveScreenshot('card-default.png', {387animations: 'disabled',388});389});390391test('truncates long content', async ({ page }) => {392await page.goto('/test-harness/card?content=long');393await expect(page.getByTestId('card')).toHaveScreenshot('card-long.png', {394animations: 'disabled',395});396});397});398```399400### Updating Snapshots401402**Use when**: Intentionally changed UI—design refresh, rebrand, new feature. Never update when diff is unexpected.403404```bash405# Update all snapshots406npx playwright test --update-snapshots407408# Update for specific file409npx playwright test tests/landing.spec.ts --update-snapshots410411# Update for specific project412npx playwright test --project=chromium --update-snapshots413```414415**Workflow for reviewing changes:**4164171. Run tests and view failures in HTML report:418```bash419npx playwright test420npx playwright show-report421```422The report shows expected, actual, and diff images side-by-side.4234242. If changes are intentional, update:425```bash426npx playwright test --update-snapshots427```4284293. Review updated snapshots before committing:430```bash431git diff --name-only432```433434**Tag visual tests for selective updates:**435436```typescript437test('landing visual @visual', async ({ page }) => {438await page.goto('/');439await expect(page).toHaveScreenshot('landing.png', {440animations: 'disabled',441});442});443```444445```bash446npx playwright test --grep @visual --update-snapshots447```448449### Cross-Browser Visual Testing450451**Use when**: Users span Chrome, Firefox, Safari and you need per-browser rendering verification.452453Playwright separates snapshots by project name automatically. Each browser gets its own baseline—browsers render fonts and shadows differently.454455```typescript456// playwright.config.ts457export default defineConfig({458expect: {459toHaveScreenshot: {460animations: 'disabled',461maxDiffPixelRatio: 0.01,462},463},464projects: [465{466name: 'chromium',467use: { ...devices['Desktop Chrome'] },468},469{470name: 'firefox',471use: { ...devices['Desktop Firefox'] },472},473{474name: 'webkit',475use: { ...devices['Desktop Safari'] },476},477],478});479```480481**Strategy**: Run visual tests in a single browser (Chromium on Linux in CI) to minimize snapshot count. Add other browsers only when you have actual cross-browser rendering bugs:482483```typescript484// playwright.config.ts485export default defineConfig({486projects: [487{488name: 'visual',489testMatch: '**/*.visual.spec.ts',490use: { ...devices['Desktop Chrome'] },491},492{493name: 'chromium',494testIgnore: '**/*.visual.spec.ts',495use: { ...devices['Desktop Chrome'] },496},497{498name: 'firefox',499testIgnore: '**/*.visual.spec.ts',500use: { ...devices['Desktop Firefox'] },501},502],503});504```505506## Decision Guide507508| Scenario | Approach | Rationale |509|---|---|---|510| Key landing/marketing pages | Full page, `fullPage: true` | Catches layout shifts, spacing, overall harmony |511| Individual components | Element screenshot | Isolated, fast, immune to unrelated changes |512| Page with dynamic content | Full page + `mask` | Covers layout while ignoring volatile content |513| Design system library | Element per variant, zero threshold | Pixel-perfect enforcement |514| Responsive verification | Screenshot per viewport | Catches breakpoint bugs |515| Cross-browser consistency | Separate snapshots per browser | Browsers render differently |516| CI pipeline | Docker container, Linux-only snapshots | Consistent rendering |517| Threshold: design system | `threshold: 0`, `maxDiffPixels: 0` | Zero tolerance |518| Threshold: content pages | `maxDiffPixelRatio: 0.01`, `threshold: 0.2` | Minor anti-aliasing variance |519| Threshold: charts/graphs | `maxDiffPixels: 200`, `threshold: 0.3` | Anti-aliasing on curves varies |520521## Anti-Patterns522523| Don't | Problem | Do Instead |524|---|---|---|525| Visual test every page | Massive maintenance, constant false failures | Pick 5-10 key pages and critical components |526| Skip masking dynamic content | Screenshots differ every run, permanently flaky | Use `mask` for all volatile elements |527| Run across macOS, Linux, Windows | Font rendering differs, snapshots never match | Standardize on Linux via Docker |528| Skip Docker in CI | OS updates shift rendering silently | Pin specific Playwright Docker image |529| Blindly run `--update-snapshots` | Accepts unintentional regressions | Always review diff in HTML report first |530| Skip `animations: 'disabled'` | CSS transitions create random diffs | Set globally in config |531| Replace functional assertions with visual tests | Diffs don't tell you *what* broke | Visual tests complement, never replace |532| Commit snapshots from different platforms | Tests fail for everyone | All team members use same Docker container |533| Set threshold too high (`0.1`) | 10% pixel change passes, defeats purpose | Start with `0.01`, adjust per-test |534| Full page on infinite scroll pages | Page height nondeterministic | Element screenshots on above-the-fold content |535536## Troubleshooting537538### "Screenshot comparison failed" on first CI run after local development539540**Cause**: Snapshots generated on macOS locally. CI runs on Linux. Font rendering differs.541542**Fix**: Generate snapshots using Docker:543544```bash545docker run --rm -v $(pwd):/work -w /work \546mcr.microsoft.com/playwright:v1.48.0-noble \547npx playwright test --update-snapshots --project=visual548```549550Commit Linux-generated snapshots.551552### "Expected screenshot to match but X pixels differ"553554**Cause**: Anti-aliasing, font hinting, sub-pixel rendering differences.555556**Fix**: Add tolerance:557558```typescript559await expect(page).toHaveScreenshot('page.png', {560maxDiffPixelRatio: 0.01,561threshold: 0.2,562});563```564565Check HTML report diff image to determine if it's regression or noise.566567### Visual tests pass locally but fail in CI (even with Docker)568569**Cause**: Different Playwright versions locally vs CI.570571**Fix**: Ensure `package.json` version matches Docker image tag:572573```json574{575"devDependencies": {576"@playwright/test": "latest"577}578}579```580581```yaml582container:583image: mcr.microsoft.com/playwright:v1.48.0-noble584```585586### Animations cause random diff failures587588**Cause**: CSS animations captured mid-frame.589590**Fix**: Set `animations: 'disabled'` globally:591592```typescript593// playwright.config.ts594export default defineConfig({595expect: {596toHaveScreenshot: {597animations: 'disabled',598},599},600});601```602603For JS animations, wait for stable state before capture.604605### Snapshot file names conflict between tests606607**Cause**: Two tests use same screenshot name without unique paths.608609**Fix**: Use explicit unique names:610611```typescript612await expect(page).toHaveScreenshot('auth-home.png');613await expect(page).toHaveScreenshot('public-home.png');614```615616Or customize snapshot path template:617618```typescript619export default defineConfig({620snapshotPathTemplate: '{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{ext}',621});622```623624### Too many snapshot files to maintain625626**Cause**: Visual tests for every page, browser, viewport.627628**Fix**: Be selective. Visual test only high-risk pages:629- Landing and marketing pages630- Design system components631- Complex layouts (dashboards, data tables)632- Pages after major refactor633634Skip pages where functional assertions cover key elements.635