How to Use Playwright’s toHaveScreenshot Assertion
Learn page and locator screenshot assertions in Playwright, manage baselines, reduce flaky diffs, and debug visual test failures.
toHaveScreenshot adds visual regression checks to Playwright tests. Use await expect(page).toHaveScreenshot('name.png') for a page or await expect(locator).toHaveScreenshot('name.png') for one element. Playwright waits for two consecutive screenshots to match, then compares the stabilized image with a stored baseline.
The assertion is part of the Playwright test runner. On the first run it creates a reference image. Later runs fail when the rendered result differs beyond the configured tolerances.
1. Install and create your first screenshot assertion
Install Playwright Test in a new project:
npm init playwright@latest
Choose TypeScript or JavaScript when prompted, then create a test such as tests/visual.spec.ts:
import { test, expect } from '@playwright/test';
test('landing page visual check', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot('landing.png');
});
test('submit button visual check', async ({ page }) => {
await page.goto('https://example.com/form');
const button = page.getByRole('button', { name: 'Submit' });
await expect(button).toHaveScreenshot('submit-button.png');
});
Generate the initial baseline with:
npx playwright test
Playwright stores snapshots in a directory associated with the test file (for example, a platform-specific snapshot directory next to visual.spec.ts). Commit those files with the test code. A later run compares the current capture with the committed reference.
2. Page screenshots versus locator screenshots
| Assertion | Captures | Use it for |
|---|---|---|
expect(page).toHaveScreenshot() |
The rendered page viewport, or the full page when configured | Routes, marketing pages, dashboards, and page-level layout |
expect(locator).toHaveScreenshot() |
The element matched by the locator | Components, cards, buttons, menus, and isolated regions |
Locator assertions usually produce smaller and more focused diffs. Use accessible locators so a markup change does not silently point the test at the wrong node:
const pricingCard = page.getByRole('article', { name: 'Pro plan' });
await expect(pricingCard).toHaveScreenshot('pricing-pro.png');
When a page contains several repeated components, give each snapshot a distinct name or use path segments:
await expect(page).toHaveScreenshot(['dashboard', 'desktop', 'overview.png']);
Snapshot names are resolved below the test file’s snapshot directory. Keep names and path segments within that directory.
3. Updating baselines safely
If a UI change is intentional, review the visual diff and then regenerate the reference:
npx playwright test --update-snapshots
Do not use this flag as a blind repair step. Inspect the generated expected, actual, and diff images first, then commit the changed snapshots in the same change as the UI update. In CI, run without --update-snapshots so unexpected changes fail the build.
You can update one test while iterating:
npx playwright test tests/visual.spec.ts --update-snapshots
4. Stabilizing screenshots before comparison
Visual assertions are sensitive to rendering inputs. Make the page deterministic before capturing it.
Disable or neutralize motion
animations: 'disabled' is the default. Finite animations are fast-forwarded and infinite animations are canceled for the capture. You can set it explicitly or add a stylesheet for app-specific transitions:
await expect(page).toHaveScreenshot('home.png', {
animations: 'disabled',
stylePath: 'tests/screenshot-styles.css'
});
/* tests/screenshot-styles.css */
*, *::before, *::after {
animation: none !important;
transition: none !important;
caret-color: transparent !important;
}
stylePath is applied while Playwright captures the screenshot. It can pierce Shadow DOM and inner frames, which makes it useful for hiding clocks, rotating banners, ads, and other dynamic content.
Remove hover and focus surprises
Hover effects are captured in the state that exists at assertion time. Move the mouse to a neutral point, or deliberately set the state you want:
await page.mouse.move(0, 0);
await page.getByRole('button', { name: 'Submit' }).focus();
await expect(page).toHaveScreenshot('focused-form.png');
The default caret: 'hide' setting prevents a blinking text caret from changing the image. Set caret: 'initial' only when the caret is part of what you intend to verify.
Wait for data and images
Wait for a meaningful application state instead of sleeping for an arbitrary duration:
await page.goto('https://example.com/dashboard');
await page.getByRole('heading', { name: 'Overview' }).waitFor();
await expect(page.locator('[data-testid="chart"]')).toHaveScreenshot('chart.png');
For pages that load content after navigation, wait for the relevant API response or selector. A short fixed delay can help with an unavoidable transition, but it is less reliable than waiting for the state that proves the page is ready.
5. Important screenshot options
The page and locator assertions share the screenshot options documented in the PageAssertions API and LocatorAssertions API.
| Option | Purpose | Typical use |
|---|---|---|
fullPage |
Captures the full scrollable page instead of only the viewport. | Long documents and landing pages. |
animations |
'disabled' stabilizes finite and infinite animations. |
Default for deterministic output. |
caret |
'hide' hides the text caret; 'initial' preserves it. |
Prevent blinking caret diffs. |
stylePath |
Injects a capture-only stylesheet. | Hide timestamps, ads, transitions, and dynamic widgets. |
timeout |
Maximum time for the assertion to retry. | Increase for slow pages; the default async expect timeout is 5,000 ms. |
maxDiffPixels |
Allows an absolute number of different pixels. | Small, known rendering noise. |
maxDiffPixelRatio |
Allows a proportion of differing pixels. | Responsive or large captures where a ratio is more meaningful. |
threshold |
Sets the perceived YIQ color difference threshold. | Minor anti-aliasing or color variation. |
scale |
'css' keeps one pixel per CSS pixel; 'device' uses device pixels. |
Use CSS scale for smaller, more portable baselines. |
pathTemplate and snapshotPathTemplate |
Control predictable output and snapshot locations. | Shared monorepos and custom artifact layouts. |
Example with several options:
await expect(page).toHaveScreenshot('account.png', {
fullPage: true,
animations: 'disabled',
caret: 'hide',
scale: 'css',
maxDiffPixelRatio: 0.001,
threshold: 0.2,
timeout: 10_000
});
Use tolerance settings after making the test environment deterministic. A large tolerance can hide a real regression.
6. Configure a consistent test environment
Playwright warns that operating system, browser version, browser settings, hardware, power source, and headless mode can affect rendering. Generate and compare baselines in the same environment whenever possible.
A basic configuration pins the browser project and a stable viewport:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
expect: {
timeout: 5_000
},
use: {
baseURL: 'https://example.com',
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 1,
colorScheme: 'light'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
]
});
Keep browser versions aligned between local development and CI. If your product intentionally supports dark mode or multiple breakpoints, create separate projects and snapshot names rather than letting one baseline represent every rendering context.
7. Handling dynamic content
Dynamic content is the most common source of flaky diffs. Use test data and capture-only styles to make it predictable:
- Seed the same database records before each test.
- Mock changing API responses, dates, random IDs, and rotating promotions.
- Hide timestamps, live counters, cursors, ads, and chat launchers with
stylePath. - Wait for the application’s loaded state or a specific selector.
- Use locator screenshots for stable components instead of comparing unrelated page regions.
Do not solve every instability by increasing maxDiffPixels or threshold. First remove the changing input; then allow only the small residual variation that remains.
8. Debugging failed visual assertions
| Symptom | Likely cause | Fix |
|---|---|---|
| Failure on the first run | The reference snapshot does not exist. | Run the test once to create it, then review and commit the snapshot. |
| Large diff after a browser or OS update | Font rasterization or rendering changed. | Compare in the pinned environment; regenerate baselines only after reviewing the change. |
| Text moves between runs | Fonts, responsive width, or asynchronous content are not stable. | Pin viewport and fonts, wait for the loaded state, and use deterministic fixtures. |
| Only a hover state differs | The pointer remained over an interactive element. | Move the mouse to a neutral location or intentionally assert the hover state. |
| Animated regions differ | An animation or transition is still active. | Use animations: 'disabled' and a stylePath rule for app-specific animation code. |
| Assertion times out | The page never reaches a stable pair of screenshots or the page is slow. | Wait for the correct selector/state, fix the underlying loading issue, or raise timeout for a genuinely slow route. |
| Diff is tiny but noisy | Anti-aliasing or subpixel color differences. | Keep the environment consistent, use scale: 'css', then tune threshold or a small pixel allowance. |
| Snapshot path is rejected | A name or path segment escapes the snapshot directory. | Use simple names and path segments that remain below Playwright’s snapshot directory. |
When a test fails, inspect Playwright’s actual, expected, and diff artifacts. The diff tells you whether the change is layout, content, color, typography, or timing related.
9. Performance and reliability practices
- Prefer locator assertions for component tests; full-page captures contain more pixels and more unrelated failure surface.
- Reuse authenticated storage state so each test does not repeat a login flow.
- Use a small, purposeful matrix of browsers and viewports instead of duplicating every snapshot across unnecessary combinations.
- Wait for application state, not long sleeps. This shortens passing tests and makes failures easier to diagnose.
- Keep snapshot files in version control and review them as code review artifacts.
- Run visual tests in a stable CI image with pinned Playwright browser binaries.
- Split large suites across workers only when the application data and test accounts are isolated.
Screenshot assertions are comparisons, not accessibility checks or functional assertions. Pair them with semantic assertions such as visible headings, roles, and expected URL or API behavior.
10. Or skip the browser setup
If you need a rendered image from a URL rather than a committed visual regression baseline, ScreenshotNeo provides a single HTTP request. See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', image);
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the shot was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with the 1,000 free monthly screenshots.
11. FAQ
Does toHaveScreenshot compare pixels exactly?
It compares the stabilized screenshot with the baseline using configurable pixel, ratio, and color thresholds. Cross-machine rendering can differ, so use a consistent environment.
Can I use it with regular Playwright?
The screenshot assertions require the Playwright test runner and its expect implementation.
Where should snapshots live?
Let Playwright create the snapshot directory next to the test, or configure a predictable location with snapshot path templates. Keep the resulting files under version control.
When should I use a locator instead of a page assertion?
Use a locator for a component or region whose appearance matters independently. Use a page assertion when the route’s overall layout is the behavior under test.
Should I update snapshots in CI?
No. Review intentional changes locally, run --update-snapshots, commit the reviewed files, and let CI compare against them.


