Visual Regression Testing in Playwright with toHaveScreenshot

Functional tests miss broken layouts, missing icons and CSS mistakes. Playwright catches them with built-in visual comparisons. In this lesson you will create baselines, compare pages and components, tune tolerances, mask dynamic content and keep results consistent in CI.

Advertisement

📖 What You’ll Learn: what visual regression testing is, toHaveScreenshot for pages and elements, how baselines are stored and updated, maxDiffPixels and threshold, masking and animations, stylePath, cross-platform consistency, and text and ARIA snapshots.


👀 What Is Visual Regression Testing?

Functional tests check that things work: the button submits, the total is correct. They do not notice that the button is now white on white, the logo moved, or a CSS change broke the layout. Visual regression testing catches those problems by comparing screenshots of your application with approved baseline images, pixel by pixel.

Playwright has visual testing built in with toHaveScreenshot(). No extra library or paid service is needed.


🖼️ Your First Visual Test

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

test('login page looks correct', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await expect(page).toHaveScreenshot('login-page.png');
});

What happens on each run

  1. First run: there is no baseline yet, so the test fails with a message that the snapshot does not exist, and Playwright writes the screenshot as the new baseline.
  2. Second run: Playwright takes a new screenshot and compares it with the baseline. If they match, the test passes.
  3. When the UI changes: the test fails and the HTML report shows Expected, Actual and Diff images, with a slider to compare them.

Baselines are saved next to the test file:

tests/
├── visual.spec.ts
└── visual.spec.ts-snapshots/
    ├── login-page-chromium-win32.png
    ├── login-page-firefox-win32.png
    └── login-page-webkit-win32.png

The file name includes the project (browser) and the platform, because fonts and rendering differ between browsers and operating systems.

💡 Tip: Commit the -snapshots folders to Git. Baselines are part of your test suite, and reviewers can see image changes in pull requests.


🔄 Updating Baselines After an Intended Change

When the design changes on purpose, regenerate the baselines:

npx playwright test --update-snapshots
npx playwright test visual.spec.ts --update-snapshots   # only one file

Review the new images before committing them: an update accepts whatever the page looks like now, including bugs.


🧩 Screenshot a Single Component

Comparing the whole page makes tests sensitive to any change anywhere. Comparing one element keeps visual tests focused:

test('login form component', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await expect(page.locator('.login-box')).toHaveScreenshot('login-form.png');
});

test('full page including content below the fold', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await expect(page).toHaveScreenshot('login-full.png', { fullPage: true });
});

🎚️ Tolerances: Avoid False Failures

Tiny rendering differences (anti-aliasing, font smoothing) can produce a few different pixels. Control how strict the comparison is:

OptionMeaningExample
maxDiffPixelsAllowed number of different pixels100
maxDiffPixelRatioAllowed share of different pixels (0 to 1)0.01 (1 percent)
thresholdHow different a pixel’s colour must be to count (0 to 1, default 0.2)0.3
await expect(page).toHaveScreenshot('dashboard.png', { maxDiffPixelRatio: 0.01 });

Set defaults for every visual assertion in playwright.config.ts:

export default defineConfig({
  expect: {
    toHaveScreenshot: { maxDiffPixels: 50 },
  },
});

🎭 Handle Dynamic Content: Mask, Animations and Styles

Dates, ads, avatars, carousels and blinking cursors change on every run. Hide them so they do not cause failures:

await expect(page).toHaveScreenshot('home.png', {
  mask: [page.locator('.current-date'), page.locator('.ad-banner')], // covered with a solid box
  animations: 'disabled',  // finishes or stops CSS animations (default for toHaveScreenshot)
  caret: 'hide',           // hides the text cursor (default)
});

You can also inject CSS only for screenshots, for example to hide a chat widget:

await expect(page).toHaveScreenshot('home.png', {
  stylePath: 'tests/screenshot.css', // e.g. .chat-widget { visibility: hidden; }
});

📝 Note: toHaveScreenshot waits until two consecutive screenshots are identical before comparing, so pages that are still loading are handled automatically. Very dynamic pages still need masking.


🖥️ Consistent Results Locally and in CI

The most common visual testing problem: baselines created on Windows fail on a Linux CI machine because fonts render differently. Solutions:

  • Create baselines in the same environment as CI, for example in the official Playwright Docker image (mcr.microsoft.com/playwright). See Playwright with Docker.
  • Keep platform-specific baselines (the default naming already separates them by OS).
  • Run visual tests in one browser project (usually Chromium) to reduce the number of baselines.
  • Fix the viewport size in the config so layouts are identical.
// Run visual tests only in Chromium with a fixed viewport
projects: [
  { name: 'visual', testMatch: /.*visual\.spec\.ts/, use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 720 } } },
  { name: 'chromium', testIgnore: /.*visual\.spec\.ts/, use: { ...devices['Desktop Chrome'] } },
],

📄 Beyond Images: Text and ARIA Snapshots

  • expect(value).toMatchSnapshot('name.txt') compares text or binary data with a stored snapshot.
  • expect(locator).toMatchAriaSnapshot(...) compares the accessibility tree (roles and names) instead of pixels. It is less fragile than images for checking page structure.

✅ Visual Testing Best Practices

  • Start small: key pages and shared components (header, login form, checkout summary).
  • Prefer element screenshots over full pages.
  • Mask anything dynamic; never accept flaky baselines.
  • Review every --update-snapshots change like code.
  • Keep visual tests in a separate file or project so they can run on their own.

📝 Assignment

  1. Write a visual test for the Sauce Demo login page, run it twice, and find the baseline image in the -snapshots folder.
  2. Add an element visual test for .login-box.
  3. Break the baseline on purpose: run once with await page.setViewportSize({ width: 800, height: 600 }) before the screenshot, then open the HTML report and study the Expected, Actual and Diff views.
  4. Add maxDiffPixels: 50 as a default in the config.
  5. Mask the .login_credentials_wrap section and regenerate the baseline with --update-snapshots.
  6. Bonus: create a visual project that runs only *visual.spec.ts files in Chromium with a fixed viewport.

❓ FAQ

🤔 How do I do visual testing in Playwright?

Use await expect(page).toHaveScreenshot('name.png') or await expect(locator).toHaveScreenshot(). The first run creates the baseline; later runs compare against it.

🤔 Why does my visual test fail on the first run?

There is no baseline yet. Playwright writes the current screenshot as the baseline and reports the missing snapshot. Run the test again and it will compare against the new baseline.

🤔 How do I update Playwright screenshots?

Run npx playwright test --update-snapshots (or -u), review the new images, and commit them.

🤔 What is the difference between page.screenshot and toHaveScreenshot?

page.screenshot() just saves an image. toHaveScreenshot() is an assertion that compares the page with a stored baseline and fails if they differ. See screenshots and videos.

🤔 Why do visual tests pass locally but fail in CI?

Different operating systems and fonts render pixels differently. Generate baselines in the same environment as CI (for example the Playwright Docker image) and use small tolerances.


🎯 Next Steps

🎬 Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

2 thoughts on “Visual Regression Testing in Playwright with toHaveScreenshot”

Leave a Comment