How to Add Visual Assertions to Functional Tests
Add screenshot comparisons after meaningful functional checks. Learn Playwright and Cypress workflows, stabilize visual tests, and diagnose common failures.
A visual assertion compares a rendered page or component with an approved screenshot. Add it after your functional test has reached and verified the state you want to protect. Keep functional and accessibility checks too: a screenshot diff checks appearance, but it cannot prove that behavior or accessibility is correct.
1. Choose a meaningful visual checkpoint
First drive the application into a stable, user-visible state. Then assert that the expected state exists and compare its appearance. For example, after submitting a form, check that its success message appears before taking the screenshot. For a dialog, verify it opened before capturing it.
Choose the screenshot scope to match what you own:
- Component or locator: use when the contract belongs to one component. Smaller diffs are usually easier to review and assign.
- Whole page: use when page layout, relationships between regions, or a complete user-visible state matters.
Do not add screenshots to every functional test by default. Each checkpoint creates a visual result that someone must review and maintain.
2. Add a screenshot assertion with Playwright Test
Playwright Test includes screenshot assertions for pages and locators. Install Playwright Test and its browser as described in the official installation guide. The following is a complete test file for a local app that serves a page at / with a “Welcome” heading:
import { test, expect } from '@playwright/test';
test('home page visual appearance', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
await expect(page).toHaveScreenshot();
});
Run it with npx playwright test. On the first run, Playwright creates a reference screenshot. Review that image and commit it only if it represents the intended appearance. Later runs compare the current render with the stored reference. When a UI change is intentional, inspect the new rendering and update the reference deliberately with npx playwright test --update-snapshots; do not use baseline updates to silence an unexplained difference.
For a focused component, use a locator screenshot assertion:
import { test, expect } from '@playwright/test';
test('success notice appearance', async ({ page }) => {
await page.goto('/form');
await page.getByLabel('Email').fill('person@example.com');
await page.getByRole('button', { name: 'Submit' }).click();
const notice = page.getByRole('status');
await expect(notice).toContainText(' submitted');
await expect(notice).toHaveScreenshot();
});
Adjust the expected text and route to your application. The semantic assertion documents the expected state; the screenshot assertion protects its appearance. See Playwright’s visual comparisons documentation for screenshot assertions and snapshot configuration.
Keep the rendering repeatable
Visual snapshots can vary with viewport, browser and operating system, installed fonts, display scale, data, animation, and third-party content. Keep those inputs consistent between baseline creation and comparison. Use stable fixtures or intercepted API responses for changing data, and choose a fixed viewport for the test project. Wait for the UI state you intend to capture rather than relying on an arbitrary delay.
If a changing region cannot be controlled, mask only that region using Playwright’s screenshot assertion options. Keep masks narrow: a large mask can hide a genuine regression. Prefer controlling the data or removing the unstable dependency when practical. Avoid relaxing whole-page comparison thresholds to address one noisy widget.
3. Add visual comparison to Cypress tests
Cypress’s built-in cy.screenshot() captures an image; it does not compare the image with a baseline. The comparison step therefore comes from a visual-testing integration or another project-specific comparison workflow. Cypress documents integrations including Applitools, Argos, Chromatic, Happo, LambdaTest SmartUI, Percy, Sauce Labs Visual, SmartBear VisualTest, and Wopee.io. That list establishes integration options, not their current pricing or comparative quality. Check each service’s current documentation and terms before choosing one.
The framework-neutral test shape is:
describe('form submission', () => {
it('shows the submitted state', () => {
cy.visit('/form');
cy.get('[name="email"]').type('person@example.com');
cy.contains('button', 'Submit').click();
cy.get('[role="status"]').should('contain.text', ' submitted');
// Replace with the snapshot command from your selected comparison integration.
// The integration should capture and compare this settled state to its baseline.
});
});
Keep the snapshot command after the state assertion. A capture without a comparison can be useful as an artifact, but it is not a visual assertion. For a focused and controlled component state, Cypress Component Testing may also fit the job.
Read Cypress’s visual testing guide for its capture and integration model. The specific command, baseline storage, review workflow, browser coverage, and cost depend on the integration you select.
4. Reduce flaky visual diffs
- Wait for the intended state. Assert that relevant content is visible and data updates have completed before capture. Avoid snapshots during loading transitions or animation frames.
- Make data deterministic. Use fixtures or intercepted responses so timestamps, randomized values, and changing server data do not drift between runs.
- Fix the rendering environment. Use consistent browser and operating system versions, viewport, fonts, and display scaling for baseline generation and CI comparison where possible.
- Control third-party regions. Prefer removing or stubbing external content. If that is not possible, mask only the small unstable area.
- Choose the narrowest useful scope. A locator assertion reduces unrelated changes; a page assertion can catch layout interactions across regions.
- Review every baseline update. Treat the reference as an approved record of appearance, not proof the current UI is correct.
Do not respond to noise by broadly increasing tolerance or masking large parts of the page. First identify which rendering input changed and whether that change is expected.
5. Keep functional, visual, and accessibility checks distinct
- Functional assertions check behavior or state: a request succeeded, text appeared, a control is visible, or a class changed.
- Visual assertions compare rendered pixels or another visual representation with an approved reference. They can reveal missing styling, overlap, layout shifts, or rendering failures.
- Accessibility checks evaluate semantics and accessibility requirements. A pixel comparison does not establish adequate contrast or usability with assistive technology.
Use the checks together where the user experience calls for them. Playwright’s accessibility testing guidance describes accessibility checks as a separate concern; its ARIA snapshots check accessible structure and are distinct from image comparisons. Cypress likewise describes accessibility testing as a companion to visual testing in its accessibility testing guide.
6. Troubleshoot common failures
| Symptom | Likely cause | What to do |
|---|---|---|
| First Playwright run has no baseline | The reference image has not been created yet. | Review the generated screenshot and create the baseline only if it is the intended state. |
| Diff appears on every run | Dynamic data, animation, font loading, browser or OS differences, or third-party content. | Stabilize data and rendering inputs; wait for the settled state; isolate or narrowly mask uncontrollable content. |
| Screenshot shows a loading or intermediate view | The test captured before the meaningful state finished rendering. | Wait for a specific user-visible condition or response-driven state, then capture. |
| Whole-page diff is hard to diagnose | The assertion covers unrelated regions or a broad layout change. | Use a locator assertion for component-owned behavior, while retaining page coverage for layout concerns that cross regions. |
| Cypress saves an image but the test passes despite changes | cy.screenshot() captures but does not compare against a baseline. |
Add a visual comparison integration and invoke its snapshot/comparison command after the functional assertion. |
| Baseline update hides an unexpected regression | The reference was refreshed without reviewing the visual change. | Inspect the diff, determine whether the change is intended, and update the approved reference only after review. |
7. Performance, reliability, and cost considerations
Each visual checkpoint adds capture and comparison work and creates an artifact that requires review. Keep checkpoints focused on important pages, shared components, and states likely to regress. A locator screenshot can reduce unrelated diffs and review effort; page screenshots provide broader layout coverage. There is no universally correct number of snapshots: balance the value of coverage against CI time and ongoing baseline review.
Reliability depends on repeatable rendering as much as the comparison tool. Pin or standardize the browser environment used for references and CI, control test data, and make baseline changes visible in code review or the chosen service’s review flow. For a team using Playwright Test, its built-in assertions are a natural starting point when local references and the team’s review process are sufficient. For Cypress, choose an integration because the built-in screenshot command does not compare. Consider a hosted service when managed baselines, review dashboards, cross-browser rendering, or pull request workflows address a real team need. Compare framework and language support, page and element capture, baseline management, browser coverage, dynamic-region handling, diff approval, CI integration, and current terms.
Or skip the browser setup
If you need a clean screenshot artifact or want to capture a URL outside the test runner, ScreenshotNeo provides a website screenshot API and MCP server. This one-call request captures a page as an image; it is not a replacement for a baseline comparison assertion inside Playwright or Cypress.
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. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000 screenshots. Sign up free for 1,000 screenshots a month, with no card required.
FAQ
How do I compare screenshots in Playwright?
Use Playwright Test’s page or locator screenshot assertion after the test reaches the intended state. Review the initial reference and subsequent diffs before accepting baseline updates.
Does Cypress compare screenshots?
No. Its built-in cy.screenshot() captures an image; comparison against a baseline requires an integration or separate comparison workflow.
Do visual tests replace accessibility tests?
No. Image comparisons do not verify accessible names, keyboard behavior, contrast requirements, or screen-reader usability. Keep focused accessibility checks.
Should every functional test have a screenshot?
No. Add visual checkpoints where appearance is part of the user-facing contract and the resulting diff is worth reviewing.


