Testing Accessibility
TL;DR
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 ../10_testing/; this is the a11y-specific layer.
Interview Q&A
Q: What can automated accessibility testing actually catch?
A: 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.”
Q: What can’t it catch?
A: 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.
Q: How do you wire axe into tests?
A: 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 ../10_testing/10_a11y_and_visual_regression.md.
Q: How does Testing Library push you toward accessibility?
A: 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 ../10_testing/03_react_testing_library.md.
Q: What’s your manual testing process?
A:
- 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.
Q: What tools help during development?
A: 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.
Q: How do you prevent accessibility regressions?
A: 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 / edge cases
- 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.
What a senior is expected to say
- “axe catches ~a third — real regressions, cheap in CI — but a clean run isn’t ‘accessible.’ I layer lint + axe + role-based tests + manual keyboard/screen-reader passes.”
- “RTL
getByRolefirst mirrors how AT finds elements; if I can’t query by role, it’s probably inaccessible.” - “Manual: keyboard-only pass, NVDA/VoiceOver, 200% zoom + 400% reflow, contrast, reduced motion. And I run axe on dynamic states, not just initial render.”
- “Gates:
eslint-plugin-jsx-a11y→jest-axe→ Playwright axe → PR checklist.”
Cross-references
- General a11y + visual regression testing: ../10_testing/10_a11y_and_visual_regression.md
- RTL role-based queries: ../10_testing/03_react_testing_library.md
- What “correct” looks like (the patterns to verify): 06_accessible_components.md
- WCAG criteria being verified: 01_wcag_and_pour.md
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