Resource library

QA How-To

How to Use Playwright toHaveScreenshot (2026)

Learn how Playwright toHaveScreenshot creates visual baselines, compares pages and components, controls noise, and produces reliable CI results in 2026.

21 min read | 2,843 words

TL;DR

Playwright toHaveScreenshot is an auto-retrying visual assertion in `@playwright/test`. Render a deterministic page, call `await expect(page).toHaveScreenshot('name.png')`, review and commit the first baseline, then let later runs compare the current image with that baseline. Use locator snapshots, masks, styles, and small evidence-based tolerances to control legitimate variation.

Key Takeaways

  • Use `expect(page).toHaveScreenshot()` or the locator form inside Playwright Test, not as a standalone image capture utility.
  • Create baselines in a controlled environment and review every generated image before committing it.
  • Stabilize data, viewport, fonts, time, animations, and network responses before relaxing pixel tolerances.
  • Prefer locator screenshots for focused component contracts and page screenshots for intentional end-to-end layouts.
  • Use masks or `stylePath` only for genuinely nondeterministic regions, and keep important content visible to assertions.
  • Update snapshots as a reviewed change, never as an automatic reaction to a failed CI comparison.

Playwright toHaveScreenshot is the built-in visual assertion for comparing a rendered page or element with an approved PNG baseline. The first run creates the expected image, and later runs fail when the current rendering differs beyond the configured tolerance. Because it waits for two consecutive screenshots to match before comparing, it is more suitable for visual regression checks than a raw page.screenshot() call.

A reliable visual test is not just a screenshot statement. It is a controlled rendering contract: the same browser project, viewport, fonts, data, time, state, and operating environment must produce the same meaningful pixels. This guide shows how to build that contract, diagnose failures, and manage baselines without hiding real defects.

TL;DR

Task Recommended API or command Purpose
Compare a viewport await expect(page).toHaveScreenshot('home.png') Protect a page-level layout
Compare one component await expect(locator).toHaveScreenshot('card.png') Reduce unrelated visual noise
Generate missing baselines npx playwright test Creates snapshots that do not exist
Review changed baselines npx playwright test --update-snapshots=changed Updates only changed expected images
Inspect a failure HTML report and actual, expected, diff attachments Locate and explain changed pixels
Control dynamic content Stable test data, mask, or stylePath Make the visual contract deterministic

The essential workflow is: prepare stable state, capture a named snapshot, review the baseline, commit it with the test, run comparisons in the same environment, and investigate every diff before updating expected output.

1. What Playwright toHaveScreenshot Actually Does

toHaveScreenshot is an asynchronous, web-first assertion supplied by Playwright Test. It can target a Page or a Locator. Before it compares pixels, Playwright repeatedly captures the target until two consecutive screenshots are identical. The last stable capture is compared with the stored expectation. That stabilization behavior helps with short rendering changes, but it does not make arbitrary live data deterministic.

On the first execution, no expected file exists. The assertion writes a baseline and reports that the snapshot was missing. After you inspect and approve that PNG, commit it beside the test's snapshot directory. On following executions, Playwright produces the current image and compares it with the committed baseline. A mismatch normally gives you expected, actual, and diff images in the test result attachments.

The assertion is different from page.screenshot(). A raw screenshot returns bytes or saves a file, but it does not know which baseline to use, retry for visual stability, calculate a comparison, or fail the test. Use raw captures for documentation and ad hoc evidence. Use toHaveScreenshot when pixels are part of the acceptance criteria.

Screenshot assertions work with the Playwright Test runner. Teams using only the lower-level Playwright library need their own comparison layer, or they should bring the scenario into Playwright Test. If you are new to assertion retries, review Playwright expect assertions before adopting a large visual suite.

2. Install and Configure a Deterministic Project

Start with a normal TypeScript Playwright Test project. The following commands create a package, add the runner, and install the Chromium browser used by the example:

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

Create playwright.config.ts with an explicit test directory, viewport, output directory, and screenshot defaults:

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

export default defineConfig({
  testDir: './tests',
  outputDir: 'test-results',
  expect: {
    timeout: 10_000,
    toHaveScreenshot: {
      animations: 'disabled',
      caret: 'hide',
      scale: 'css',
      maxDiffPixels: 20,
    },
  },
  projects: [
    {
      name: 'chromium-desktop',
      use: {
        ...devices['Desktop Chrome'],
        viewport: { width: 1280, height: 720 },
        locale: 'en-US',
        timezoneId: 'UTC',
        colorScheme: 'light',
      },
    },
  ],
});

The value 20 is illustrative, not a universal standard. Begin with strict comparisons, observe whether your controlled environment produces harmless isolated differences, and document any tolerance you add. Explicit emulation settings prevent the host machine from silently choosing viewport, locale, time zone, or theme.

Do not mix baseline generation across laptops and containers unless they use equivalent browser binaries, fonts, rendering libraries, and configuration. The most dependable pattern is to generate and compare Linux baselines inside the same versioned container image used by CI.

3. Your First Playwright toHaveScreenshot Test

This self-contained example runs without an external application. It loads fixed markup, verifies a user-visible state, and compares the viewport:

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

test('account summary matches the approved layout', async ({ page }) => {
  await page.setContent(`
    <style>
      body { margin: 0; font: 16px Arial, sans-serif; background: #f4f7fb; }
      main { width: 520px; margin: 48px auto; padding: 24px; background: white; }
      .balance { color: #14532d; font-size: 32px; font-weight: 700; }
    </style>
    <main>
      <h1>Account summary</h1>
      <p>Available balance</p>
      <p class='balance'>$2,450.00</p>
      <button>Transfer funds</button>
    </main>
  `);

  await expect(page.getByRole('heading', { name: 'Account summary' }))
    .toBeVisible();
  await expect(page).toHaveScreenshot('account-summary.png');
});

Run it with npx playwright test. The initial run writes a baseline under a snapshot directory derived from the spec name and browser project. Inspect the PNG at full size. Confirm text, colors, spacing, clipping, and the exact state being approved. Then add the spec and baseline to version control.

Run the test again without update flags. It should pass because the rendered image matches the approved one. To prove the assertion works, intentionally change the background color, rerun, and inspect the diff. Revert that learning change rather than accepting it. This small exercise teaches the complete lifecycle before your suite accumulates hundreds of images.

Name snapshots by business state, such as empty-cart.png or payment-declined.png. Stable descriptive names survive test-title edits and make review attachments understandable.

4. Page Screenshots Versus Locator Screenshots

The right comparison boundary has a large effect on signal quality. expect(page).toHaveScreenshot() captures the viewport by default, or the entire scrollable document with fullPage: true. expect(locator).toHaveScreenshot() captures the bounding box of a resolved element. Both assertions stabilize and compare their target, but they protect different contracts.

Boundary Best for Main advantage Main risk
Viewport page Headers, navigation, page composition Detects relationship and overflow defects Unrelated widgets create noise
Full page Static reports, receipts, long landing pages Covers content below the fold Lazy loading and sticky elements complicate capture
Locator Cards, dialogs, charts, design-system components Small, focused, reviewable diff Misses surrounding placement problems
Clipped page Fixed canvas-like region Exact coordinates for a special surface Coordinate coupling makes maintenance harder

Prefer locator screenshots for a component with a clear visual contract:

const receipt = page.getByRole('region', { name: 'Order receipt' });
await expect(receipt).toBeVisible();
await expect(receipt).toHaveScreenshot('paid-receipt.png');

Add a smaller set of page-level snapshots for critical compositions. Avoid capturing every page at every step. A visual suite should protect risks that semantic assertions cannot fully express, such as clipping, overlap, responsive arrangement, brand color application, and chart rendering. Text existence and enabled state are usually clearer with normal locators. The Playwright locator best practices guide helps define stable component targets.

5. Make Fonts, Data, Time, and Motion Stable

Most flaky visual tests are uncontrolled-input problems. Wait for the application state that matters before the screenshot. Seed fixed records, use stable account names, freeze application time when dates are rendered, and ensure custom fonts have loaded. A response finishing does not always mean the page has completed rendering, so assert a user-visible ready state.

For a web font, wait on the browser's font readiness promise:

await page.goto('/invoice/visual-test');
await page.evaluate(() => document.fonts.ready);
await expect(page.getByText('Invoice INV-1042')).toBeVisible();
await expect(page).toHaveScreenshot('invoice.png');

toHaveScreenshot disables CSS animations, CSS transitions, and Web Animations by default. Finite animations are advanced to completion, while infinite animations are canceled for capture and then resume. Keep the default unless motion itself is the feature under test. A looping canvas, video, or server-driven ticker may require application-specific controls because CSS animation handling cannot freeze every dynamic source.

Use Playwright's Clock API or a test hook in your application when the UI displays the current date. Mock volatile API responses with page.route() when the purpose is visual rendering rather than backend integration. Do not mock away the behavior the test claims to cover.

A good pre-capture checklist is: the intended URL is loaded, the correct user is signed in, data identifiers are fixed, fonts are ready, loading indicators are gone, the viewport is explicit, and the target has its final dimensions.

6. Use mask and stylePath Without Hiding Defects

A mask overlays each selected locator with a solid color before comparison. It is suitable for content whose exact value is irrelevant and cannot be fixed, such as a third-party avatar or generated token. By default the overlay is pink, and maskColor can change it. Masks also apply to invisible elements, so filter for visible targets when that distinction matters.

await expect(page).toHaveScreenshot('team-dashboard.png', {
  mask: [
    page.getByTestId('live-clock'),
    page.locator('.external-avatar:visible'),
  ],
  maskColor: '#000000',
});

stylePath loads a stylesheet only for the screenshot. The CSS can hide a nondeterministic widget, neutralize a cursor, or normalize a third-party animation. It pierces Shadow DOM and applies inside frames, which makes it powerful and potentially broad. Keep the file small and explain every rule:

/* tests/visual-snapshot.css */
[data-visual-test='volatile-ad'] { visibility: hidden !important; }
.live-map canvas { opacity: 0 !important; }
await expect(page).toHaveScreenshot('search-results.png', {
  stylePath: 'tests/visual-snapshot.css',
});

First ask whether you can control the data or state. Hiding an order total, validation message, or layout-shifting banner would remove valuable coverage. A mask says that the region's content is excluded, but its rectangular footprint still participates in the image. A style can alter layout entirely. Review these controls as test logic, not harmless formatting.

7. Choose Pixel Tolerances With Evidence

Playwright offers three related comparison controls. threshold is the accepted perceived color difference for an individual pixel in YIQ color space, from zero to one, with a default of 0.2. maxDiffPixels caps the absolute number of differing pixels. maxDiffPixelRatio caps differing pixels as a fraction of the entire image. The absolute and ratio limits describe how much may differ, while threshold describes when one pixel counts as different.

Do not raise all three until a noisy test passes. First inspect the diff pattern. A one-pixel halo around text often signals font or rasterization differences. A large solid block signals a layout or state change. Scattered changes in timestamps signal volatile data. Each pattern calls for a different fix.

Global defaults should be conservative and consistent. A specific data visualization may need a local exception:

await expect(page.getByTestId('revenue-chart')).toHaveScreenshot(
  'revenue-chart.png',
  { maxDiffPixelRatio: 0.001, threshold: 0.2 },
);

Record why the exception exists and keep meaningful semantic assertions beside it. For example, verify the chart title, accessible data table, or legend values separately. Never choose a tolerance as a percentage of whatever currently fails. That approach converts a defect into the new acceptance policy. Compare repeated clean runs, establish the smallest safe allowance, and revisit it when browser or OS upgrades change rendering.

8. Manage Snapshot Paths and Multiple Projects

By default, baseline names include project-related suffixes so Chromium, Firefox, WebKit, desktop, and mobile projects do not compare incompatible images. This separation is desirable. Different browser engines can render fonts, form controls, and subpixel geometry differently. Do not force all projects to share one PNG unless equivalence is intentional and repeatedly demonstrated.

snapshotPathTemplate controls snapshot locations for a project. Current Playwright configuration also supports a pathTemplate specifically under expect.toHaveScreenshot. A repository might group images beneath a central directory, but the chosen template must keep arguments unique enough to prevent one test from overwriting another. The default convention is often easiest for parallel execution and code review.

Treat baselines as source artifacts. Commit them, review them, and change them in the same pull request as the UI or test that justifies the change. Git Large File Storage may help a very large suite, but first reduce unnecessary snapshots and focus boundaries. Do not store expected images in test-results, because output directories are routinely cleaned.

When upgrading Playwright, update the package and browser binaries together. Run the visual suite in a dedicated branch or pull request, inspect systematic changes, and regenerate baselines in the canonical environment. A mass update without review can approve widespread regression. If your matrix is large, the Playwright projects guide explains how device and browser settings inherit.

9. Update and Review Baselines Safely

Use the update command only after you understand why a comparison changed. Modern Playwright CLI modes let you control the scope:

# Update changed snapshots for one visual spec.
npx playwright test tests/account.visual.spec.ts --update-snapshots=changed

# Create or refresh all snapshots selected by the command.
npx playwright test tests/account.visual.spec.ts --update-snapshots=all

# Update missing snapshots without accepting changed ones.
npx playwright test --update-snapshots=missing

Without a value, --update-snapshots uses its documented default behavior, but an explicit mode makes review intent clearer. Filter to a file, project, or test title so an unrelated failure cannot rewrite many images. Inspect the expected image before and after. Ask whether the product requirement changed, whether the test entered the wrong state, and whether the change is consistent across responsive and browser projects.

Never enable snapshot updates on the ordinary CI verification job. A quality gate that rewrites its own expected results cannot detect regression. A separate developer-invoked or approval-gated workflow may generate candidate artifacts, but a person still needs to review them.

Useful pull request evidence includes the UI change description, affected snapshot names, before and after images, and the command plus environment used to generate them. This turns a binary file change into an auditable product decision.

10. Debug a Playwright toHaveScreenshot Failure

Begin with the diff attachments, not the snapshot update command. The expected image is the approved contract. The actual image shows current output. The diff emphasizes changed areas. Look for boundaries: a shifted block points to dimensions or fonts, a missing block points to state or loading, and widespread color change points to theme or CSS.

Reproduce the exact project locally:

npx playwright test tests/account.visual.spec.ts \
  --project=chromium-desktop --workers=1 --trace=on

Open the HTML report with npx playwright show-report. Use the trace to confirm navigation, DOM state, network responses, and the action immediately before capture. The trace is excellent for explaining how the page reached the wrong state, while screenshot diff attachments explain what pixels violated the visual contract.

Check environment parity next: Playwright package, downloaded browser revision, OS image, installed fonts, locale, time zone, color scheme, viewport, device scale, and test data. If local passes and CI fails consistently, reproduce inside the CI container rather than widening tolerance.

When a diff is intermittent, repeat the focused test and compare actual images across runs. Fluctuating content calls for deterministic inputs. Fluctuating geometry may indicate late font loading, asynchronous hydration, or an unawaited application transition. Add a user-visible readiness assertion at the earliest point where the intended state becomes observable.

Interview Questions and Answers

Q: What is the difference between page.screenshot() and expect(page).toHaveScreenshot()?

page.screenshot() captures an image and optionally writes it to a path. toHaveScreenshot() is a Playwright Test assertion that waits for screenshot stability, compares against a stored baseline, attaches comparison evidence, and fails when differences exceed policy. I use the raw API for evidence and the assertion for a visual acceptance contract.

Q: Why does Playwright capture two identical screenshots before comparison?

It uses consecutive identical captures to avoid comparing during a short rendering transition. This reduces noise from frames that are still changing. It does not solve volatile data, live video, missing fonts, or an application that never reaches stable state, so those inputs still need control.

Q: When would you use a locator screenshot instead of a page screenshot?

I use a locator screenshot when one component owns the visual risk, such as a dialog, receipt, chart, or card. It creates a smaller diff and avoids failures from unrelated page regions. I add page-level snapshots separately when placement, overlap, or whole-page composition is itself a requirement.

Q: How do you handle timestamps in visual tests?

I first freeze time or provide fixed test data so the timestamp remains meaningful and visible. If the value comes from an uncontrollable third party and its pixels are irrelevant, I mask only that locator. I do not hide broad containers because doing so can remove layout and content coverage.

Q: What makes a screenshot baseline trustworthy?

A trustworthy baseline was generated from the intended state in the canonical browser environment, visually reviewed, and committed with the test. Its viewport, fonts, locale, theme, data, and browser revision are reproducible. Updating it requires a known product or rendering change, not merely a failing comparison.

Q: How would you investigate a CI-only screenshot difference?

I compare expected, actual, and diff images, then verify package, browser, OS image, fonts, viewport, locale, theme, and test data. I inspect the trace for state or network differences and reproduce in the CI container. I change tolerance only if repeated evidence shows a small harmless rendering variance remains after the environment is controlled.

Q: What is the risk of a high maxDiffPixelRatio?

A ratio scales with image size, so a seemingly small percentage can permit a large visible defect on a full-page image. It can also accept different pixels on every run. I prefer strict defaults, focused targets, deterministic inputs, and the smallest locally justified tolerance.

Common Mistakes

  • Taking a screenshot immediately after navigation without asserting that the intended business state is ready.
  • Generating baselines on one operating system and comparing them on an incompatible CI image.
  • Using full-page snapshots for every test when a component boundary would give a clearer signal.
  • Masking large containers that include prices, validation errors, or layout behavior the test should protect.
  • Increasing threshold, maxDiffPixels, and maxDiffPixelRatio together without reading the diff.
  • Leaving names implicit, then struggling to understand snapshot files after test titles change.
  • Updating all snapshots after any failure and reviewing only whether CI becomes green.
  • Capturing live production data, advertisements, video, random avatars, or current time without controls.
  • Assuming animation disabling also freezes canvas, media playback, server polling, and all JavaScript timers.
  • Running visual comparisons across projects without separate baselines for different browser engines or device settings.

The corrective pattern is simple: make state explicit, keep the comparison boundary narrow, diagnose the diff by its visual shape, and treat baseline updates like production code changes.

Conclusion

Playwright toHaveScreenshot works best as a precise visual assertion, not a blanket screenshot generator. Stabilize the inputs, select a page or locator boundary that represents real risk, use strict comparison settings, and store reviewed baselines with the test. Masks, styles, and tolerances are useful controls when they have a documented reason.

Start with one critical component and one page composition. Run them repeatedly in the same environment, review every baseline and diff, and add coverage only where pixel comparison catches defects that semantic assertions cannot describe as clearly.

Interview Questions and Answers

What problem does Playwright toHaveScreenshot solve?

It turns a rendered page or element into an auto-retrying visual assertion against an approved baseline. It detects layout, clipping, style, and rendering regressions that semantic assertions may miss. Playwright also produces expected, actual, and diff evidence when the comparison fails.

How is a baseline created and maintained?

The first run creates a missing expected image, which I inspect before committing. Later runs compare against that file. I update it only after confirming an intentional product or rendering change in the canonical environment, and I review the image change like code.

How do you reduce flaky visual tests?

I control the browser project, OS image, viewport, fonts, locale, theme, time, data, network responses, and readiness state. I prefer locator snapshots and keep default animation and caret stabilization. Masks and tolerances come only after those controls are in place.

What is the difference between threshold and maxDiffPixels?

`threshold` decides how different two corresponding colors may be before that pixel counts as changed. `maxDiffPixels` limits the number of pixels allowed to count as changed. `maxDiffPixelRatio` expresses that amount relative to image size.

When should a team avoid screenshot assertions?

Avoid them when a semantic assertion expresses the requirement more directly, when the surface is intentionally volatile, or when no reproducible rendering environment exists. Screenshot coverage should target visual risk, not duplicate every functional test. Too many broad snapshots create review fatigue.

How do you debug a screenshot failure?

I inspect expected, actual, and diff images, identify the change pattern, then use the trace to verify state, network, and preceding actions. I compare browser and environment settings with the baseline environment. I update the baseline only when evidence proves the change is intended.

Why are locator screenshots often more maintainable?

They isolate the component that owns the visual contract, which makes diffs smaller and reduces failures caused by unrelated widgets. They also encourage stable semantic targeting. Page snapshots remain useful for a smaller set of composition and overflow risks.

Frequently Asked Questions

How do I use Playwright toHaveScreenshot?

Import `test` and `expect` from `@playwright/test`, render a deterministic page, and call `await expect(page).toHaveScreenshot('name.png')`. Review and commit the baseline created on the first run, then run the same test without update flags to compare future output.

Where does Playwright store screenshot baselines?

By default, Playwright stores them in a snapshot directory associated with the test file, with names that distinguish projects and platforms as needed. You can customize paths with snapshot path templates, but every test and project must still resolve to a unique file.

Does toHaveScreenshot wait for the page to load?

It waits until two consecutive screenshots are identical, but it does not know your business-ready state. Navigate and assert the intended heading, data, or loading completion before taking the visual snapshot.

How do I update Playwright screenshots?

Run a focused test with a command such as `npx playwright test path/to/spec --update-snapshots=changed`. Inspect the cause and resulting images before committing them, and never update snapshots automatically in a normal verification job.

Can Playwright ignore a dynamic screenshot region?

Yes. Pass locators in the `mask` option or apply a narrow stylesheet with `stylePath`. Prefer fixed data or frozen time first, because hiding a region removes visual information from the contract.

Why do Playwright screenshots differ between local and CI?

Common causes are different browser revisions, operating system rendering, fonts, viewport, device scale, locale, time zone, theme, and data. Reproduce in the CI container and align those inputs before increasing tolerances.

Should I use maxDiffPixels or maxDiffPixelRatio?

Use an absolute limit for a known small amount of noise on a consistently sized target. A ratio can suit targets whose size legitimately varies, but it may permit many pixels on a large image. Choose the smallest evidence-based local setting.

Related Guides