Testing

Accessibility Testing with Playwright (and Cypress)

May 19, 20267 min read

Playwright (and Cypress, and similar e2e tools) can run axe-core against your live app on every commit. The integration is straightforward; the value is catching accessibility regressions before they ship.

Why automated e2e accessibility testing matters

Unit tests check individual components; e2e tests check the whole rendered page in a real browser. Accessibility issues often emerge from the composition (header + main + modal all interacting) — exactly what e2e catches and unit tests miss.

Setting up axe-core in Playwright

Install @axe-core/playwright:

npm install --save-dev @axe-core/playwright

In your test:

import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

test('homepage has no critical accessibility violations', async ({ page }) => {
  await page.goto('/')
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag22a', 'wcag22aa'])
    .analyze()

  const critical = results.violations.filter((v) => v.impact === 'critical' || v.impact === 'serious')
  expect(critical).toEqual([])
})

Strategies that work

Scan critical pages, not every page

Pick 5-10 representative pages: homepage, signup, dashboard, settings, checkout. Scanning every page on every commit slows CI without finding much extra.

Block on critical and serious only

Default axe categorizes by severity: critical, serious, moderate, minor. Block deploys on critical and serious. Log moderate and minor for backlog grooming.

Scan key user flows mid-interaction

The most interesting accessibility bugs appear after interaction: modal open, form errors visible, dropdown expanded. Tell Playwright to perform the action then scan.

await page.click('button:has-text("Open dialog")')
await page.waitForSelector('[role="dialog"]')
const results = await new AxeBuilder({ page }).include('[role="dialog"]').analyze()
expect(results.violations).toEqual([])

Use disableRules sparingly

Axe has rules you may legitimately disable (e.g. color-contrast on a page intentionally low-contrast for a design demo). Document each disabled rule with a TODO.

What axe-in-Playwright catches

  • Missing alt text, missing labels, missing accessible names
  • ARIA validity (invalid roles, required attributes missing)
  • Color contrast (computed in headless Chromium)
  • Skipped headings, missing landmarks
  • Empty buttons / links

What it does NOT catch (use AccessProof for these)

  • Behavior over time. A scheduled scan that runs daily catches the regression introduced after the e2e suite passes.
  • Multi-page consistency. CSS or JS changes that affect every page silently.
  • Real network conditions. Tests run in fixed environments; AccessProof scans production exactly as users see it.
  • Court-ready evidence. CI logs are not great evidence. A timestamped PDF report is.

Pair Playwright + axe (catch on every commit) with AccessProof (scheduled scans of production + PDF reports). Different layers of the same coverage strategy.

Cypress is the same idea

Use cypress-axe with the same patterns. The API differs slightly but the strategy is identical.

Want this checked on your site automatically?

AccessProof scans your site against WCAG 2.2 every day, scores it, and tells you exactly what to fix. Free plan available.