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 });
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" });});
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",
});
});
Test attachments are very useful when your tests deal with file uploads and downloads.
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` });});
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.
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.
By default, Playwright disables animations and transitions when taking screenshots to avoid flakiness caused by moving elements.
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
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 avatarsmaxDiffPixels/maxDiffPixelRatio: how many pixels are allowed to differ before the assertion fails (absolute count vs. ratio)threshold: per-pixel color tolerance, default0.2stylePath: inject a CSS file at capture time to hide flaky elements site-wideanimations:'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")], });});
Practice visual regression
Mask the product catalog
- Navigate to the catalog at
/search. - Add a visual regression test and mask every product so that the captured image hides the product tiles behind the default mask color.