Screenshots and visual regression testing

Learn how to take some pictures and implement visual regression tests.

Playwright's debugging tools (traces, UI mode) usually answer "what happened?", but a plain screenshot is sometimes the fastest way to peek inside a headless run.

Page screenshots

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

test("get started link", async ({ page }) => {
  await page.goto("/");

  // take a page screenshot
  await page.screenshot({ path: "./home.png" });
});

Use page.screenshot() to capture the state of the page. Pass fullPage: true to capture the entire scrollable page instead of just the viewport:

await page.screenshot({ path: "./home.png", fullPage: true });
Todo

Locator screenshots

If you're only interested in a particular DOM element, locators have a screenshot() method, too.

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

test("get started link", async ({ page }) => {
  await page.goto("/");

  // take a screenshot of a particular element
  await page
    .getByRole("link", { name: "Products" })
    .screenshot({ path: "./products.png" });
});
Inline exercise

Capture a single product card

Adjust one of your existing tests, navigate to /search, and take a screenshot of the first product card only.

Show how
test("product card screenshot", async ({ page }) => {
  await page.goto("/search");

  await page
    .getByTestId("search-grid")
    .getByRole("link")
    .first()
    .screenshot({ path: "./screenshots/product.png" });
});

Screenshots as test attachments

Honestly, I rarely reach for screenshots myself — trace files already capture every action, network call, and DOM snapshot, so the trace almost always has whatever I need to debug a failure. The one place screenshots earn their keep is the HTML report: a thumbnail next to a failing test is much faster to scan than opening a trace, especially when triaging CI runs. Calling page.screenshot() without a path returns the image as a Buffer, which you can hand straight to testInfo.attach() to surface it in the test report.

test("basic test", async ({ page }, testInfo) => {
  await page.goto("/");
  const screenshot = await page.screenshot();
  await testInfo.attach("screenshot", {
    body: screenshot,
    contentType: "image/png",
  });
});
Note

Test attachments can be very valuable when your tests are dealing with file uploads and downloads.

Todo

Include the browser name in the path

If you're running parallel tests with multiple browsers, including the browserName in the screenshot path keeps each browser's screenshots from clobbering the others'.

test("get started link", async ({ page, browserName }) => {
  await page.goto("https://playwright.dev/");
  await page
    .getByRole("link", { name: "Docs" })
    .screenshot({ path: `./docs-${browserName}.png` });
});
Note

The page, browserName and other test variables are called test fixtures. Playwright provides many fixtures for different use cases and we'll look at them later.

You only need this manual interpolation for page.screenshot(). toHaveScreenshot() includes the project name in the baseline filename automatically, so multi-browser projects don't collide.

Todo

Visual regression snapshots

Once you're comfortable taking manual screenshots, you can promote them to assertions with toHaveScreenshot() — Playwright's built-in visual regression check.

test("get started link", async ({ page, browserName }) => {
  await page.goto("https://playwright.dev/");
  // visual regression works on a page level...
  await expect(page).toHaveScreenshot("home.png");
  // but also on a locator level
  await expect(page.getByRole("link", { name: "Docs" })).toHaveScreenshot(
    "docs.png",
  );
});

toHaveScreenshot() stores baseline screenshots next to your test files in a <testfile>-snapshots/ folder and compares against them on future runs. The filename includes the project name and platform (e.g. home-chromium-darwin.png), so multi-browser projects produce one baseline per browser. Unlike page.screenshot(), toHaveScreenshot() auto-retries until two consecutive captures match before comparing — that's why it's the right tool for visual regression.

Note

Playwright by default disables animations and transitions when taking screenshots to avoid flakiness because of moving elements.

Todo

Creating and updating baselines

The first run of a toHaveScreenshot() assertion always fails because there's no baseline to compare against yet. It will be so kind to generate one for you, though.

Later on, generate new snapshots with the --update-snapshots flag (short: -u):

npx playwright test --update-snapshots

This writes any missing baselines and overwrites existing ones. Commit the resulting *-snapshots/ directories to version control — they're the source of truth your tests compare against.

When a visual regression fails, the HTML report shows a side-by-side diff with an overlay slider:

npx playwright show-report
Warning

Screenshots differ across operating systems, browser versions, and font rendering. Baselines generated on macOS will almost always fail on a Linux CI runner. Generate baselines in the same environment they'll run in. Either by running --update-snapshots on CI and commiting the snapshots, or by running tests locally inside the official Playwright Docker image.

Cross-platform rendering is a very annoying problem and that's why many people don't get into spending much time on implementing visual regression.

Screenshot configuration

screenshot() and toHaveScreenshot() share most options. The ones you'll reach for most often:

  • fullPage — capture the entire scrollable page (page-level only)
  • mask — pass an array of locators to overlay with a solid color, hiding dynamic content like timestamps or avatars
  • maxDiffPixels / maxDiffPixelRatio — how many pixels are allowed to differ before the assertion fails (absolute count vs. ratio)
  • threshold — per-pixel color tolerance, default 0.2
  • stylePath — inject a CSS file at capture time to hide flaky elements site-wide
  • animations'disabled' (default) or 'allow'
  • clip — capture a rectangular region

Set project-wide defaults in playwright.config.ts under expect.toHaveScreenshot so individual tests stay clean:

// playwright.config.ts
export default defineConfig({
  expect: {
    toHaveScreenshot: {
      maxDiffPixelRatio: 0.01,
      stylePath: "./tests/screenshot.css",
    },
  },
});

Per-test overrides still work — pass options as the second argument to toHaveScreenshot():

test("product listing", async ({ page }) => {
  await page.goto("/search");

  await expect(page).toHaveScreenshot("products.png", {
    mask: [page.getByTestId("product")],
  });
});

Hands on

Practice visual regression

Exercise 1 of 1

Mask the product catalog

  1. Navigate to the catalog at /search.
  2. Add visual regression testing and mask every product so the captured image hides the product tiles behind the default mask color