DEV Community

Romain
Romain

Posted on • Originally published at access-proof.com

Accessibility Testing with Playwright (and Cypress)

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
Enter fullscreen mode Exit fullscreen mode

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([])
})
Enter fullscreen mode Exit fullscreen mode

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([])
Enter fullscreen mode Exit fullscreen mode

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.


Originally published on access-proof.com.

Top comments (0)