Playwright Screenshot Testing: Capture Full Pages and Compare Changes
Capture full pages in Playwright Test, create visual baselines, and make screenshot comparisons reliable and useful.
To capture and compare a full page in Playwright Test, navigate to the intended page state and assert await expect(page).toHaveScreenshot({ fullPage: true }). The first run creates a reference image; later runs compare against it. For a screenshot file without a visual assertion, use await page.screenshot({ path: 'page.png', fullPage: true }). Keep baseline generation and comparisons in the same rendering environment to avoid differences caused by the operating system, browser, settings, hardware, or headless mode. Playwright visual comparisons guide.
1. Set up a Playwright Test project
Screenshot assertions belong to the Playwright Test runner. If a project is not already configured, install the runner and its browser:
npm init playwright@latest
npx playwright install chromium
Choose TypeScript or JavaScript in the setup prompts. The following example is TypeScript; in a JavaScript test, remove the type annotations and use a .spec.js filename.
2. Capture a full page and compare it with a baseline
Create tests/homepage.visual.spec.ts:
import { test, expect } from '@playwright/test';
test('homepage full-page visual baseline', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('http://127.0.0.1:3000/', { waitUntil: 'networkidle' });
// Establish a deterministic state before capturing.
await page.getByRole('heading', { name: 'Welcome' }).waitFor();
await expect(page).toHaveScreenshot('homepage-full.png', {
fullPage: true,
animations: 'disabled',
});
});
Run it with npx playwright test tests/homepage.visual.spec.ts. On the first run, Playwright reports that the expected snapshot does not exist and writes an actual image for review. Inspect that image before accepting the baseline, then commit the test and its generated snapshot directory. On subsequent runs, Playwright takes screenshots until two consecutive captures match, then compares the last capture with the stored reference. The assertion retries while the image differs; a mismatch eventually fails the test and produces actual/expected/diff artifacts in the test output.
The name can be omitted, but an explicit name documents what the image covers. PNG is the default; a name ending in .webp stores a lossless WebP snapshot. Snapshot paths must remain inside the snapshot directory associated with the test file. See PageAssertions documentation.
3. Choose between a visual assertion and a screenshot file
| Goal | Use | What it does |
|---|---|---|
| Detect unintended visual changes in a test | expect(page).toHaveScreenshot() |
Compares with a managed baseline and fails the test when the difference exceeds configured tolerance. |
| Save or share an image | page.screenshot() |
Writes an image or returns image bytes; it does not compare to a baseline by itself. |
A standalone full-page capture can be written as:
import { chromium } from '@playwright/test';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('http://127.0.0.1:3000/');
await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();
Run this from a project where @playwright/test is installed, with the target site available at that address. The screenshot API accepts additional options for format, scale, quality, clipping, and more; see the Page API. Quality is relevant to JPEG and WebP output, not PNG. For a visual baseline in Playwright Test, prefer toHaveScreenshot() over manually comparing a saved file.
4. Make the page state repeatable
A screenshot test is meaningful only when it captures the state the test is intended to protect. Before capture:
- Navigate to a stable test URL and wait for a page-specific landmark, such as a heading or loaded card.
- Set the viewport explicitly if responsive layout matters. Use separate tests or projects for distinct viewport sizes.
- Control data that changes between runs: use fixtures, seeded records, fixed test accounts, or a stable API response.
- Wait for meaningful content instead of relying only on a fixed sleep. A fixed delay can waste time and still race with slow content.
- For pages that lazy-load images or content while scrolling, verify the full-page capture includes the expected material. If the application loads content only on scroll, scroll through the relevant areas before taking the screenshot.
- Keep font files and other visual assets available in the same environment used for baseline creation.
fullPage: true captures the full scrollable page rather than only the viewport. It does not mean every application-specific interaction has happened; menus, expanded accordions, authenticated states, and content loaded only after scrolling must still be deliberately set up by the test.
5. Reduce noise without hiding regressions
Disable animations
animations: 'disabled' is the default for screenshot assertions. Finite animations are fast-forwarded to completion and infinite animations are canceled for the capture, then resume afterward. You can explicitly set it to 'allow' when animation appearance itself is what you need to test.
Mask volatile regions
Mask values that are expected to vary but are outside the visual behavior under test, such as a timestamp or rotating avatar. The mask covers the locator’s bounding box with pink by default:
await expect(page).toHaveScreenshot('dashboard.png', {
fullPage: true,
mask: [
page.getByTestId('updated-at'),
page.locator('.random-avatar'),
],
});
Masking means the underlying pixels are no longer checked. Do not mask large regions or components whose appearance is important. If a locator matches several dynamic items, narrow it to the intended element or deliberately mask all matches.
Apply a capture-only stylesheet
Use stylePath for consistent normalization when appropriate, for example to hide a volatile cursor or a third-party embed that is not part of the test’s scope. The stylesheet applies during screenshot capture, including to inner frames and shadow roots:
/* tests/visual-screenshot.css */
.test-only-clock,
.third-party-promo {
visibility: hidden !important;
}
import path from 'node:path';
import { test, expect } from '@playwright/test';
test('stable page capture', async ({ page }) => {
await page.goto('http://127.0.0.1:3000/');
await expect(page).toHaveScreenshot('page.png', {
fullPage: true,
stylePath: path.join(import.meta.dirname, 'visual-screenshot.css'),
});
});
Use a path expression supported by the Node.js version and module format in your project; in CommonJS projects, __dirname is commonly available. Hiding content can conceal a real defect, so keep the stylesheet focused and review it like test code. The visual comparisons guide documents capture stylesheets and shared screenshot settings.
Set a deliberate difference tolerance
By default, a difference in rendered pixels can fail the comparison. maxDiffPixels allows a bounded number of different pixels. It can be set for one assertion or shared in configuration:
await expect(page).toHaveScreenshot('page.png', {
fullPage: true,
maxDiffPixels: 50,
});
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 50,
},
},
});
Choose tolerance after examining real diffs in the project’s pinned environment. A large allowance can let genuine UI changes pass. Color threshold settings are another comparison control; consult the version-specific docs before relying on a setting not shown in your installed Playwright release. See the official options reference.
6. Manage snapshots in development and CI
- Generate the initial baseline from the same browser, operating system, fonts, and relevant settings that will run comparisons.
- Inspect the actual screenshot for missing images, incomplete content, overlays, and incorrect state.
- Commit baseline files with the test. Review snapshot diffs alongside source changes during code review.
- When an intended UI change is made, run
npx playwright test --update-snapshots, inspect the new images, and commit only the reviewed updates. - When CI uses a different browser or platform from local development, use project-specific baselines or generate and verify baselines in the CI rendering environment.
Snapshot naming includes the test and project/browser context by default. If your configuration has multiple projects, the project name is used in snapshot names. The path template can be customized through testConfig.snapshotPathTemplate. Avoid updating snapshots automatically on every failure: that turns a regression into an accepted baseline without review.
7. cURL, Python, and Node.js for a standalone screenshot API
Playwright’s screenshot capture and visual assertions run inside the Playwright browser/test workflow. cURL and Python are not alternate ways to call Playwright Test assertions; they are useful when you want to request a screenshot from an HTTP screenshot service. The following examples use ScreenshotNeo’s documented endpoint, with its API docs for parameters and response behavior.
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 request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Keep API keys out of committed source code; load them from your runtime’s secret configuration. These calls create a standalone screenshot. To make a repeatable visual regression check, store a trusted baseline and compare images in your own test workflow, or use Playwright Test’s managed screenshot assertions.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
See the ScreenshotNeo API documentation for request options. Sign up free for 1,000 screenshots a month with no card.
Performance, reliability, and cost
Keep full-page suites practical
A full-page image contains more pixels than a viewport capture, so it can take more time and storage to capture, compare, and review. Capture full-page images for flows where below-the-fold layout matters; use a focused locator screenshot or viewport capture for narrowly scoped components. Split a very long page into meaningful sections when one enormous diff becomes difficult to diagnose.
Screenshot assertions wait for matching consecutive captures, which helps with settling but does not make an unstable page deterministic. Remove avoidable sources of change before relaxing the comparison. Run only the browser/project combinations that provide useful coverage, and keep the rendering environment fixed so ordinary infrastructure changes do not churn baselines.
Make failures diagnosable
Use descriptive test and snapshot names, preserve test artifacts in CI when a comparison fails, and inspect the expected, actual, and difference images before changing thresholds or snapshots. A failure can reveal either a real visual change or a capture-state problem; the diff and page state help distinguish them.
Budget storage and review time
Playwright snapshots are files in the repository by default, so repository growth and review effort depend on how many pages, viewports, and projects you cover. Keep only useful baselines, name them clearly, and update them intentionally. No external screenshot service is required for Playwright’s native visual assertions. If you use a screenshot API for separate capture workflows, check that provider’s current plan and options; ScreenshotNeo’s listed tiers range from 1,000 free monthly shots to paid plans starting at $5 for 3,000.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| First run fails because the snapshot is missing | No expected image exists yet; this is baseline creation behavior. | Inspect the generated actual image, then commit the snapshot. Do not accept it without review. |
| Comparison fails although the page looks the same | Fonts, browser version, OS, headless mode, device scale, or other rendering settings differ. | Run baseline and comparison in the same environment, or maintain project-specific baselines. |
| Image contains a loading skeleton or missing media | The test captured before application content or assets finished loading. | Wait for a page-specific locator or application-ready condition. For content loaded on scroll, scroll it into view before the capture. |
| Intermittent diff around a timestamp, avatar, or rotating content | The page includes volatile visual data. | Use deterministic fixtures where possible; otherwise mask the smallest appropriate locator or normalize it with stylePath. |
| Assertion times out on a page with ongoing changes | Two consecutive screenshots never become identical, or the page remains visually active. | Disable or control animations, stop rotating content in test mode, wait for the intended state, and isolate volatile areas. |
| Only the first screen appears in the image | The call omitted fullPage: true or the content is not in the document’s scrollable page. |
Pass fullPage: true and verify that the target content is in the page flow and loaded before capture. |
| Baseline update changes many files | The run may use another browser/platform/project, or an intended global style change affected many pages. | Check project identity and environment, inspect every diff, and update only expected baselines. |
| Snapshot assertion method is unavailable | The test may not run under Playwright Test, or dependencies are out of sync. | Use @playwright/test, install the project’s browsers, and check the API available in the pinned version. |
| Screenshot is unexpectedly huge or slow to review | The full page is very long or includes content outside the behavior under test. | Use a focused capture, split the page into sections, or choose viewport coverage for that test. |
FAQ
Does toHaveScreenshot() work outside Playwright Test?
No. Screenshot assertions are part of the Playwright Test runner. Use page.screenshot() in a standalone script when you only need an image file.
Can I use JPEG for the assertion baseline?
Playwright’s screenshot assertions store PNG by default and support lossless WebP when the snapshot name ends in .webp. Use the installed version’s docs for supported assertion formats.
Should every page have a full-page baseline?
No. Cover the full page where below-the-fold layout is important. Use viewport or focused captures for narrower visual behavior.
Should I automatically update snapshots after CI failures?
No. Review the changed image first and update only when the UI change is intended.


