PW Visual Regression
You set up snapshot tests whose first baselines the engineer must
generate and eyeball — never trust an auto-approved baseline. You make
snapshots deterministic, and visual assertions complement functional ones
rather than replacing them.
When to use
- A page or component needs pixel/visual regression coverage.
- Flaky snapshot diffs need masking or threshold tuning.
- Someone says "add visual regression", "manage/update baselines", "why
does my screenshot test keep failing".
When not to use
- Generating the underlying Playwright test →
pw-test-generator.
- Fixing a brittle locator used to find the screenshot target →
pw-locator-fixer.
- Diagnosing general test flakiness →
pw-flaky-debugger.
- Analyzing a trace →
pw-trace-analyzer.
- Controlling network responses that feed the page →
pw-network-mocker.
- Building the Page Object or fixture a visual test reuses →
pw-page-object-builder / pw-fixture-designer (reuse what exists;
don't design new ones here).
Don't use visual regression as just another way to assert functional
behavior — keep a functional assertion (toBeVisible, toHaveText) for
behavior, and the screenshot for appearance.
Language and project conventions
Support both JavaScript and TypeScript — preserve the project's
existing language and conventions; never convert between them, and never
introduce TypeScript syntax into JavaScript output.
Workflow
- Pick the smallest stable target — an element or component locator
over a full page where possible; less surface means fewer false diffs.
- Reach a deterministic state before snapping. Wait on a web-first
assertion (
await expect(heading).toBeVisible()) first — never add an
arbitrary delay because a screenshot occasionally differs.
- Control rendering conditions as tightly as practical: viewport,
device scale factor, fonts, OS/browser version, locale, timezone, color
scheme, reduced-motion settings, and application data. The more these
vary between baseline generation and comparison, the less reliable the
result — generate baselines in the CI environment, not just locally.
- Neutralize dynamic content — timestamps, avatars, ads, live
counters, loading indicators, rotating content:
mask only the smallest region that's actually dynamic; never mask
content the test is meant to validate, and never mask a large area
just to make a diff go away.
animations: 'disabled' unless the animation itself is what the test
validates — disabling it there would defeat the test's purpose.
- Prefer controlled/deterministic test data over live or random data.
- Never invent a selector, test ID, or masked region that doesn't exist.
- Set thresholds deliberately (
maxDiffPixels / maxDiffPixelRatio /
threshold), not sprinkled ad hoc and never bumped just to make a
failing test pass. Before adjusting one: inspect the actual diff,
determine why the pixels differ, decide whether that difference is
meaningful, and only then adjust — a large tolerance can hide a real
regression.
- Classify every screenshot failure before touching a baseline:
- Expected change — an intentional UI change; update the baseline
after verifying it.
- Unexpected regression — don't update the baseline; report the bug.
- Test instability — e.g. a timestamp differs between runs;
stabilize or mask the dynamic region instead of updating.
- Environment difference — e.g. font rendering differs by platform;
standardize the rendering environment instead of accepting the diff.
- Generate baselines intentionally via
--update-snapshots, then
review each PNG by eye before committing — an unreviewed baseline
can lock in a bug forever. Document the update flow so baselines are
refreshed on purpose, per platform.
Output format
- Visual target — what's being compared.
- Stable state — how the test reaches the intended UI state before
snapping.
- Screenshot test — the Playwright code.
- Dynamic content — what needs stabilizing or masking.
- Baseline strategy — when/how the baseline is created or updated.
- Tolerance — thresholds, only when there's a clear reason for them.
- Verification — how to review a failure and tell an expected change
from a regression, instability, or an environment difference.
Example
import { test, expect } from '@playwright/test';
test('dashboard card matches baseline', async ({ page }) => {
await page.goto('/dashboard');
const card = page.getByTestId('summary-card');
await expect(card).toBeVisible(); // web-first: wait for render
await expect(card).toHaveScreenshot('summary-card.png', {
animations: 'disabled',
mask: [page.getByTestId('last-updated')], // hide volatile timestamp
maxDiffPixelRatio: 0.01,
});
});
# generate/refresh baselines, then review the PNGs before committing
npx playwright test --update-snapshots
Guardrails
- Baselines are generated then human-reviewed — never auto-approve;
never update one merely because a test failed.
- Never invent a route, selector, test ID, accessible name, application
data, or baseline file that doesn't exist.
- Never use a large tolerance, or
waitForTimeout(), to hide an
unstable or unreviewed diff.
- Don't mask large areas unnecessarily, and never mask content the test is
actually meant to validate.
- Don't replace a functional assertion with a screenshot — keep both.
- Investigate why a diff occurred before changing a threshold or a
baseline; prefer the smallest screenshot target that gives meaningful
coverage.
- Keep rendering conditions consistent where practical (viewport, fonts,
OS/browser, locale, timezone, color scheme).
- Reuse existing Page Objects and fixtures rather than designing new ones
here — that belongs to
pw-page-object-builder / pw-fixture-designer.
- Preserve the project's JS/TS convention; never convert between them.
- State assumptions explicitly when the UI or rendering environment can't
be verified.
1---2name: pw-visual-regression3description: Sets up and reviews Playwright visual/screenshot regression testing — JavaScript or TypeScript — to catch meaningful visual regressions while minimizing false positives from nondeterministic rendering or dynamic content. Use when an SDET says "add visual regression", "snapshot this component", "set up toHaveScreenshot", "mask the dynamic parts of this page", "why does my screenshot test keep failing", or "manage baselines". Produces snapshot tests with masking, thresholds, and a baseline strategy — a draft the engineer runs to generate and review the first baselines.4license: MIT5---67# PW Visual Regression89You set up **snapshot tests whose first baselines the engineer must10generate and eyeball** — never trust an auto-approved baseline. You make11snapshots deterministic, and visual assertions complement functional ones12rather than replacing them.1314## When to use15- A page or component needs pixel/visual regression coverage.16- Flaky snapshot diffs need masking or threshold tuning.17- Someone says "add visual regression", "manage/update baselines", "why18 does my screenshot test keep failing".1920## When *not* to use21- Generating the underlying Playwright test → `pw-test-generator`.22- Fixing a brittle locator used to find the screenshot target →23 `pw-locator-fixer`.24- Diagnosing general test flakiness → `pw-flaky-debugger`.25- Analyzing a trace → `pw-trace-analyzer`.26- Controlling network responses that feed the page → `pw-network-mocker`.27- Building the Page Object or fixture a visual test reuses →28 `pw-page-object-builder` / `pw-fixture-designer` (reuse what exists;29 don't design new ones here).3031Don't use visual regression as just another way to assert functional32behavior — keep a functional assertion (`toBeVisible`, `toHaveText`) for33behavior, and the screenshot for appearance.3435## Language and project conventions36Support both **JavaScript and TypeScript** — preserve the project's37existing language and conventions; never convert between them, and never38introduce TypeScript syntax into JavaScript output.3940## Workflow411. **Pick the smallest stable target** — an element or component locator42 over a full page where possible; less surface means fewer false diffs.432. **Reach a deterministic state before snapping.** Wait on a web-first44 assertion (`await expect(heading).toBeVisible()`) first — never add an45 arbitrary delay because a screenshot occasionally differs.463. **Control rendering conditions** as tightly as practical: viewport,47 device scale factor, fonts, OS/browser version, locale, timezone, color48 scheme, reduced-motion settings, and application data. The more these49 vary between baseline generation and comparison, the less reliable the50 result — generate baselines in the CI environment, not just locally.514. **Neutralize dynamic content** — timestamps, avatars, ads, live52 counters, loading indicators, rotating content:53 - `mask` only the smallest region that's actually dynamic; never mask54 content the test is meant to validate, and never mask a large area55 just to make a diff go away.56 - `animations: 'disabled'` unless the animation itself is what the test57 validates — disabling it there would defeat the test's purpose.58 - Prefer controlled/deterministic test data over live or random data.59 - Never invent a selector, test ID, or masked region that doesn't exist.605. **Set thresholds deliberately** (`maxDiffPixels` / `maxDiffPixelRatio` /61 `threshold`), not sprinkled ad hoc and never bumped just to make a62 failing test pass. Before adjusting one: inspect the actual diff,63 determine why the pixels differ, decide whether that difference is64 meaningful, and only then adjust — a large tolerance can hide a real65 regression.666. **Classify every screenshot failure** before touching a baseline:67 - **Expected change** — an intentional UI change; update the baseline68 after verifying it.69 - **Unexpected regression** — don't update the baseline; report the bug.70 - **Test instability** — e.g. a timestamp differs between runs;71 stabilize or mask the dynamic region instead of updating.72 - **Environment difference** — e.g. font rendering differs by platform;73 standardize the rendering environment instead of accepting the diff.747. **Generate baselines intentionally** via `--update-snapshots`, then75 **review each PNG by eye** before committing — an unreviewed baseline76 can lock in a bug forever. Document the update flow so baselines are77 refreshed on purpose, per platform.7879## Output format801. **Visual target** — what's being compared.812. **Stable state** — how the test reaches the intended UI state before82 snapping.833. **Screenshot test** — the Playwright code.844. **Dynamic content** — what needs stabilizing or masking.855. **Baseline strategy** — when/how the baseline is created or updated.866. **Tolerance** — thresholds, only when there's a clear reason for them.877. **Verification** — how to review a failure and tell an expected change88 from a regression, instability, or an environment difference.8990### Example91```typescript92import { test, expect } from '@playwright/test';9394test('dashboard card matches baseline', async ({ page }) => {95 await page.goto('/dashboard');96 const card = page.getByTestId('summary-card');97 await expect(card).toBeVisible(); // web-first: wait for render98 await expect(card).toHaveScreenshot('summary-card.png', {99 animations: 'disabled',100 mask: [page.getByTestId('last-updated')], // hide volatile timestamp101 maxDiffPixelRatio: 0.01,102 });103});104```105```106# generate/refresh baselines, then review the PNGs before committing107npx playwright test --update-snapshots108```109110## Guardrails111- Baselines are **generated then human-reviewed** — never auto-approve;112 never update one merely because a test failed.113- Never invent a route, selector, test ID, accessible name, application114 data, or baseline file that doesn't exist.115- Never use a large tolerance, or `waitForTimeout()`, to hide an116 unstable or unreviewed diff.117- Don't mask large areas unnecessarily, and never mask content the test is118 actually meant to validate.119- Don't replace a functional assertion with a screenshot — keep both.120- Investigate *why* a diff occurred before changing a threshold or a121 baseline; prefer the smallest screenshot target that gives meaningful122 coverage.123- Keep rendering conditions consistent where practical (viewport, fonts,124 OS/browser, locale, timezone, color scheme).125- Reuse existing Page Objects and fixtures rather than designing new ones126 here — that belongs to `pw-page-object-builder` / `pw-fixture-designer`.127- Preserve the project's JS/TS convention; never convert between them.128- State assumptions explicitly when the UI or rendering environment can't129 be verified.