How to Use Snapshot Testing for End-to-End Tests
Add visual and accessibility snapshots to Playwright end-to-end tests, stabilize the captured state, review diffs, and update baselines safely.
Use a snapshot assertion after your end-to-end test has reached and verified a meaningful user-visible state. In Playwright Test, await expect(page).toHaveScreenshot('name.png') compares the rendered page with a reviewed baseline. A screenshot snapshot can catch visual changes; it does not prove that an interaction works or that the page is accessible. Keep functional assertions, and add ARIA snapshot assertions when accessible structure is part of the contract.
This guide uses Playwright Test because it provides built-in screenshot comparison. Its first run creates the reference image, and later runs compare new captures against it. For stable results, control the test data and rendering environment, inspect each diff, and update a baseline only after deciding the new output is correct. See the official Playwright visual comparisons documentation.
1. What snapshot testing checks
“Snapshot test” can mean several kinds of comparison. Be precise about what is being recorded and what a passing test establishes.
| Snapshot kind | Compared output | Useful for | Does not establish |
|---|---|---|---|
| Visual screenshot | Rendered pixels in a page or locator | Unexpected layout, styling, visibility, and visual regressions | That controls work, content is correct, or the UI is accessible |
| ARIA snapshot | Accessible roles, names, and hierarchy | Changes to accessible structure and labels | Visual appearance or complete accessibility conformance |
| Text or serialized-output snapshot | Text, JSON, or other serialized values | Stable structured output with a meaningful contract | That the user-facing page renders correctly |
Use an end-to-end test to reach the state through the user journey, then assert the behavior that matters. Add a visual or ARIA snapshot as a focused regression check. Playwright describes screenshots as visual comparisons and supports ARIA snapshots separately; those assertions complement each other rather than serving the same purpose (visual comparisons, ARIA snapshots).
2. Add a Playwright screenshot assertion
Install Playwright Test in a JavaScript or TypeScript project and create a test. The example below assumes the application is running at http://127.0.0.1:3000 and provides a deterministic checkout fixture. Replace the route and fixture setup with your application’s test environment.
npm install --save-dev @playwright/test
npx playwright install chromium
Create tests/checkout.spec.ts:
import { test, expect } from '@playwright/test';
test('checkout confirmation looks correct', async ({ page }) => {
await page.goto('http://127.0.0.1:3000/checkout?fixture=paid-order');
// Verify the journey reached the intended state before capturing it.
await page.getByRole('button', { name: 'Place order' }).click();
await expect(
page.getByRole('heading', { name: 'Order confirmed' })
).toBeVisible();
await expect(page.getByText('Order #TEST-1042')).toBeVisible();
await expect(page).toHaveScreenshot('order-confirmation.png');
});
Run the test with npx playwright test tests/checkout.spec.ts. On its first run, Playwright reports that the reference snapshot is missing and writes an actual screenshot. Review that image and add the generated tests/checkout.spec.ts-snapshots/ directory to version control if it represents the intended design. Subsequent runs compare against the checked-in reference. Playwright may capture repeatedly until consecutive screenshots match before saving the initial reference. Snapshot names normally include browser and platform information because rendering can vary across environments.
Make the test state meaningful
- Use a dedicated test route or known fixture data. Avoid depending on an order, account, or database record that changes between runs.
- Navigate and interact as a user would, then assert the expected result with web-first assertions such as
toBeVisible(). - Prefer user-facing locators such as
getByRole()and explicit labels. They express what the test is checking and are less tied to incidental DOM structure. - Keep the snapshot focused on a state that matters: a confirmation, validation error, menu, or other important interface checkpoint.
- Do not use a screenshot assertion as a substitute for assertions about saved data, navigation, calculations, or other behavior.
Playwright recommends isolated tests, user-visible behavior, and controlling test data. It also recommends avoiding dependence on third-party content that your test cannot control (Playwright best practices).
3. Choose page or element snapshots
A page snapshot checks the whole viewport by default. For a long page you can request a full-page image; for a smaller, high-value region you can use a locator snapshot. Element-level checkpoints often keep diffs easier to review when unrelated page sections change.
// The viewport (default)
await expect(page).toHaveScreenshot('checkout-viewport.png');
// The full document, including content below the fold
await expect(page).toHaveScreenshot('checkout-full-page.png', {
fullPage: true,
});
// A focused region
await expect(page.getByTestId('order-summary'))
.toHaveScreenshot('order-summary.png');
Use full-page capture when the page’s overall composition or below-the-fold content is part of the requirement. Use a locator when the important contract is a component, such as a dialog or order summary. Keep enough surrounding behavior assertions to show that the captured component is in the right state.
4. Stabilize the capture and tune comparisons
Visual output can change for reasons outside the application change you intend to detect. Keep the capture conditions repeatable before adjusting the diff threshold.
| Source of variation | Control to consider |
|---|---|
| Browser and operating system | Generate and compare baselines in the same browser, OS, browser version, and CI image. If you intentionally test multiple projects, expect separate rendering baselines. |
| Data and account state | Seed or reset test data for every run. Use a stable staging fixture rather than mutable production-like data. |
| Fonts and assets | Make sure fonts and required images have loaded before capture; keep installed fonts and browser dependencies consistent in local and CI environments. |
| Time-dependent UI | Freeze or control the clock where appropriate, and avoid displaying a live timestamp in the captured region unless it is the subject of the test. |
| Asynchronous rendering | Wait for a visible application-level condition, such as a heading or completed state. Avoid arbitrary sleeps as a general synchronization strategy. |
| Third-party content | Stub or control external responses, or exclude volatile regions from the visual contract. External content can change without an application change. |
| Animation and transient elements | Disable or mask a specific animation, cursor, or known volatile element when it is not under test; do not hide meaningful content just to silence a failure. |
Playwright supports screenshot options including fullPage, locator screenshots, animation handling, masking, custom styles, and comparison thresholds. A page-wide expectation can be configured in playwright.config.ts. Use only options your test needs and keep any exclusions visible in code review.
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
baseURL: 'http://127.0.0.1:3000',
browserName: 'chromium',
viewport: { width: 1280, height: 800 },
colorScheme: 'light',
},
expect: {
toHaveScreenshot: {
// An explicit tolerance. Start strict and raise only with a reason.
maxDiffPixels: 100,
},
},
});
Then tests can use the configured base URL with page.goto('/checkout'). Choose a viewport and browser that match the UI contract you want to protect. If mobile layout matters, add a separate project or test with an intentional mobile viewport rather than assuming a desktop snapshot covers it.
A threshold such as maxDiffPixels permits a stated number of differing pixels. It is a tolerance choice, not a fix for nondeterministic rendering. A loose threshold can hide meaningful changes. Start with the default or a strict value, inspect actual diffs, and set a documented tolerance only when small rendering variation is expected. See the screenshot assertion options.
5. Add an ARIA snapshot when structure matters
Screenshot comparison will not tell you whether the accessible name or role of a control changed. Playwright’s toMatchAriaSnapshot() compares the accessible structure against a template. Pair it with visual and behavioral assertions when those are all part of the contract.
import { test, expect } from '@playwright/test';
test('confirmation exposes its key actions', async ({ page }) => {
await page.goto('/checkout?fixture=paid-order');
await page.getByRole('button', { name: 'Place order' }).click();
await expect(
page.getByRole('heading', { name: 'Order confirmed' })
).toBeVisible();
await expect(page.getByRole('main')).toMatchAriaSnapshot(`
- main:
- heading "Order confirmed" [level=1]
- paragraph: "Order #TEST-1042"
- link "View receipt"
`);
await expect(page.getByRole('main'))
.toHaveScreenshot('confirmation-main.png');
});
Adjust the template to the accessible tree your page actually exposes. ARIA snapshots support partial matching for cases where a label or attribute is intentionally outside the contract. They do not evaluate visual layout or replace broader accessibility checks. Consult the ARIA snapshot guide for supported matching syntax and update workflow.
6. Review diffs and update baselines deliberately
- Run the failing test and open the actual image and diff produced by the test runner or its report.
- Confirm the test reached the intended state. Check the behavior assertions, fixture data, loaded fonts and images, and browser environment.
- Classify the change: application regression, environment or data drift, or intentional design update.
- If it is a defect or drift, fix its cause and rerun against the existing baseline.
- If the design change is intentional, review the new image at the affected viewport, update the baseline, and include the snapshot change with the related code change.
To regenerate screenshots after review, run npx playwright test --update-snapshots. Do not make automatic baseline acceptance the normal CI response to a failure: it can convert an unnoticed regression into the new expectation. Review snapshot files alongside code, just as you review any other test artifact.
7. Troubleshooting flaky or failing snapshots
| Symptom | Likely cause | What to do |
|---|---|---|
| The same test produces different diffs on repeat runs | Uncontrolled data, a live clock, asynchronous content, animation, or external content | Make the fixture deterministic; wait for an application state; freeze time if relevant; control third-party responses; mask or disable only specific irrelevant animation. |
| Local passes, CI fails | Different OS, browser version, installed fonts, rendering mode, viewport, or dependencies | Generate and compare in the same CI image and browser project. Keep browser and operating-system versions aligned with baseline creation. |
| Text wraps differently or spacing shifts | A font did not load, a font differs between machines, or viewport dimensions differ | Ensure the intended font is available and loaded, and set a fixed viewport. Avoid updating the baseline until you identify which environment produced the intended rendering. |
| Screenshot is blank or incomplete | The page or component had not reached its ready state, navigation failed, or the fixture did not render | Assert a meaningful heading or ready marker before the capture. Inspect the test report and network/application logs; fix navigation or fixture setup. |
| A full-page snapshot has many unrelated diffs | The page includes volatile or low-value regions | Use a focused locator snapshot or split important states into smaller checks. Stabilize or explicitly exclude only known dynamic regions. |
| Threshold hides changes you expected to catch | The allowed pixel count is too high for the region | Reduce the threshold and inspect whether an element-level assertion gives a clearer contract. Do not increase tolerance just to force a green run. |
| Snapshot is missing or has the wrong name | It has not been generated, is in another project’s snapshot directory, or the test name/path changed | Run the test once to generate the reference, check the associated *-snapshots directory, and inspect project and snapshot naming configuration. |
| Diff appears after changing browser or OS | Rendering is environment-specific | Use a consistent capture environment or intentionally create and review separate project baselines for the environments you support. |
8. Choose a local or hosted visual testing workflow
For a small project, Playwright’s built-in comparison or a local Cypress visual testing plugin can keep capture and baseline review close to the application and its CI. Hosted services may offer cross-browser or responsive rendering and review dashboards, depending on the service. Cypress lists integrations including Applitools Eyes, Chromatic, Happo, LambdaTest SmartUI, Percy, Sauce Labs Visual, SmartBear VisualTest, and Wopee.io as examples; this list is not an endorsement. Check each vendor’s current documentation and terms before choosing.
| Decision axis | Questions to answer |
|---|---|
| Framework and browser coverage | Does it support your test framework and the browser or device matrix you need? |
| Rendering and baseline location | Do captures run locally, in your CI, or in a hosted environment? Where are baselines stored? |
| Comparison approach | Is the comparison pixel-based, assisted, or both? Which types of change can reviewers understand? |
| Review workflow | Can the team inspect, discuss, and approve changes in the workflow it already uses? |
| Control of dynamic content | Can you keep data, time, fonts, and third-party dependencies sufficiently stable? |
| Operational overhead | What configuration, CI setup, access control, storage, and ongoing review does the team need to own? |
Keep end-to-end snapshots for stable, user-visible states where layout or accessible structure matters. Use functional assertions for behavior, and compare tools against your actual browser coverage, environment control, review workflow, and operational needs. Cypress’s guidance similarly emphasizes meaningful checkpoints and focused diffs (Cypress visual testing).
9. Or skip the browser setup
If you need a clean screenshot of a live URL for a report, preview, or AI workflow rather than an assertion inside your end-to-end test, ScreenshotNeo is a website screenshot API and MCP server. A GET request returns an image or PDF. For automated regression tests, keep the capture in your test runner so the state, assertions, and baseline stay together. ScreenshotNeo is useful when you want a URL capture without setting up and maintaining a browser capture script.
Install the Python dependency with python -m pip install requests, set YOUR_API_KEY to your API key, and run:
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)
Equivalent cURL and Node.js calls:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
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 Bun.write('shot.webp', res);
See the ScreenshotNeo API documentation for request options and response details. Its cookie and consent banner handling removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. The 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 shots per month with no card. Paid plans start at $5 for 3,000 shots; higher plans are Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000, and Business at $249 for 1,000,000. Yearly billing gives two months free, and all features are included on every plan. Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
10. FAQ
Does a passing screenshot mean the feature works?
No. It means the captured pixels matched within the configured comparison tolerance. Assert the underlying action and result separately.
Should every end-to-end test have a screenshot?
No. Add snapshots to a small set of stable, important visual checkpoints. Broad snapshot coverage can create a large baseline review burden without clarifying the behavior under test.
Can ARIA snapshots replace visual snapshots?
No. ARIA snapshots check accessible structure, while screenshots compare rendered appearance. Use the one that matches the contract, or both when both matter.
Can I share one baseline across browsers?
Usually the rendered output differs by browser and operating system. Keep capture environments consistent or intentionally maintain reviewed baselines for each test project.
When should I update a baseline?
After reviewing the difference and deciding the new rendering is intentional. A failing comparison alone does not say whether the application or the expected image is wrong.


