Visual, Snapshot, and Accessibility Testing

By Syncfusion®, Inc. · Last updated

Visual regression testing with toHaveScreenshot, baselines and --update-snapshots, masking dynamic content, thresholds and platforms, snapshot testing values with toMatchSnapshot, ARIA snapshots with ariaSnapshot and toMatchAriaSnapshot, automated accessibility scans with @axe-core/playwright, and keyboard checks axe cannot do.

Visual, Snapshot, and Accessibility Testing

  • A test can pass while the page looks broken. Text that turned white on white still "has the right text".
  • Visual tests compare how the page looks with a saved picture.
  • Snapshot tests compare values and page structure with a saved copy.
  • Accessibility tests check that everyone can use the page, including people with screen readers.

Simple lesson: save what "good" looks like once. Then every run compares against it.

A Real-World Example

  • The Practice Shop's Summer Sale banner must look right. It has a live countdown and a visitor counter that change all the time.
  • This test takes a screenshot of the banner and hides the parts that change.
  • Run it twice. The first run has nothing to compare with, so it saves a picture and fails. The second run compares and passes.

The lab cannot paint real pixels. It compares every visible element with its text and main styles (colour, size, weight, layout). A real run compares PNG images pixel by pixel. The workflow, options and messages are the same.

What You Will Learn

  1. How visual tests work: baselines, actual images and diffs.
  2. Updating baselines with --update-snapshots.
  3. Hiding dynamic content, and allowing small differences.
  4. Snapshot testing values with toMatchSnapshot().
  5. ARIA snapshots: checking the page's structure.
  6. Accessibility scans with axe-core, and what they miss.

Visual Testing with Screenshots

Baselines, Actuals and Diffs

  • The first time toHaveScreenshot() runs, there is no picture to compare with. Playwright saves the actual screenshot and fails with "A snapshot doesn't exist ... writing actual."
  • That saved picture is the baseline (also called the golden image). You commit it to git.
  • Every later run takes a new screenshot and compares. If too many pixels differ, the test fails and Playwright saves an expected, actual and diff image to look at.

Animation: run 1 saves a baseline picture of the banner. Run 2 takes a new picture, the heading colour changed, and the diff image highlights the heading in red, so the test fails

Baseline (run 1) Actual (run 2) Diff

Summer Sale

Summer Sale

1,204 pixels differ ✘

A CSS change turned the heading red. The diff shows exactly where.

The baseline is saved once. Each run compares a new picture with it, and the diff marks the changed pixels.

Your First Visual Test

  • await expect(page).toHaveScreenshot() takes the whole page. await expect(locator).toHaveScreenshot() takes one element.
  • Give the picture a name, like 'login-form.png'. Without one, Playwright makes a name from the test title.
  • Like other web-first assertions, it retries. It waits until two screenshots in a row are the same, then compares.
  • Run this box twice. Watch the output: the first run writes the baseline.
  • Real file names include the browser and the system: login-form-chromium-linux.png. Each browser and system gets its own baseline, because fonts render a little differently.

Catching a Visual Bug

  • Here, the second test adds a CSS rule, like a bad deploy would. Its screenshot uses the same name as the first test, so it is compared with the same baseline.
  • Run it twice. The second test fails, and the output lists what changed.
  • No text changed, so a toHaveText() check would still pass. Only a visual test catches this.
  • The helper masks() keeps both tests hiding the same parts. Page is the type of the page fixture.

Updating Baselines

  • When the design changes on purpose, the old baseline is wrong. Update it:
npx playwright test --update-snapshots
  • -u is the short form. It rewrites the baselines of the screenshots that changed.
  • --update-snapshots=missing only writes baselines that do not exist yet, and the run passes. Handy for a new test on CI.
  • --update-snapshots=all rewrites every baseline. none never writes any.
  • Look at the new images before you commit them. An update can hide a real bug if nobody checks.

Never run --update-snapshots just to make a red build green. First open the HTML report, compare the expected, actual and diff images, and make sure the change was meant.

Simple lesson: the first run saves a baseline. Later runs compare. -u updates it on purpose.

Making Screenshots Stable

Masking Dynamic Content

  • Clocks, counters, ads, random tips and dates change every run. They make screenshots fail for no reason.
  • mask: [locator, ...] covers those elements with a solid box (pink by default) in both pictures.
  • Without masks, the banner keeps changing, so Playwright cannot even get two equal screenshots. This box shows that error.
  • Add mask: [page.getByTestId('countdown'), page.getByTestId('visitors')] and run again. It passes.

Other Ways to Reduce Noise

  • animations: 'disabled' stops CSS animations and transitions. It is the default for toHaveScreenshot().
  • stylePath: 'tests/screenshot.css' adds CSS only while the screenshot is taken. Use it to hide things with visibility: hidden.
  • maxDiffPixels: 100 allows up to 100 different pixels. maxDiffPixelRatio: 0.01 allows 1 percent.
  • threshold: 0.2 (0 to 1) decides how different one pixel's colour must be to count.
  • Set defaults for every screenshot in the config:
export default defineConfig({
  expect: {
    toHaveScreenshot: { maxDiffPixels: 100, animations: 'disabled' },
  },
});

Same Machine, Same Pictures

  • Fonts and anti-aliasing differ between Windows, macOS and Linux. A baseline from your laptop can fail on CI.
  • Most teams make and check baselines in one place: CI, or the official Playwright Docker image. CI/CD and Scaling shows how.

Simple lesson: mask what changes, allow tiny differences, and make baselines on one kind of machine.

Snapshot Testing Values

toMatchSnapshot()

  • expect(value).toMatchSnapshot('name.txt') works like a screenshot, but for text.
  • Use it for long output that should not change by accident: an exported CSV, a generated email, an API response.
  • The same rules apply: the first run writes the file, -u updates it.

ARIA Snapshots

What the Page Means, Not How It Looks

  • Screen readers do not see colours. They read the accessibility tree: roles, names and states.
  • locator.ariaSnapshot() prints that tree as YAML. Each line is - role "name", with states like [level=1] or [checked].
  • The output is a good start for a test. Copy the lines you care about.

toMatchAriaSnapshot()

  • await expect(locator).toMatchAriaSnapshot(template) checks the tree against a YAML template.
  • Matching is partial. List only what matters. Lines must appear in the same order, but other elements may sit between them.
  • A name can be a regular expression: - heading /Welcome back, \w+/.
  • It survives CSS changes and wrapper divs, but fails when the meaning changes: a missing label, a renamed button, a wrong heading level.
  • Inside a JavaScript template string, a regex backslash is written twice: \\w.

Simple lesson: ARIA snapshots check what the page means. They ignore how it looks.

Accessibility Testing with axe

Installing and Running axe-core

  • axe-core is a popular accessibility rule engine. @axe-core/playwright runs it inside your Playwright tests.
  • Install it once: npm install -D @axe-core/playwright.
  • new AxeBuilder({ page }).analyze() scans the page and returns violations. An empty list means no rule failed.
  • The page has four real problems: an image with no alt text, an email field with no label, a close button with no name, and grey text that is too light to read.
  • /newsletter?fixed=1 is the same page with all four fixed. Change the URL and run again.

Choosing What to Scan

  • .withTags(['wcag2a', 'wcag2aa']) runs only the rules for those WCAG levels. Most teams aim for WCAG 2 AA.
  • .include('#main') scans one part. .exclude('.ads') skips a part you do not own.
  • .disableRules(['color-contrast']) turns one rule off. Use it rarely, with a comment and a ticket to fix it.

What Automated Checks Cannot Find

  • Tools find only part of the problems, often quoted as about a third. They cannot tell if alt text is helpful, or if the page makes sense with a keyboard.
  • Add keyboard tests: press Tab and check where the focus goes with toBeFocused().
  • Nothing replaces trying the page yourself with a keyboard and a screen reader.

Locators like getByRole() and getByLabel() are a small accessibility check on their own. If a test cannot find a button by its role and name, a screen reader user may not find it either.

Simple lesson: axe finds common problems fast. Keyboard tests and real people find the rest.

Try It in Your Project

  • These steps take the lesson from the lab to your own app.
  • Add one toHaveScreenshot() for your most visual page, with masks for dates and ads.
  • Generate baselines in CI or Docker, not on your laptop.
  • Add an axe scan for your key pages and fix the critical violations first.

Simple lesson: use one idea from this lesson on your own app this week.

TL;DR

  • toHaveScreenshot() compares with a baseline. The first run writes it and fails.
  • --update-snapshots (-u) updates baselines. Check the images before you commit.
  • mask, stylePath, maxDiffPixels and threshold remove noise. Make baselines on one kind of machine.
  • toMatchSnapshot() does the same for text.
  • ariaSnapshot() prints the accessibility tree. toMatchAriaSnapshot() checks it, partially.
  • new AxeBuilder({ page }).analyze() finds common accessibility bugs. Keyboard tests cover more.

Common mistakes

MistakeWhat happensFix
Running -u whenever a screenshot failsReal visual bugs become the new baselineLook at the diff first
Screenshots of pages with clocks or countersTests fail on every runMask the changing parts
Baselines made on a laptop, checked on Linux CIFont differences fail every testMake baselines in CI or Docker
Screenshot tests for everythingSlow, noisy suitesA few key screens. Use assertions for the rest
Treating zero axe violations as "accessible"Many problems stay hiddenAdd keyboard tests and manual checks

Check Yourself

Exercises

What to Remember

  • Visual tests: baseline first, compare after, -u to update on purpose.
  • Mask what changes. Make baselines on one kind of machine.
  • toMatchSnapshot() for text, toMatchAriaSnapshot() for structure.
  • axe-core finds common accessibility bugs. Keyboard tests and people find the rest.

Next: Debugging and Tracing