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 because the trace files already capture every action, network call, and DOM snapshot. Playwright traces almost always have everything I need to debug a failure. The one place where screenshots help me is the HTML report. A thumbnail next to a failing test is much faster to scan than opening a trace, especially when it's a recurring issue you've seen before.

If you call page.screenshot() without a path, it returns the image as a Buffer, which you can hand straight to testInfo.attach() to show it in the test report. That's pretty neat!

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 are very useful when your tests deal 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 prevents the browsers from overwriting each other's screenshots.

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(). Visual regression with 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 turn them into visual regression checks with toHaveScreenshot().

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() retries until two consecutive captures match, so the page is stable before it's compared to the baseline.

Note

By default, Playwright disables animations and transitions when taking screenshots to avoid flakiness caused by moving elements.

Todo

Creating and updating baselines

The first run of a toHaveScreenshot() assertion always fails because there are no baseline images to compare against yet. Playwright is kind enough to generate them 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 baseline 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.

You can either run --update-snapshots in CI and commit the snapshots there, or run the tests locally inside the official Playwright Docker image.

Cross-platform rendering is a very annoying problem, and that's why many people don't invest much time in visual regression testing.

Screenshot configuration

screenshot() and toHaveScreenshot() share most options. The ones you'll use 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

You can set project-wide defaults in playwright.config.ts under expect.toHaveScreenshot, too. This keeps your assertions clean:

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

You can still override the options in individual toHaveScreenshot() calls if needed.

test("home page", async ({ page }) => {  await page.goto("/");  await expect(page).toHaveScreenshot("home.png", {    mask: [page.getByTestId("geo-location")],  });});

Hands on

Practice visual regression

Exercise 1 of 1

Mask the product catalog

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