Skip to content

How to use Playwright for screenshot testing

Build Playwright screenshot tests with visual baselines, browser projects, stable waits, diff thresholds, and update review.

5 min readWebsite captureBy Temposhot Team

Screenshot testing compares how a page looks now with an approved image from earlier. If the pixels differ by more than an allowed amount, the test fails and shows the difference. Playwright Test has this built in through expect(page).toHaveScreenshot(), with no extra plugin. The method is also called visual regression testing.

When screenshot testing is worth it

Visual tests catch problems that DOM assertions miss: a stylesheet that failed to load, a button pushed off screen, a font that fell back to a system default. They are worth the maintenance when:

  • the UI has a design system or shared components used on many pages
  • CSS changes often and reviewers cannot check every page by hand
  • layout bugs have reached production before

They cost more than they return on pages full of live data, ads or user content. Each run would differ, and the suite turns into noise.

Install Playwright Test

npm init playwright@latest

The installer creates playwright.config.ts, an example test folder and an optional CI workflow, then downloads the browsers. Screenshot assertions need nothing else.

Create the first screenshot test

// tests/visual.spec.ts
import { test, expect } from '@playwright/test';

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

The first run has no baseline to compare against. Playwright writes the capture as the expected image and fails the test so that the new baseline gets reviewed. Run npx playwright test again and the test passes against the saved file.

When a later run differs, Playwright saves the expected image, the actual capture and a diff image with changed pixels highlighted. The HTML report from npx playwright show-report shows all three.

Test a smaller part of the page

test('pricing card', async ({ page }) => {
  await page.goto('https://example.com/pricing');
  await expect(page.getByTestId('pro-plan')).toHaveScreenshot('pro-card.png');
});

Element-level assertions are smaller and fail less often for unrelated reasons. A header change will not break the pricing card test. Use full-page assertions for a few key pages and element assertions for components.

Add browser and device projects

Projects in playwright.config.ts run the same tests in several browsers and device profiles.

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

export default defineConfig({
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    { name: 'mobile', use: { ...devices['iPhone 13'] } },
  ],
});

Each project keeps its own baselines. File names include the project and platform, for example home-chromium-darwin.png. Three projects therefore mean three baselines per assertion, so add projects only where layouts actually differ.

Make the page state stable

A screenshot is only comparable if the page reaches the same state every run.

  1. Wait for a ready element, such as a heading or a loaded image, before the assertion.
  2. Use fixed test data instead of live accounts.
  3. Set a fixed viewport in the config.
  4. Wait for web fonts with await page.evaluate(() => document.fonts.ready).

toHaveScreenshot already retries until two consecutive captures match, and it stops CSS animations by default. Those two behaviors remove much of the timing noise without extra code.

Hide or mask volatile elements

await expect(page).toHaveScreenshot('dashboard.png', {
  mask: [page.locator('.timestamp'), page.locator('.user-avatar')],
});

Masked areas are covered with a solid box in both images, so their content is ignored. For elements that should disappear entirely, pass a stylesheet with the stylePath option and hide them there. Masking is better than deleting elements, because the layout around them still gets checked.

Set a diff threshold carefully

OptionMeaning
maxDiffPixelsNumber of pixels allowed to differ.
maxDiffPixelRatioShare of pixels allowed to differ, from 0 to 1.
thresholdHow different one pixel's color must be to count, from 0 to 1. Default 0.2.

Keep tolerances low. A maxDiffPixelRatio of 0.01 allows 1 percent of pixels to change, which on a 1280×4000 full-page capture is more than 50,000 pixels. That is enough to hide a missing icon. Start with maxDiffPixels around 100 and adjust only when a specific false failure demands it.

Review and update baselines

When a design change is intended, regenerate the affected baselines:

npx playwright test --update-snapshots

Limit updates to the failing tests by passing a file name or -g with the test title. A blanket update across the whole suite makes accidental changes easy to miss.

Run screenshot tests in CI

Baselines made on macOS will not match captures from a Linux runner, because fonts and anti-aliasing differ. The reliable setup is to generate and compare baselines in the same environment, usually the official Playwright Docker image at the same version as the installed package.

A workable routine:

  1. Developers run the visual suite inside the Docker image, not directly on macOS.
  2. CI runs the same image and uploads the HTML report as an artifact on failure.
  3. Intended changes get new baselines, committed with the code change.
  4. Reviewers check the image diffs in the pull request.

Where plain screenshot automation still fits

Visual tests answer one question: did this page change compared to its baseline. They do not produce a review set for a design meeting, a client report or a dated archive, and people outside engineering rarely run them.

For those jobs, a plain capture tool is simpler. Temposhot Website Capture takes a URL list on a Mac and captures each page at Desktop 1920×1080, Tablet 768×1024 or Mobile 390×844 with no test code. To compare two captures by eye, the Compare Images highlights differences in the browser. The screenshot comparison entry covers the general method.

Common mistakes

  • Committing baselines generated on a laptop and comparing them in Linux CI.
  • Asserting full pages that include dates, carousels or ads.
  • Raising thresholds to silence a flaky test instead of fixing the page state.
  • Running --update-snapshots for the whole suite after one intended change.
  • Adding many device projects before the desktop baselines are stable.

FAQ

Where are baseline images stored?

By default in a folder next to the test file, named after the test file with a -snapshots suffix. The snapshotPathTemplate config option changes the location.

Can I compare screenshots without Playwright Test?

Yes. Capture with any tool and compare the files with an image diff tool. Playwright Test adds retries, masking and reporting on top of that comparison.

Should screenshot tests run on every pull request?

For component-level assertions, usually yes. Large full-page suites can run on a schedule or before releases if they slow down the pipeline.

References

Need no-code website screenshot automation?

When the goal is a set of screenshots rather than a pass or fail result, Temposhot Website Capture runs a URL list locally on a Mac. See website screenshot automation or the Website Capture quick start.

playwrightvisual-testingscreenshot-comparisonci

Terms used here: playwright, responsive screenshot, screenshot automation, screenshot comparison, viewport screenshot, visual regression testing

Turn repeatable screenshots into finished work

Temposhot captures on a timer or on every click and exports the result as a PDF, images, ZIP or MP4.

Related posts