How to Add Visual Testing to Functional Tests
Add screenshot assertions to Playwright functional tests, review baselines, stabilize rendering, and run visual checks reliably in CI.
A functional test checks whether an interaction works; a visual test checks whether the resulting interface looks right. Add both to the same Playwright Test flow: reach a meaningful UI state, assert its behavior, then use toHaveScreenshot() to compare the page or a focused component against a reviewed reference image.
Playwright creates a reference screenshot when you first establish one, then compares later captures against it. Keep those reference images under review in version control: a changed screenshot may be a regression, an intentional design change, or rendering noise. [Playwright screenshot assertions](https://playwright.dev/docs/test-snapshots)
1. Add a screenshot assertion to an existing test
Install Playwright Test if the project does not already use it, and add the assertion after the test has reached the state you want to protect. The example below checks a checkout summary after a functional assertion confirms the expected heading is present.
import { test, expect } from '@playwright/test';
test('checkout summary looks correct', async ({ page }) => {
await page.goto('/checkout');
// Keep behavior and appearance checks together.
await expect(page.getByRole('heading', { name: 'Your order' })).toBeVisible();
await expect(page.locator('[data-testid="order-summary"]'))
.toHaveScreenshot('order-summary.png');
});
Use a locator assertion when the intended contract is a component. It captures only that element, which usually makes failures easier to understand and reduces unrelated page changes. Use a page assertion when the page-level composition, spacing, or navigation is what matters:
await expect(page).toHaveScreenshot('checkout.png');
Choose a stable, meaningful state: for example, after navigation, after loading a known product, or after opening a dialog. Avoid taking the screenshot before the UI has settled. Playwright’s screenshot assertion waits for two consecutive screenshots to match while generating or comparing a snapshot, but deterministic test data and state still matter. [Playwright screenshot assertions](https://playwright.dev/docs/test-snapshots)
2. Generate, inspect, and commit the baseline
- Run the visual test once in the environment you intend to use for creating baselines. Playwright reports that a new snapshot is needed and writes the reference image.
- Open the generated image and verify it shows the intended state, with the expected content and no transient loading or error state.
- Commit the reference image together with the test. Treat it as a reviewed test artifact, just like the assertion itself.
- When an intentional UI change updates the expected appearance, regenerate snapshots with
npx playwright test --update-snapshots, inspect every changed image, and commit those updates with the implementation.
Do not update a baseline simply because a test failed. First inspect the diff and decide whether the change is an unintended regression, a planned product change, or an environment difference. A baseline that is automatically accepted after every failure stops detecting regressions.
3. Make captures reproducible
Visual comparisons depend on rendering conditions. Playwright identifies the host operating system, browser version, browser settings, hardware, power source, and headless mode as factors that can affect screenshots. Produce and compare baselines in the same environment; matching the OS and browser version is especially important. [Playwright screenshot assertions](https://playwright.dev/docs/test-snapshots) and [visual testing best practices](https://playwright.dev/docs/best-practices)
- Pin the browser setup: use the Playwright browser version installed for the project and keep CI’s browser installation aligned with it.
- Use predictable content: control test accounts, records, timestamps, and feature flags so the test reaches the same state on each run.
- Control animation and timing: wait for the state the test cares about rather than relying on arbitrary sleeps. If animation creates unstable frames, disable or finish it as part of test setup.
- Mask only irrelevant volatility: Playwright supports a stylesheet for screenshot assertions. You can use it to hide a changing timestamp or other known-noisy region, but apply it narrowly so it does not conceal real defects.
- Keep viewport and device settings stable: use the same configured viewport, device scale factor, and browser project for baseline generation and comparison.
For content that changes in a known region, the assertion can apply a capture-only stylesheet:
await expect(page).toHaveScreenshot('dashboard.png', {
stylePath: './tests/visual-stability.css',
});
/* tests/visual-stability.css */
[data-testid="volatile-clock"] {
visibility: hidden !important;
}
Keep the stylesheet specific to the screenshot. Hiding large page areas can make the comparison pass while an actual layout problem remains.
4. Tune screenshot comparison carefully
By default, Playwright compares screenshots at the pixel level. Its assertion options can set a per-pixel color threshold and a maximum number or proportion of differing pixels. These tolerances can help with minor rendering variation, but they can also permit meaningful regressions. Start with strict comparisons in a stable environment, then tune only after examining representative diffs.
await expect(page.locator('[data-testid="order-summary"]'))
.toHaveScreenshot('order-summary.png', {
// Example values only; calibrate against your UI and environment.
threshold: 0.2,
maxDiffPixels: 40,
});
| Option or technique | Use it for | Watch out for |
|---|---|---|
| Locator screenshot | Protecting a component or visual contract with a clear boundary | Changes outside the element are intentionally not checked |
| Page screenshot | Checking overall page composition and layout | More unrelated content can cause diffs |
stylePath |
Removing a known volatile region during capture | Overbroad hiding can mask regressions |
threshold, maxDiffPixels, maxDiffPixelRatio |
Allowing a carefully measured amount of rendering variation | Loose values can make the assertion insensitive to defects |
| Named snapshot | Making a component or state easy to identify in snapshot files | Keep names stable and descriptive |
Exact option behavior and supported arguments are documented in the [Playwright Test API](https://playwright.dev/docs/api/class-snapshotassertions).
5. Run visual checks in CI
CI should install the project dependencies, install Playwright browsers and their required system dependencies, then run the same test suite used locally. A container can make the rendering environment more consistent. Playwright recommends one worker by default in CI to prioritize stability; teams with appropriate infrastructure can use sharding to run work in parallel. [Playwright CI guide](https://playwright.dev/docs/ci)
# Install project packages first, for example with npm ci.
npx playwright install --with-deps
npx playwright test
A minimal configuration can keep CI at one worker while retaining the project’s standard browser projects:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'github' : 'list',
use: {
trace: 'on-first-retry',
},
});
For a GitHub Actions workflow, use the official Playwright container or follow the installation sequence in the CI guide. Pin the container image to a Playwright release compatible with the version in your lockfile, and update them together. Store screenshot diffs, traces, and test reports as CI artifacts when the job fails so reviewers can inspect the rendered result and the steps that led to it.
Require snapshot changes to be reviewed alongside the UI change. Do not have CI blindly accept new baselines on failure. For broader parallel execution, Playwright supports sharding, but keep environment and snapshot ownership clear across jobs. [Playwright CI guide](https://playwright.dev/docs/ci)
6. Troubleshoot failed visual assertions
| Symptom | Likely cause | Fix |
|---|---|---|
| Snapshot is missing | The assertion has not generated a baseline in this checkout, or the snapshot was not committed. | Run the test in the chosen baseline environment, inspect the generated image, and commit it. |
| Many pixels differ on CI but not locally | Different OS, browser build, fonts, scaling, headless mode, or rendering hardware. | Align the environments and regenerate baselines only after confirming the environment is the source. |
| Screenshot includes a spinner or incomplete content | The test captured before the intended UI state was ready. | Wait for a meaningful locator or assert the relevant content before the screenshot; avoid arbitrary delays when a condition can be checked. |
| Diff changes on each run | Uncontrolled data, time, animation, ads, or another volatile region. | Make test data deterministic; narrowly mask known irrelevant volatility or disable the source in test setup. |
| Small tolerance hides a real change | Comparison thresholds are too permissive. | Reduce or remove tolerance and inspect representative diffs before settling on a value. |
| Snapshot update creates many unexpected files | The command updated more tests than intended or the test environment changed. | Review the full diff, run the affected test set deliberately, and revert unrelated snapshot changes. |
| Test times out during screenshot | The page, locator, or screenshot stabilization does not complete within the configured timeout. | Check navigation and state waits, investigate rendering or animation loops, and raise the timeout only when the slower behavior is expected. |
Use Playwright’s Trace Viewer to inspect CI failures and recover the actions, DOM state, and screenshots around the failure. Its CI guidance shows how to collect a trace on the first retry. [Trace Viewer](https://playwright.dev/docs/trace-viewer) and [Playwright CI guide](https://playwright.dev/docs/ci)
7. Decide whether to use native snapshots or a hosted workflow
Playwright’s built-in assertions are a straightforward starting point when you want the test and baseline files managed in the repository. Hosted visual-testing services may suit teams that need a centralized baseline workflow or shared diff review. The researched product documentation describes a Playwright integration for Chromatic, an Applitools Playwright SDK with named checkpoints and match controls, and Percy visual testing within BrowserStack. Check their current compatibility, pricing, and data-handling terms directly before adopting a service. [Chromatic Playwright documentation](https://www.chromatic.com/docs/playwright/), [Applitools Playwright documentation](https://applitools.com/docs/eyes/sdks/playwright/), [Percy documentation](https://www.browserstack.com/docs/percy)
Compare where baselines live, how reviewers approve diffs, which browsers and viewports are covered, how dynamic regions are handled, CI integration, and data requirements. A hosted service is not required for Playwright screenshot comparisons.
Or skip the browser setup
If you need screenshots for a visual check, report, or test fixture without maintaining a browser capture setup, ScreenshotNeo provides a website screenshot API and an MCP server. One GET request returns an image or PDF; see the ScreenshotNeo API documentation for options. ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed, and response headers indicate the page verdict and billing status. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://playwright.dev -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://playwright.dev"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://playwright.dev',
});
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 ScreenshotNeo for the service and the API documentation for request configuration. The free sign-up includes 1,000 screenshots a month with no card: create a free ScreenshotNeo account.
FAQ
Do visual tests replace functional assertions?
No. A screenshot comparison checks rendered appearance. Keep assertions for behavior and state, then add a screenshot assertion for the appearance that matters.
Should every page have a full-page snapshot?
No. Protect the visual contracts that matter. A focused locator assertion often produces a clearer signal than capturing every part of a page.
Can I update baselines automatically in CI?
Baseline changes should be inspected and reviewed. Automatically accepting each failure can turn a regression into the new reference.
Is a hosted visual testing service required?
No. Playwright Test includes screenshot comparison. A service is an optional workflow choice for teams that need centralized review or baseline management.


