ScreenshotNeo

BlogHow-to

Visual Testing with Playwright: How to Catch UI Regressions

Use Playwright Test screenshots to catch unintended UI changes. Build stable baselines, review diffs, and tune comparisons without hiding real regressions.

By the ScreenshotNeo team4 October 202610 min read

Use Playwright Test’s built-in toHaveScreenshot() assertion to compare a page or component against a reviewed reference image. The first run creates the reference; later runs capture the UI and fail when the rendered result differs beyond your configured tolerance. Keep the browser, operating system, viewport, data, and application state consistent so the diff points to a UI change instead of environmental noise. Playwright visual comparisons

A screenshot diff detects appearance changes. Pair it with role, text, URL, and other assertions to check behavior and content. A difference is evidence of a changed render, not proof by itself that the change is a defect.

1. Set up Playwright Test

toHaveScreenshot() is a Playwright Test runner assertion. Install the test package and browser, then create a test file. These commands use npm:

npm install --save-dev @playwright/test
npx playwright install chromium

Save this as tests/home.visual.spec.ts. It assumes the app runs at the configured base URL and shows a heading named “Welcome.” Adjust the URL and locator to match your app.

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

test('home page visual baseline', async ({ page }) => {
  await page.goto('/');
  await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
  await expect(page).toHaveScreenshot('home-page.png');
});

If you do not already have a configuration, save this as playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  reporter: 'html',
  use: {
    baseURL: 'http://127.0.0.1:3000',
    browserName: 'chromium',
    viewport: { width: 1280, height: 800 },
    colorScheme: 'light',
    locale: 'en-US',
    timezoneId: 'UTC',
    trace: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Run the suite with npx playwright test. If your app starts another way, change webServer.command, or omit webServer and start the app separately. Install the browser matching the Playwright package in the CI environment as well as locally.

2. Create and review the baseline

  1. Run the test once. Playwright reports that the expected screenshot does not exist and writes the actual image as the initial reference.
  2. Open the generated snapshot and check that it shows the intended page, data, viewport, and state. Do not accept an accidental error page or incomplete render as the golden image.
  3. Commit the reviewed snapshot files alongside the test. Playwright stores them in a snapshot directory associated with the test file by default.
  4. Run the test again. Playwright captures the page and compares it with the committed reference.

Playwright waits for two consecutive screenshots to match before comparing them. This helps avoid capturing a changing render, but it does not replace waiting for your application to reach the correct state. PageAssertions documentation

3. Choose the right screenshot scope

Compare a page

Use a page screenshot for important overall layouts: a landing page, checkout step, dashboard, or responsive navigation state. For a long page, enable fullPage:

await expect(page).toHaveScreenshot('catalog-full-page.png', {
  fullPage: true,
});

Full-page capture can make a small difference difficult to locate and can increase review burden. Use it when content below the fold matters; use viewport captures for states users see at a particular scroll position.

Compare a component

Use a locator screenshot when you need to isolate a reusable or high-risk component. Prefer stable test IDs or accessible locators over brittle positional selectors.

const navigation = page.getByTestId('navigation');
await expect(navigation).toBeVisible();
await expect(navigation).toHaveScreenshot('navigation.png');

Component screenshots reduce unrelated page noise, but they do not catch changes in the component’s placement within the full page. Keep page-level coverage for a few important end-to-end layouts.

4. Make captures deterministic

Every uncontrolled change in rendered pixels can make a visual test noisy. Make the test state repeatable before tuning the comparison.

  • Control data: seed test records and use fixed fixtures. Avoid live user-generated data, random values, rotating recommendations, and current timestamps.
  • Wait for the state you need: assert a heading, loaded component, or other explicit condition before capturing. Do not rely on a fixed sleep as the only readiness check.
  • Pin viewport and device settings: set viewport size, device scale behavior, locale, timezone, and color scheme explicitly where they affect appearance.
  • Stabilize assets: make sure fonts, images, and CSS have loaded. Keep test assets available and avoid dependencies on external pages that may change.
  • Handle animation and caret: screenshot assertions disable animations and hide the caret by default. Keep those defaults unless animation itself is what you are testing.
  • Control dynamic regions deliberately: use a screenshot stylesheet or mask for genuinely irrelevant changing content. Do not mask areas where regressions matter.
  • Keep environment consistent: generate and compare baselines using the same operating system and browser version, ideally the same pinned CI image and Playwright browser revision.

Playwright warns that screenshots can vary with operating system, browser version and settings, hardware, power source, and headless mode. Its guidance is to run tests in the same environment where the baselines were generated. Visual comparisons · Best practices

5. Configure screenshot comparison options

Use options locally for an exceptional test, or set shared defaults in the Playwright configuration. Start with strict comparisons in a stable environment; relax only for a known source of harmless pixel variation.

Option What it controls When to use it
fullPage Captures the full scrollable page rather than the viewport. Long layouts where below-the-fold content is part of the check.
maxDiffPixels Maximum number of differing pixels allowed. A known small, fixed amount of harmless rendering noise.
maxDiffPixelRatio Maximum fraction of differing pixels, from 0 to 1. A tolerance that should scale with image dimensions.
threshold Per-pixel perceived color difference in YIQ; lower is stricter. The documented default is 0.2. Known color-rendering variation, after checking the diff.
mask, maskColor Overlays locator bounding boxes in the screenshot; the default mask color is pink. Unavoidable dynamic content that is outside the visual contract.
stylePath Applies a stylesheet during capture; it can hide or alter dynamic elements. Consistently suppressing volatile regions. Review the stylesheet like test code.
animations Disables or allows animations; defaults to disabled. Use allow only when animation is the behavior being checked.
caret Hides the text caret by default, or preserves its initial behavior. Preserve it only when caret rendering is intentionally under test.
scale css gives one image pixel per CSS pixel; device captures device pixels. Use CSS scale for smaller, consistent captures; device scale when high-DPI output matters.
clip Captures a specified rectangle. A fixed region of the page when a locator screenshot is not suitable.
omitBackground Allows transparent background; does not apply to JPEG. Components whose transparent output is part of the expected appearance.
timeout Time allowed for the retrying assertion. Slow but bounded rendering. Fix indefinite readiness problems rather than raising timeouts without limit.

Example with a component mask and explicit tolerance:

await expect(page).toHaveScreenshot('account-summary.png', {
  maxDiffPixels: 10,
  mask: [page.getByTestId('live-clock')],
});

These settings are documented in PageAssertions and SnapshotAssertions. A larger tolerance reduces failures but can also conceal a small color, spacing, or alignment regression. Treat every increase as a reviewed trade-off.

6. Review diffs and update intentionally

When a comparison fails, inspect the expected image, actual image, and diff. Determine whether the change is intended before changing the reference.

  • Unintended change: fix the UI or restore the expected state, then rerun the test.
  • Intended design change: update the reference with npx playwright test --update-snapshots, inspect the new images, and include them in the same review as the code change.
  • Environment-only difference: align the machine, browser revision, fonts, and settings with the baseline environment before updating images.
  • Unstable area: make its data deterministic, wait for readiness, or narrowly mask/style that region if it is not part of the visual contract.

Do not routinely run snapshot updates as a way to make CI green. That replaces the evidence needed to review the change. Playwright documents the update command and recommends committing and reviewing snapshot files. Visual comparisons

7. Run visual checks in CI

  1. Use the same CI image, operating system, browser revision, and Playwright version that produced the committed baselines.
  2. Install the browser through the project’s Playwright install command so the version matches the package.
  3. Run the normal suite with npx playwright test and retain the test report or trace for failed runs.
  4. Review changed screenshots with the corresponding app change. Update expected images only after approving the UI change.

Local screenshots may differ from CI even when the application code is unchanged. Baselines are environment-specific artifacts: use a deliberate baseline-generation environment and avoid mixing snapshots from different operating systems or browser revisions. Parallel test execution can reduce elapsed suite time, while screenshot-heavy suites still consume browser CPU, memory, and storage; choose concurrency based on the CI runner’s capacity.

8. Pair visual checks with functional assertions

A screenshot can show that something changed, but it cannot explain whether a button works, whether text is correct, or whether a route is valid. Add assertions for the contract you care about:

test('signed-in dashboard renders its key state', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page).toHaveURL(/dashboard/);
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  await expect(page.getByRole('button', { name: 'Create report' })).toBeEnabled();
  await expect(page).toHaveScreenshot('dashboard.png');
});

Use semantic assertions for content, roles, and behavior; use screenshot assertions for layout, color, typography, and visual hierarchy. These checks complement one another.

9. Troubleshooting common failures

Symptom Likely cause Fix
“Snapshot doesn’t exist” on the first run No expected image has been generated yet. Run the test, inspect the created image, then commit it if it is the intended baseline.
Same code passes locally and fails in CI OS, browser version, fonts, hardware, settings, or headless mode differ. Generate and compare snapshots in the same pinned CI environment and browser revision.
Intermittent diff around a spinner or timestamp Dynamic data, animation, or capture before the page reaches its intended state. Use fixed data, wait for a meaningful condition, and mask only content outside the visual contract.
Large parts of the page are blank Capture occurred before data, images, or fonts loaded, or the test navigated to an error state. Assert a page-specific ready condition and verify the navigation and app state before capturing.
Snapshot updates change many files A shared layout, viewport, browser, or rendering dependency changed—or updates were run broadly. Inspect diffs by test, verify environment consistency, and update only after understanding the common change.
Visual assertion method is unavailable or fails outside the test suite toHaveScreenshot() belongs to Playwright Test’s runner. Run it through npx playwright test with test and expect imported from @playwright/test.
Path error for a named snapshot The supplied path escapes the test’s snapshot directory. Use a simple filename or path segments contained within the test snapshot directory.
Too many harmless failures after raising tolerance Tolerance does not address the real source of nondeterminism, or it is too strict/too broad. Inspect actual and expected images, stabilize the cause, then choose a narrowly justified pixel or color threshold.
Assertion times out waiting to match The page continues changing, never reaches the target state, or is too slow under test conditions. Check readiness assertions and app errors; use a longer bounded assertion timeout only when rendering legitimately needs more time.

10. Performance, reliability, and maintenance

Visual tests add browser rendering and image comparison work to a suite, and each baseline adds a file that reviewers and version control must manage. Keep screenshots focused on valuable pages and representative interaction states instead of capturing every minor variation. Locator captures can reduce irrelevant page changes; a small set of page captures still checks integration and composition.

Stability comes primarily from controlling the rendered input and comparison environment. Avoid broad masks and permissive thresholds that hide the very changes the suite is meant to catch. A reliable workflow makes failures actionable: preserve failure artifacts, inspect diffs, and make baseline updates reviewable.

11. Or skip the browser setup

If you need an image of a live URL without maintaining a browser test environment, ScreenshotNeo provides a screenshot API. This is useful for capturing a page for review or documentation; it does not replace Playwright’s baseline comparison and assertion workflow.

See the ScreenshotNeo API documentation. Example request:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo accepts consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before the capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

FAQ

Does a screenshot assertion prove a page is correct?

No. It proves the render matches the accepted reference within configured comparison rules. Add semantic and functional assertions for behavior and content.

Can I use Playwright screenshots without Playwright Test?

You can capture screenshots with Playwright APIs, but toHaveScreenshot() and its expected-image comparison are Playwright Test runner features.

Should I baseline every browser and viewport?

Only when the supported browser or viewport differences matter to your product. Each combination adds baseline and review work; start with the highest-value target environments.

Should I update snapshots whenever a test fails?

No. Inspect the diff and decide whether the visual change is intended. Update the reference only when you approve that new appearance.