frontend / accessibility / 07_testing_accessibility.md

Testing Accessibility

4 min read source

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:

  1. 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?
  2. 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.
  3. Zoom / reflow — 200% zoom (WCAG 1.4.4) and 400% reflow (1.4.10): no loss of content or horizontal scroll.
  4. Contrast — DevTools or a contrast checker for text and UI components.
  5. Reduced motionprefers-reduced-motion respected.

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.
  • getByTestId everywhere 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 getByRole first 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-a11yjest-axe → Playwright axe → PR checklist.”

Cross-references

Further reading