Testing Accessibility
Automated tools (axe-core via jest-axe, @axe-core/playwright, Lighthouse) catch ~30–40% of WCAG issues — and they’re worth running in CI because they catch real regressions cheaply. But they can’t judge meaningful alt text, sensible focus order, or whether a widget is actually usable. So the senior answer is layered: lint + axe in CI, query by role in component tests (which enforces accessibility), then manual keyboard and screen-reader passes. General testing mechanics live in Frontend Testing — Senior Interview Prep; this is the a11y-specific layer.
What can automated accessibility testing actually catch?
Deterministic, machine-checkable rules: missing/empty alt, form fields with no label, insufficient color contrast, invalid ARIA (bad role, required attrs missing), duplicate ids, missing document language, some focus-order issues. axe-core is the engine behind most tools (Lighthouse, browser extensions, CI integrations). It explicitly avoids false positives, so a clean axe run is meaningful — but it is not “the page is accessible.”
What can’t it catch?
Anything requiring judgment: is the alt text useful or just present? Is the focus order logical? Does the custom combobox work with a keyboard and screen reader? Is the content understandable? Are error messages helpful? These need manual testing. Roughly 60–70% of WCAG criteria are not automatable.
How do you wire axe into tests?
Component/integration tests with jest-axe:
import { render } from "@testing-library/react";
import { axe } from "jest-axe";
test("form has no a11y violations", async () => {
const { container } = render(<SignupForm />);
expect(await axe(container)).toHaveNoViolations();
});End-to-end with Playwright (@axe-core/playwright) catches issues only visible in the real, fully-rendered page:
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);See A11y Testing + Visual Regression in CI.
How does Testing Library push you toward accessibility?
RTL’s recommended query priority is getByRole first, then label/text — the same way assistive tech finds elements. If you can query getByRole("button", { name: /submit/i }), the element has the right role and an accessible name; if you can’t, it’s probably inaccessible. Writing role-based tests surfaces missing labels/roles as test failures. Reaching for getByTestId is the escape hatch that bypasses this signal. See React Testing Library — Query Priority, Role-Based Queries.
What’s your manual testing process?
- Keyboard only — unplug the mouse. Tab through everything: can you reach and operate every control? Is focus visible? Any traps? Does Escape close overlays? Does focus return sensibly?
- Screen reader — test the real pairings: NVDA + Firefox/Chrome (Windows), VoiceOver + Safari (macOS/iOS), TalkBack (Android). Listen for correct name/role/state and announced changes.
- Zoom / reflow — 200% zoom (WCAG 1.4.4) and 400% reflow (1.4.10): no loss of content or horizontal scroll.
- Contrast — DevTools or a contrast checker for text and UI components.
- Reduced motion —
prefers-reduced-motionrespected.
What tools help during development?
Browser DevTools Accessibility panel (inspect the accessibility tree, computed name/role, contrast), the axe DevTools / WAVE extensions, Lighthouse’s accessibility audit, and ESLint’s eslint-plugin-jsx-a11y to catch issues at author time (missing alt, invalid ARIA, click-without-keyboard). Lint is the cheapest gate — it fails the build before review.
How do you prevent accessibility regressions?
Layer the gates: eslint-plugin-jsx-a11y in lint, jest-axe in component tests, @axe-core/playwright in E2E, and an a11y item on the PR checklist (keyboard pass for new interactive UI). Automated gates catch the mechanical regressions; the checklist enforces the human judgment automation can’t.
Gotchas
- A clean axe run ≠ accessible — the most dangerous misconception; it’s a floor, not a ceiling.
- Testing only the initial render misses dynamic states — run axe after opening the modal, expanding the menu, showing the error.
jsdom(Jest/Vitest) doesn’t compute layout or real contrast — color-contrast and some visibility rules need a real browser (Playwright); axe in jsdom skips them.- Overlay/“accessibility widget” vendors don’t make a site compliant and have been litigated against — fix the source.
- Screen-reader behavior varies by SR + browser pairing — test the common combinations, don’t assume one represents all.
getByTestIdeverywhere hides accessibility gaps that role-based queries would expose.
Related
Further reading
- Deque axe-core: https://github.com/dequelabs/axe-core
jest-axe: https://github.com/nickcolley/jest-axe- WAI — Evaluation tools & involving users: https://www.w3.org/WAI/test-evaluate/
eslint-plugin-jsx-a11y: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y
Interview angle 5
- “What does an automated accessibility check actually cover?” - about a third of the issues: contrast, missing labels, invalid ARIA, structural problems. They are real regressions and cheap to catch in CI, and a clean axe run does not mean the page is accessible - it means nothing statically detectable is wrong.
- “How do component tests help?” - querying by role in Testing Library mirrors how assistive technology finds elements, so the test fails for the same reason a screen reader user would be stuck. If you cannot query it by role, that is the finding.
- “What has to be manual?” - a keyboard-only pass through the real flows, a screen reader pass with NVDA or VoiceOver, 200% zoom and 400% reflow, and checking reduced-motion is honoured. None of those are inferable from the DOM.
- “What do people forget to test?” - dynamic states. Most violations live in an open menu, a validation error or a loading state, and a check that only runs against the initial render never sees any of them.
- “How would you layer the gates?” -
eslint-plugin-jsx-a11ywhile typing,jest-axein component tests, axe in Playwright for whole pages, and a review checklist for the judgement calls. Each catches a different class, and the cheap ones catch the most.