Part 05 · Operations & Advanced Card 27
Visual Regression
Card 27: Visual Regression with toHaveScreenshot
What This Pattern Solves
Card 18 introduced a single component screenshot, but real suites need more: handling OS-level anti-aliasing differences, masking volatile content, tuning sensitivity with threshold and maxDiffPixels, and applying shared CSS. This card covers the full toHaveScreenshot depth.
How It Works
- The
describeblock is tagged{ tag: '@visual' }so visual tests can be selected (or excluded) as a group in CI. - A
beforeEachmocks the API deterministically withmakePerson, then injects a stylesheet that disables transitions/animations so screenshots are stable. PersonPage.open(page, '1', '/cards/18')loads the page, andpersonCardLocator(page, '1')scopes to the card.- The first test takes an element-level screenshot of the person card.
- The second test uses
maxDiffPixelsandthresholdto tolerate minor rendering differences across machines. - The third test reuses the same golden with a
maxDiffPixelstolerance.
The update workflow: delete the stale snapshot (or run with --update-snapshots) and Playwright writes a new golden image on the next run.
Code Example
import { test, expect } from '@playwright/test';
import { personCardLocator } from '../e2e-patterns/person/locators';
import { PersonPage } from '../e2e-patterns/person/PersonPage';
import { makePerson } from '../swapi/builders';
test.describe(
'27-visual-regression: toHaveScreenshot depth: masking, threshold, stylePath',
{ tag: '@visual' },
() => {
test.beforeEach(async ({ page }) => {
await page.route('**/swapi.dev/api/people/1/**', (route) =>
route.fulfill({
json: makePerson({
name: 'Luke Skywalker',
height: '172',
mass: '77',
url: 'https://swapi.dev/api/people/1/',
}),
}),
);
await page.addInitScript(() => {
const css =
'* { transition: none !important; animation: none !important; }';
const style = document.createElement('style');
style.textContent = css;
(document.head ?? document.documentElement).appendChild(style);
});
});
test('element-level screenshot of the person card', async ({ page }) => {
await PersonPage.open(page, '1', '/cards/18');
const card = personCardLocator(page, '1');
await expect(card).toBeVisible();
await expect(card).toHaveScreenshot('person-card-element.png');
});
test('lenient comparison with maxDiffPixels and threshold', async ({
page,
}) => {
await PersonPage.open(page, '1', '/cards/18');
const card = personCardLocator(page, '1');
await expect(card).toBeVisible();
await expect(card).toHaveScreenshot('person-card-lenient.png', {
maxDiffPixels: 100, // tolerate up to 100 differing pixels
threshold: 0.3, // each pixel can differ by up to 30%
});
});
},
);
Run This Example
pnpm test src/27-visual-regression
pnpm test src/27-visual-regression --update-snapshots
Prerequisites
- Card 18: First component screenshot (
toHaveScreenshotbasics). - Card 12/26: PersonPage and page object patterns.
Key Concepts
- maxDiffPixels: Absolute number of pixels allowed to differ.
- threshold: Per-pixel tolerance (0-1). 0.1 means 10% allowed difference.
- mask: Array of
{ x, y, width, height }rectangles. Content inside is ignored. - stylePath: Path to a CSS file injected before the screenshot.
--update-snapshots: CLI flag that overwrites golden images.
Common Mistakes
- Not masking volatile content (timestamps, random data) — they fail every run.
- Committing stale snapshots without reviewing the diff.
- Running visual tests on a different OS than CI (use
threshold).
Related Patterns
- Previous: Card 18 (Stability Techniques)
- Next: Card 28 (Component Testing)
- Complementary: Card 26 (Full Architecture)
Live Demo
👇 This component is what the Playwright test interacts with:
Loading…
Run This Example
pnpm test src/27-visual-regression