Accessibility
Testing
Automated accessibility testing, manual testing techniques, and screen reader testing
Accessibility Testing
Automated tools catch about 30-50% of accessibility issues. Comprehensive testing requires a combination of automated scans, manual checks, and assistive technology testing.
Testing Strategy
Accessibility Testing Pyramid
├── Automated (CI/CD) — catch regressions fast
│ ├── Linters (eslint-plugin-jsx-a11y)
│ ├── Unit tests (jest-axe, Testing Library)
│ └── Integration tests (axe-core, Lighthouse CI)
├── Manual — catch what automation misses
│ ├── Keyboard navigation walkthrough
│ ├── Zoom and text spacing tests
│ └── Content and reading order review
└── Assistive Technology — validate real experience
├── Screen reader testing (NVDA, VoiceOver, JAWS)
├── Voice control testing (Dragon, Voice Control)
└── Magnification software testingAutomated Testing
ESLint Plugin (Static Analysis)
Catches common mistakes during development.
npm install --save-dev eslint-plugin-jsx-a11y// .eslintrc.js
module.exports = {
extends: ['plugin:jsx-a11y/recommended'],
plugins: ['jsx-a11y'],
};Common issues caught:
- Images without
altattributes - Click handlers without keyboard handlers
- Missing
htmlForon labels - Invalid ARIA attributes
- Autofocus usage
Unit Testing with jest-axe
npm install --save-dev jest-axeimport { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
describe('Button', () => {
it('has no accessibility violations', async () => {
const { container } = render(
<button onClick={() => {}}>Submit</button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
describe('Form', () => {
it('has no accessibility violations', async () => {
const { container } = render(
<form>
<label htmlFor="email">Email</label>
<input id="email" type="email" required />
<button type="submit">Submit</button>
</form>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});Testing Library (Accessibility-First Queries)
Testing Library encourages accessible patterns by prioritizing queries that reflect how users interact with the page.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// Query priority (most to least preferred):
// 1. getByRole — accessible role
// 2. getByLabelText — form fields
// 3. getByPlaceholderText — when no label
// 4. getByText — non-interactive text
// 5. getByDisplayValue — current input value
// 6. getByAltText — images
// 7. getByTitle — title attribute
// 8. getByTestId — last resort
test('form submission', async () => {
const user = userEvent.setup();
render(<LoginForm />);
// Uses accessible queries
const emailInput = screen.getByRole('textbox', { name: /email/i });
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });
await user.type(emailInput, 'user@example.com');
await user.type(passwordInput, 'password123');
await user.click(submitButton);
expect(screen.getByRole('alert')).toHaveTextContent('Welcome!');
});
test('dialog keyboard interaction', async () => {
const user = userEvent.setup();
render(<ConfirmDialog />);
// Open dialog
await user.click(screen.getByRole('button', { name: /delete/i }));
// Verify dialog is open
const dialog = screen.getByRole('dialog');
expect(dialog).toBeInTheDocument();
// Close with Escape
await user.keyboard('{Escape}');
expect(dialog).not.toBeInTheDocument();
});E2E Testing with axe-core
npm install --save-dev @axe-core/playwright// Playwright + axe-core
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});
test('navigation is keyboard accessible', async ({ page }) => {
await page.goto('/');
// Tab through main navigation
await page.keyboard.press('Tab'); // Skip link
await page.keyboard.press('Enter'); // Activate skip link
// Verify focus moved to main content
const focused = await page.evaluate(() => document.activeElement?.id);
expect(focused).toBe('main-content');
});
// Scan specific page sections
test('form section is accessible', async ({ page }) => {
await page.goto('/contact');
const results = await new AxeBuilder({ page })
.include('#contact-form')
.analyze();
expect(results.violations).toEqual([]);
});Lighthouse CI
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000/', 'http://localhost:3000/about'],
numberOfRuns: 3,
},
assert: {
assertions: {
'categories:accessibility': ['error', { minScore: 0.9 }],
// Specific audits
'color-contrast': 'error',
'document-title': 'error',
'html-has-lang': 'error',
'image-alt': 'error',
'label': 'error',
'link-name': 'error',
'list': 'error',
'meta-viewport': 'error',
},
},
upload: {
target: 'temporary-public-storage',
},
},
};# Run Lighthouse CI
npx lhci autorunManual Testing
Keyboard Testing Checklist
| Check | What to Verify |
|---|---|
| Tab through entire page | All interactive elements reachable, logical order |
| Shift+Tab | Reverse navigation works |
| Enter/Space on buttons | All buttons activate |
| Escape on modals/popups | Dialogs close, focus returns |
| Arrow keys in widgets | Tabs, menus, dropdowns navigate correctly |
| Focus visibility | Focus indicator always visible |
| No keyboard traps | Can always Tab away from any element |
| Skip link | Bypasses navigation, moves focus to main content |
Zoom Testing
| Test | Expected Result |
|---|---|
| Browser zoom to 200% | Content reflows, no horizontal scroll |
| Browser zoom to 400% | Content remains readable in single column |
| Text-only zoom (Firefox) | Text enlarges without breaking layout |
| Text spacing override | Content remains visible, no overlap or clipping |
Content Review
- Reading order matches visual order
- Heading hierarchy is logical (no skipped levels)
- Links have descriptive text (not just "click here" or "read more")
- Error messages explain how to fix the problem
- Color is not the sole indicator of information
- Images have appropriate alt text
Screen Reader Testing
Screen Reader + Browser Combinations
| Screen Reader | Platform | Browser | Usage |
|---|---|---|---|
| NVDA | Windows | Firefox, Chrome | Most popular free screen reader |
| JAWS | Windows | Chrome, Edge | Most popular commercial screen reader |
| VoiceOver | macOS | Safari | Built into macOS |
| VoiceOver | iOS | Safari | Built into iOS |
| TalkBack | Android | Chrome | Built into Android |
| Narrator | Windows | Edge | Built into Windows |
VoiceOver Quick Reference (macOS)
| Action | Keys |
|---|---|
| Turn on/off | Cmd + F5 |
| Navigate next | VO + Right Arrow (Ctrl + Option + Right) |
| Navigate previous | VO + Left Arrow |
| Activate element | VO + Space |
| Read all | VO + A |
| Open rotor (landmarks, headings) | VO + U |
| Navigate by heading | VO + Cmd + H |
NVDA Quick Reference (Windows)
| Action | Keys |
|---|---|
| Turn on | Ctrl + Alt + N |
| Stop speaking | Ctrl |
| Navigate next | Tab or Down Arrow |
| Navigate headings | H |
| Navigate landmarks | D |
| List elements | NVDA + F7 |
| Read current line | NVDA + Up Arrow |
What to Test with Screen Readers
Screen Reader Testing Checklist
├── Page Structure
│ ├── Page title is announced
│ ├── Headings create logical outline
│ ├── Landmarks are present and labeled
│ └── Language is correctly identified
├── Navigation
│ ├── Skip link works
│ ├── Navigation items are announced with count
│ ├── Current page is indicated (aria-current)
│ └── Focus management on route changes
├── Forms
│ ├── Labels announced with each input
│ ├── Required fields indicated
│ ├── Error messages announced
│ └── Groups (fieldset/legend) announced
├── Dynamic Content
│ ├── Live regions announce updates
│ ├── Modals trap focus and announce
│ ├── Loading states communicated
│ └── Status messages announced
└── Interactive Widgets
├── Custom components announce role, name, state
├── State changes announced (expanded, checked, selected)
├── Keyboard patterns work as expected
└── Disabled state communicatedAccessibility Audit Workflow
Recommended Process
Audit Workflow
├── 1. Automated Scan
│ ├── Run axe-core or Lighthouse on all pages
│ ├── Fix all automatically detected issues
│ └── Re-scan to confirm fixes
├── 2. Keyboard Audit
│ ├── Tab through every page
│ ├── Test all interactive components
│ └── Verify focus management
├── 3. Visual Review
│ ├── Check contrast with browser DevTools
│ ├── Test at 200% zoom
│ ├── Test with text spacing overrides
│ └── Test in forced-colors mode
├── 4. Screen Reader Audit
│ ├── Test with VoiceOver (Mac) or NVDA (Windows)
│ ├── Navigate by headings, landmarks, forms
│ └── Test dynamic content and notifications
└── 5. Report & Prioritize
├── Document issues by WCAG criterion
├── Rate severity (critical, serious, moderate, minor)
└── Create remediation plan with prioritiesIssue Severity Levels
| Severity | Description | Example |
|---|---|---|
| Critical | Blocks access for a user group | No keyboard access, missing form labels |
| Serious | Major barrier, workaround exists | Poor contrast, missing skip link |
| Moderate | Some difficulty, content still usable | Missing landmark labels, ambiguous link text |
| Minor | Annoyance, doesn't block access | Inconsistent focus styles, missing lang on inline text |
Tool Reference
| Tool | Type | Use Case |
|---|---|---|
| eslint-plugin-jsx-a11y | Linter | Static analysis during development |
| jest-axe | Unit test | Component-level accessibility testing |
| @axe-core/playwright | E2E test | Page-level accessibility scanning |
| Lighthouse | Audit | Performance and accessibility scoring |
| axe DevTools (browser extension) | Manual | Interactive accessibility inspection |
| WAVE (browser extension) | Manual | Visual accessibility evaluation |
| Colour Contrast Analyser | Manual | Color contrast checking (desktop app) |
| Accessibility Insights | Manual | Guided manual assessment (Microsoft) |
| pa11y | CLI / CI | Automated accessibility testing |
| Storybook a11y addon | Development | Component accessibility testing in Storybook |
Best Practices
Accessibility Testing Guidelines
- Integrate automated testing (axe, Lighthouse) into CI/CD
- Use Testing Library queries that enforce accessible patterns
- Test keyboard navigation manually on every feature
- Test with at least one screen reader before release
- Audit contrast, zoom, and text spacing during design review
- Prioritize fixes by severity — critical blockers first
- Automate what you can, but never skip manual testing
- Include accessibility in your Definition of Done