ScreenshotNeo

BlogHow-to

How to Capture and Compare HTML Snapshots in Playwright

Learn when to use Playwright visual screenshots or HTML snapshots, how to create stable baselines, tune diffs, and fix flaky comparisons.

By the ScreenshotNeo team1 October 20267 min read

Playwright has two snapshot systems:

  • Visual snapshots compare rendered pixels with expect(page).toHaveScreenshot() or a locator screenshot assertion.
  • HTML or data snapshots compare strings or binary values with expect(value).toMatchSnapshot().

Use visual snapshots when layout, spacing, colors, and responsive rendering matter. Use HTML snapshots when the serialized DOM or another exact value is the contract you want to review. The first run creates a baseline; later runs compare against it.

Choose the right snapshot assertion

Goal Assertion What changes fail the test?
Whole-page rendering expect(page).toHaveScreenshot() Pixel differences in the page image
One component expect(locator).toHaveScreenshot() Pixel differences inside the locator
Serialized HTML expect(html).toMatchSnapshot() Text or markup differences
Other text or binary output expect(value).toMatchSnapshot() Differences in the supplied value

Playwright’s screenshot assertion waits until two consecutive screenshots are identical before comparing the final image. This reduces failures caused by an in-flight render. See the PageAssertions API and visual comparisons guide.

Set up a TypeScript project

npm init playwright@latest

Select TypeScript, install the browsers, and keep the generated playwright.config.ts. A minimal configuration is:

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

export default defineConfig({
  testDir: './tests',
  projects: [
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        baseURL: 'https://example.com',
      },
    },
  ],
  snapshotPathTemplate: '{testDir}/__snapshots__/{projectName}/{testFilePath}/{arg}{ext}',
});

The template keeps snapshots grouped by project, test file, and assertion name. Choose a path convention that remains stable when tests are reordered or moved.

Capture a visual page snapshot

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

test('home page visual baseline', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('home.png', {
    fullPage: true,
  });
});

Run it once to create the reference image:

npx playwright test tests/home.spec.ts

Commit the generated snapshot directory. On later runs, Playwright compares the new image with that file and reports a diff when they differ. When a UI change is intentional, review the diff and update baselines explicitly:

npx playwright test --update-snapshots

Do not update snapshots automatically in CI; that would accept regressions without review.

Capture only a component

test('checkout summary', async ({ page }) => {
  await page.goto('/checkout');
  const summary = page.locator('[data-testid="checkout-summary"]');
  await expect(summary).toHaveScreenshot('checkout-summary.png');
});

Locator snapshots reduce unrelated failures from navigation, ads, or other page regions.

Capture serialized HTML

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

test('serialized document', async ({ page }) => {
  await page.goto('https://example.com');
  const html = await page.content();
  expect(html).toMatchSnapshot('example.html');
});

toMatchSnapshot() accepts strings and binary data. Normalize values that are expected to change before asserting:

test('stable article HTML', async ({ page }) => {
  await page.goto('/article');
  const html = await page.content();
  const normalized = html
    .replace(/data-generated-at="[^"]*"/g, 'data-generated-at="<timestamp>"')
    .replace(/id="random-[^"]*"/g, 'id="random-<id>"');
  expect(normalized).toMatchSnapshot('article.html');
});

Prefer targeted locators or extracted data when the entire document contains third-party markup that is not part of your contract.

Make rendering deterministic

Screenshot output can vary with the operating system, browser version, browser settings, hardware, power source, and headless mode. Keep the comparison environment consistent:

  • Pin the Playwright version and browser binaries.
  • Run comparisons in the same OS or container image.
  • Use a fixed viewport, device scale factor, and color scheme.
  • Install and pin the fonts used by the application.
  • Seed test data and freeze application time where possible.
  • Disable animations and caret blinking.
  • Wait for application data and images before asserting.
  • Use stable locale, timezone, and network fixtures.
import { test as base } from '@playwright/test';

export const test = base.extend({
  page: async ({ page }, use) => {
    await page.addStyleTag({
      content: `*, *::before, *::after {
        animation-duration: 0s !important;
        animation-delay: 0s !important;
        transition: none !important;
        caret-color: transparent !important;
      }`,
    });
    await use(page);
  },
});

Apply this fixture after the page is created and before the screenshot. For an app with web fonts, wait for document.fonts.ready:

await page.evaluate(() => document.fonts.ready);

Tune screenshot comparisons safely

Playwright exposes three related controls:

Option Meaning Typical use
threshold Per-pixel color distance allowed by the comparison algorithm Small antialiasing or color-rendering variance
maxDiffPixels Maximum number of different pixels A known fixed-size region that may vary
maxDiffPixelRatio Maximum differing-pixel proportion Responsive images where the affected area scales
await expect(page).toHaveScreenshot('dashboard.png', {
  threshold: 0.2,
  maxDiffPixels: 120,
  maxDiffPixelRatio: 0.001,
});

Start with strict defaults. Investigate every diff, fix its cause, and then add the smallest allowance that matches known rendering variance. A large tolerance can hide a real layout regression.

Snapshot paths, projects, and naming

Playwright stores expectation files in a snapshot directory associated with the test file. snapshotPathTemplate lets you include placeholders such as the test directory, project name, test file path, assertion argument, and extension. A useful layout separates browser projects:

snapshotPathTemplate: '{testDir}/snapshots/{projectName}/{testFilePath}/{arg}{ext}'

Name snapshots for the behavior they represent: cart-empty.png, profile-mobile.png, or article.html. Avoid names based on test order or timestamps.

Control dynamic content

Dynamic content is the most common source of noisy snapshots. Use one or more of these approaches:

  1. Mock API responses with page.route() and return fixed fixtures.
  2. Hide volatile elements with the screenshot mask option.
  3. Replace timestamps, random IDs, and rotating ads before a text assertion.
  4. Wait for a specific selector instead of using an arbitrary sleep.
  5. Capture a component locator instead of the entire page.
await expect(page).toHaveScreenshot('account.png', {
  mask: [page.locator('[data-testid="last-login"]')],
});

Masking is appropriate when the element is intentionally outside the test’s scope. If the timestamp itself is important, freeze the application clock or assert its format separately.

CI workflow and review checklist

  1. Build the application from a locked dependency set.
  2. Install the exact Playwright browser revision.
  3. Run tests in a fixed container image.
  4. Upload failed screenshots and diff images as CI artifacts.
  5. Review baseline changes in code review.
  6. Use --update-snapshots only for an approved UI change.
  • Is the baseline generated in the same browser and OS used by CI?
  • Are fonts installed and loaded before capture?
  • Are API responses, time, locale, and timezone stable?
  • Does the assertion cover only the UI contract you intend to protect?
  • Does the tolerance document the known source of variance?

Common errors and fixes

Error or symptom Cause Fix
“Snapshot … is missing” No baseline exists for this project or assertion name Run the test once, inspect the generated image, and commit it.
Large diff after a browser upgrade Rendering changed with the browser, OS, or fonts Pin versions; if the change is expected, regenerate and review all affected baselines.
Intermittent diffs Animations, late network responses, fonts, or time-dependent data Disable motion, wait for readiness, mock data, and await document.fonts.ready.
Full-page image is unexpectedly tall fullPage includes the entire scrollable document Use a locator screenshot, set a fixed viewport, or remove unintended overflow.
HTML snapshot changes on every run Random IDs, timestamps, ads, or serialization order Normalize volatile fields, mock third parties, or snapshot a stable locator.
Snapshot passes locally but fails in CI Different OS, fonts, browser revision, or headless settings Use the same container and browser revision for baseline generation and CI.
Diff tolerance hides a regression threshold or pixel limits are too broad Reduce the allowance and fix the underlying nondeterminism.

Performance, reliability, and cost considerations

Visual snapshots render a browser page and are slower and more resource-intensive than comparing a short string. Component screenshots reduce image size and rendering work. Reuse the Playwright browser context, mock slow services, and avoid redundant full-page assertions. Parallel workers can improve throughput, but each worker must use the same deterministic fixtures.

Snapshot files belong in source control when they are part of the reviewed contract. Keep large or generated artifacts out of the repository when they are only diagnostic output. The main cost of this approach is CI browser time and storage; tighter test scope usually reduces both.

Or skip the browser setup

ScreenshotNeo provides a hosted screenshot API when you need an image rather than a Playwright test environment. One GET request captures a URL as PNG, JPEG, WebP, or PDF. Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result.

See the ScreenshotNeo API documentation for all options, including full-page capture, CSS-selector elements, device presets, custom CSS and JavaScript, waits, request blocking, headers, cookies, geolocation, caching, signed links, async jobs, bulk capture, and PDF output.

cURL

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

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

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 failed: ${res.status}`);
const body = Buffer.from(await res.arrayBuffer());
await require('node:fs').promises.writeFile('shot.webp', body);

ScreenshotNeo also includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots each month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan.

Create a free ScreenshotNeo account to get started.

FAQ

Does an HTML snapshot contain a screenshot?

No. toMatchSnapshot() compares the value you provide, such as page.content(). Use toHaveScreenshot() for rendered pixels.

Where are Playwright visual baselines stored?

They are stored as files in the test snapshot directory. Configure snapshotPathTemplate when you need a predictable project and test-file hierarchy.

Should I use a tolerance for every screenshot?

No. Begin with strict comparison, remove nondeterminism, and add a narrowly justified tolerance only when the remaining variation is understood.

Can I compare only one element?

Yes. Call toHaveScreenshot() on a locator to limit the comparison to that component.

How do I approve an intentional visual change?

Review the diff, then run npx playwright test --update-snapshots in the controlled environment and commit the new baseline.