Testing and Validation Tools
1. Automated Testing Integration
| Tool | Type | Best Use Case | Coverage |
|---|---|---|---|
| axe-core | JavaScript library | Comprehensive automated testing (unit, integration, E2E) | ~57% WCAG issues detectable |
| @axe-core/react | React DevTools | Development-time warnings in console | Real-time feedback during development |
| jest-axe | Jest matcher | Unit tests for React/Vue components | Automated a11y assertions in test suite |
| @axe-core/playwright | E2E testing | Full page accessibility scans in Playwright | Integration with Playwright test runner |
| cypress-axe | E2E testing | Accessibility checks in Cypress tests | Per-page and per-component scanning |
| pa11y | CLI/Node.js | Command-line testing, CI/CD integration | HTML CodeSniffer + custom rules |
| Lighthouse CI | CI/CD | Performance + accessibility scoring | Google Lighthouse accessibility audit |
| eslint-plugin-jsx-a11y | Linter | Static analysis of JSX code | Catches common React a11y mistakes |
Example: Automated testing setup
// Install dependencies
// npm install --save-dev jest-axe @testing-library/react @testing-library/jest-dom
// Jest setup (setupTests.js)
import { toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
// Component test with jest-axe
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import Button from './Button';
describe('Button accessibility', () => {
it('should not have accessibility violations', async () => {
const { container } = render(
<Button onClick={() => {}}>Click me</Button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should have accessible name', () => {
const { getByRole } = render(<Button>Submit</Button>);
expect(getByRole('button', { name: 'Submit' })).toBeInTheDocument();
});
});
// Playwright with axe-core
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage should not have accessibility violations', async ({ page }) => {
await page.goto('http://localhost:3000');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
// Cypress with cypress-axe
import 'cypress-axe';
describe('Accessibility tests', () => {
beforeEach(() => {
cy.visit('/');
cy.injectAxe();
});
it('should have no accessibility violations', () => {
cy.checkA11y();
});
it('should check specific component', () => {
cy.checkA11y('.modal-dialog');
});
it('should exclude specific elements', () => {
cy.checkA11y(null, {
exclude: ['.third-party-widget']
});
});
});
// ESLint configuration (.eslintrc.js)
module.exports = {
extends: [
'plugin:jsx-a11y/recommended'
],
plugins: ['jsx-a11y'],
rules: {
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/no-autofocus': 'warn',
'jsx-a11y/label-has-associated-control': 'error'
}
};
Automated Testing Limitations: Automated tools catch ~30-50% of accessibility issues. They
can't detect: poor visual design, logical reading order, meaningful alt text quality, keyboard usability, screen
reader UX. Always combine with manual testing.
2. Screen Reader Testing Procedures
| Platform | Screen Reader | Key Commands | Testing Focus |
|---|---|---|---|
| Windows | NVDA (free) |
Ctrl: Stop speechInsert+Down: Read allH/Shift+H: HeadingsK/Shift+K: LinksD/Shift+D: LandmarksB/Shift+B: ButtonsF/Shift+F: Form fields
|
Heading structure, landmarks, form labels, ARIA announcements |
| Windows | JAWS (commercial) |
Insert+F5: Form fields listInsert+F6: Headings listInsert+F7: Links listR: Regions/landmarksInsert+F3: Elements list
|
Complex ARIA widgets, forms, tables, dynamic content |
| macOS | VoiceOver |
Cmd+F5: Toggle VOVO+A: Read allVO+Right/Left: NavigateVO+U: Rotor menuVO+Space: ActivateVO+Shift+Down: Into group
|
Safari/Chrome compatibility, mobile web, native app integration |
| iOS | VoiceOver |
Swipe right/left: Navigate Double-tap: Activate Two-finger Z: Back Rotor: Quick nav |
Touch gestures, mobile-specific patterns, responsive design |
| Android | TalkBack |
Swipe right/left: Navigate Double-tap: Activate Swipe down-then-up: Reading controls Local context menu: Actions |
Android web, PWA, custom actions, material design |
Example: Screen reader testing checklist
// Screen Reader Test Script
1. Page Structure Test
□ Navigate by headings (H key) - logical hierarchy?
□ Navigate by landmarks (D/R key) - proper regions?
□ Page title announced correctly?
□ Main content easily accessible?
2. Interactive Elements Test
□ Navigate by buttons (B key) - all announced?
□ Button states announced (pressed, expanded)?
□ Links (K key) - descriptive text, not "click here"?
□ Form fields (F key) - labels associated?
3. Dynamic Content Test
□ Form validation errors announced?
□ Live region updates announced?
□ Loading states announced?
□ Route changes announced (SPA)?
4. Complex Widgets Test
□ Modal dialog focus trapped?
□ Accordion expand/collapse announced?
□ Tab panels keyboard navigable?
□ Combobox autocomplete working?
5. Table Test
□ Table headers announced with cells?
□ Navigate by cell (Ctrl+Alt+Arrow)?
□ Row/column headers associated?
// Screen Reader Testing Script Example
describe('Screen reader announcements', () => {
it('should announce form errors', () => {
// Simulate screen reader
const form = document.querySelector('form');
const errorMessage = document.querySelector('[role="alert"]');
// Verify error in accessibility tree
expect(errorMessage).toHaveAttribute('role', 'alert');
expect(errorMessage).toHaveTextContent('Email is required');
});
it('should announce dynamic updates', () => {
const liveRegion = document.querySelector('[aria-live="polite"]');
expect(liveRegion).toBeInTheDocument();
// Update content
liveRegion.textContent = '3 items added to cart';
// Verify content updated
expect(liveRegion).toHaveTextContent('3 items added to cart');
});
});
Screen Reader Testing Gotchas: Different screen readers behave differently (test with at least
2). Browser choice matters (NVDA+Firefox vs Chrome). Mobile screen readers have different navigation. Virtual
cursor vs focus mode impacts testing. Always test in actual screen readers, not simulators.
3. Keyboard Navigation Testing
| Key/Combination | Expected Behavior | Test Scenario | Common Issues |
|---|---|---|---|
| Tab | Move focus forward through interactive elements | All buttons, links, inputs reachable in logical order | Skip important elements, illogical order, focus traps |
| Shift+Tab | Move focus backward | Reverse navigation works correctly | Order different from forward tab |
| Enter | Activate buttons, links, submit forms | All interactive elements respond to Enter | Custom buttons missing Enter handler |
| Space | Activate buttons, checkboxes, toggle switches | Buttons work with Space key | Custom buttons missing Space handler |
| Escape | Close modals, cancel actions, clear autocomplete | Modals/dialogs close, focus restored | No Escape handler, focus not restored |
| Arrow keys | Navigate within composite widgets (tabs, menus, lists) | Tab panels, radio groups, dropdown menus | Arrows move page instead of focus |
| Home/End | Jump to first/last item in lists, first/last character in inputs | Long lists, combobox options | Missing Home/End support in custom widgets |
| Page Up/Down | Scroll content or navigate by page in lists | Scroll containers, data grids | Focus lost when scrolling |
Example: Keyboard testing automation
// Keyboard navigation test utilities
class KeyboardTester {
// Simulate Tab key
static tab(element, shift = false) {
const event = new KeyboardEvent('keydown', {
key: 'Tab',
code: 'Tab',
keyCode: 9,
shiftKey: shift,
bubbles: true
});
element.dispatchEvent(event);
}
// Get all focusable elements
static getFocusableElements(container = document) {
const selector = [
'a[href]',
'button:not([disabled])',
'textarea:not([disabled])',
'input:not([disabled]):not([type="hidden"])',
'select:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
return Array.from(container.querySelectorAll(selector));
}
// Test tab order
static testTabOrder(expectedOrder) {
const focusable = this.getFocusableElements();
const actualOrder = focusable.map(el =>
el.getAttribute('data-testid') || el.textContent.trim()
);
console.log('Expected:', expectedOrder);
console.log('Actual:', actualOrder);
return JSON.stringify(expectedOrder) === JSON.stringify(actualOrder);
}
}
// Playwright keyboard testing
test('keyboard navigation works correctly', async ({ page }) => {
await page.goto('/');
// Tab through all interactive elements
await page.keyboard.press('Tab');
await expect(page.locator('button').first()).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.locator('a').first()).toBeFocused();
// Test Shift+Tab (reverse)
await page.keyboard.press('Shift+Tab');
await expect(page.locator('button').first()).toBeFocused();
// Test Enter activation
await page.keyboard.press('Enter');
// Verify button action occurred
});
// Testing-Library keyboard test
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('modal closes with Escape key', async () => {
const user = userEvent.setup();
const onClose = jest.fn();
const { getByRole } = render(
<Modal isOpen onClose={onClose}>
<h2>Modal Title</h2>
</Modal>
);
// Modal is open
expect(getByRole('dialog')).toBeInTheDocument();
// Press Escape
await user.keyboard('{Escape}');
// Modal should close
expect(onClose).toHaveBeenCalled();
});
// Manual test checklist
/*
Keyboard Navigation Test Checklist:
□ Tab reaches all interactive elements
□ Tab order follows visual/logical order
□ Shift+Tab works in reverse
□ No keyboard traps (can always Tab out)
□ Enter activates buttons and links
□ Space activates buttons and checkboxes
□ Escape closes modals and cancels actions
□ Focus indicators clearly visible (3:1 contrast)
□ Arrow keys work in composite widgets
□ Home/End jump to first/last items
□ No auto-focus unless necessary
□ Focus restored after modal/dropdown closes
*/
Keyboard Testing Tips: Unplug your mouse and navigate entire site with keyboard only. Test
focus visibility (3:1 contrast minimum). Verify no focus traps (can always Tab out). Check custom widgets follow
ARIA keyboard patterns. Test with browser zoom at 200%.
4. Color Contrast Validation
| Tool | Type | Features | Best For |
|---|---|---|---|
| WebAIM Contrast Checker | Web tool | Foreground/background ratio, WCAG level compliance | Quick manual checks, design validation |
| Chrome DevTools | Browser tool | Color picker with contrast ratio, suggestions | Real-time adjustments during development |
| Colour Contrast Analyser (CCA) | Desktop app | Eyedropper, pass/fail indicators, simulations | Pixel-level accuracy, design review |
| axe DevTools | Browser extension | Automated contrast checking, element highlighting | Full page audits, QA testing |
| Stark (Figma plugin) | Design tool | Contrast check in Figma, suggestions, color blindness sim | Design phase validation |
| APCA Calculator | Web tool | Advanced Perceptual Contrast Algorithm (future WCAG 3) | Forward-looking contrast validation |
| Polypane | Browser | Built-in contrast checking, visual impairment simulators | Professional accessibility testing |
Example: Contrast ratio requirements and calculations
// WCAG Contrast Requirements
/*
Text Size | WCAG AA | WCAG AAA
--------------------- | -------- | --------
Normal text (<18px) | 4.5:1 | 7:1
Large text (≥18px) | 3:1 | 4.5:1
Large bold (≥14px) | 3:1 | 4.5:1
UI components | 3:1 | N/A
Graphics/icons | 3:1 | N/A
Focus indicators | 3:1 | N/A (WCAG 2.4.13)
*/
// Contrast ratio calculation (relative luminance)
function getLuminance(r, g, b) {
const [R, G, B] = [r, g, b].map(val => {
val = val / 255;
return val <= 0.03928
? val / 12.92
: Math.pow((val + 0.055) / 1.055, 2.4);
});
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
function getContrastRatio(rgb1, rgb2) {
const lum1 = getLuminance(...rgb1);
const lum2 = getLuminance(...rgb2);
const lighter = Math.max(lum1, lum2);
const darker = Math.min(lum1, lum2);
return (lighter + 0.05) / (darker + 0.05);
}
// Usage
const textColor = [0, 0, 0]; // Black
const bgColor = [255, 255, 255]; // White
const ratio = getContrastRatio(textColor, bgColor);
console.log(`Contrast ratio: ${ratio.toFixed(2)}:1`); // 21:1
// Validation function
function meetsWCAG(ratio, level = 'AA', isLargeText = false) {
if (level === 'AAA') {
return isLargeText ? ratio >= 4.5 : ratio >= 7;
}
// AA
return isLargeText ? ratio >= 3 : ratio >= 4.5;
}
// Automated contrast checking in tests
import { toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
test('button has sufficient contrast', async () => {
const { container } = render(
<button style={{ background: '#0066cc', color: '#ffffff' }}>
Click me
</button>
);
const results = await axe(container, {
rules: {
'color-contrast': { enabled: true }
}
});
expect(results).toHaveNoViolations();
});
// CSS color contrast validation
/*
Good contrast examples:
- #000000 on #FFFFFF = 21:1 (AAA)
- #0066CC on #FFFFFF = 8.59:1 (AAA)
- #666666 on #FFFFFF = 5.74:1 (AA)
- #FFFFFF on #0066CC = 8.59:1 (AAA)
Poor contrast examples (avoid):
- #777777 on #FFFFFF = 4.47:1 (fails AA for normal text)
- #0066CC on #000000 = 2.44:1 (fails AA)
- #CCCCCC on #FFFFFF = 1.61:1 (fails all)
*/
Contrast Testing Gotchas: Test with actual rendered colors, not design specs (gradients,
opacity, overlays affect contrast). Images with text need manual checking. Focus indicators need 3:1 against
both adjacent colors. Disabled elements don't need to meet contrast (but consider UX).
5. Manual Testing Checklists
| Test Category | Key Checkpoints | Tools Needed | WCAG Coverage |
|---|---|---|---|
| Keyboard Navigation |
✓ All interactive elements reachable ✓ Logical tab order ✓ Visible focus indicators (3:1) ✓ No keyboard traps ✓ Escape closes modals |
Keyboard only, DevTools | 2.1.1, 2.1.2, 2.4.3, 2.4.7, 2.4.13 |
| Screen Reader |
✓ Headings in logical order ✓ Landmarks identify regions ✓ Images have alt text ✓ Form labels associated ✓ Errors announced |
NVDA/JAWS/VoiceOver | 1.1.1, 1.3.1, 2.4.6, 3.3.1, 4.1.2 |
| Zoom & Reflow |
✓ Text to 200% without horizontal scroll ✓ Content reflows at 320px ✓ No loss of content/functionality ✓ Touch targets at least 24×24px |
Browser zoom, mobile view | 1.4.4, 1.4.10, 2.5.8 |
| Color & Contrast |
✓ Text contrast 4.5:1 (AA) ✓ UI components 3:1 ✓ Info not conveyed by color alone ✓ Color blindness friendly |
Contrast checker, color filters | 1.4.1, 1.4.3, 1.4.11 |
| Forms |
✓ All inputs have labels ✓ Required fields indicated ✓ Error messages clear & helpful ✓ Success confirmation ✓ Keyboard accessible |
Screen reader, keyboard | 1.3.1, 3.3.1, 3.3.2, 3.3.3, 4.1.2 |
| Dynamic Content |
✓ Updates announced (live regions) ✓ Focus managed on changes ✓ Loading states announced ✓ Time limits adjustable |
Screen reader, DevTools | 2.2.1, 4.1.2, 4.1.3 |
| Mobile/Touch |
✓ Touch targets 44×44px minimum ✓ Works in portrait & landscape ✓ Zoom not disabled ✓ Gestures have alternatives |
Real device, mobile SR | 1.3.4, 1.4.4, 2.5.1, 2.5.8 |
Example: Comprehensive testing checklist
// Accessibility Testing Checklist Template
=== KEYBOARD NAVIGATION ===
□ Tab through entire page (top to bottom)
□ Verify all interactive elements reachable
□ Check tab order matches visual order
□ Shift+Tab works in reverse
□ Focus visible on all elements (3:1 contrast)
□ No keyboard traps detected
□ Enter activates buttons/links
□ Space activates buttons/checkboxes
□ Escape closes modals/dropdowns
□ Arrow keys work in custom widgets
=== SCREEN READER ===
□ Use heading navigation (H key) - logical structure
□ Navigate by landmarks (D/R key) - proper regions
□ Check all images have alt text
□ Verify form labels read correctly
□ Test form validation announcements
□ Check ARIA states announced (expanded, pressed, etc.)
□ Verify live region updates announced
□ Test table navigation (headers announced)
□ Check custom widget announcements
=== VISUAL & CONTRAST ===
□ Text contrast meets 4.5:1 (normal) or 3:1 (large)
□ UI component contrast meets 3:1
□ Focus indicators visible (3:1 against adjacent)
□ Information not conveyed by color alone
□ Test with color blindness simulator
□ Check in high contrast mode (Windows)
=== ZOOM & REFLOW ===
□ Zoom to 200% - no horizontal scroll
□ Zoom to 400% for text - content visible
□ Resize to 320px width - content reflows
□ No loss of content at any zoom level
□ Touch targets remain 24×24px minimum
□ Test responsive design breakpoints
=== FORMS ===
□ All inputs have visible labels
□ Labels programmatically associated
□ Required fields clearly indicated
□ Error messages clear and specific
□ Errors announced by screen reader
□ Success confirmation provided
□ Can submit with keyboard
=== MEDIA ===
□ Videos have captions
□ Audio has transcripts
□ Auto-play videos can be paused
□ Media controls keyboard accessible
□ Transcripts provided for audio-only
=== MOBILE ===
□ Test with VoiceOver (iOS)
□ Test with TalkBack (Android)
□ Touch targets 44×44px minimum
□ Works in both orientations
□ Zoom not disabled (no user-scalable=no)
□ Gestures have button alternatives
=== DYNAMICS ===
□ Route changes announced (SPA)
□ Loading states announced
□ Form submission feedback
□ Modal opens - focus trapped
□ Modal closes - focus restored
□ Live regions working (cart updates, etc.)
=== DOCUMENTATION ===
□ Document any known issues
□ Note browser/AT combinations tested
□ Include WCAG conformance level
□ List exemptions (if any)
□ Provide remediation timeline
Manual Testing Best Practices: Test with real users with disabilities when possible. Use
multiple screen readers (NVDA, JAWS, VoiceOver). Test on actual mobile devices, not just emulators. Document
test results with screenshots/videos. Retest after fixes to verify remediation.
6. Accessibility Linting and CI/CD
| Tool/Approach | Integration Point | Configuration | Benefits |
|---|---|---|---|
| ESLint jsx-a11y | Pre-commit, IDE, CI | Rules for React/JSX accessibility | Catches issues during coding; instant feedback |
| Prettier (formatting) | Pre-commit, CI | Consistent ARIA attribute formatting | Code consistency, readability |
| Stylelint a11y | Pre-commit, CI | CSS accessibility rules (contrast, outline, etc.) | Prevents CSS-based a11y issues |
| Pa11y CI | GitHub Actions, GitLab CI | Automated page scans on deployment | Catches regressions before production |
| Lighthouse CI | GitHub Actions, GitLab CI | Performance + accessibility scoring | Metrics tracking, budget enforcement |
| axe-core in tests | Unit tests, E2E tests | Jest/Playwright/Cypress integration | Automated testing in CI pipeline |
| Git pre-commit hooks | Local git hooks (Husky) | Run linters before commit | Prevents committing a11y violations |
| Storybook a11y addon | Component library | axe checks in Storybook | Component-level validation during development |
Example: CI/CD accessibility pipeline
// package.json scripts
{
"scripts": {
"lint:a11y": "eslint --ext .js,.jsx,.ts,.tsx src/",
"test:a11y": "jest --testMatch '**/*.a11y.test.js'",
"audit:a11y": "pa11y-ci --config .pa11yci.json",
"lighthouse": "lighthouse-ci --config=.lighthouserc.json"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"jest --findRelatedTests --passWithNoTests"
]
}
}
// .eslintrc.js
module.exports = {
extends: [
'plugin:jsx-a11y/recommended'
],
plugins: ['jsx-a11y'],
rules: {
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/aria-props': 'error',
'jsx-a11y/aria-role': 'error',
'jsx-a11y/aria-unsupported-elements': 'error',
'jsx-a11y/label-has-associated-control': 'error',
'jsx-a11y/no-autofocus': 'warn'
}
};
// .pa11yci.json (Pa11y CI configuration)
{
"defaults": {
"standard": "WCAG2AA",
"runners": ["axe", "htmlcs"],
"timeout": 10000,
"wait": 1000,
"chromeLaunchConfig": {
"args": ["--no-sandbox"]
}
},
"urls": [
"http://localhost:3000",
"http://localhost:3000/about",
"http://localhost:3000/products"
]
}
// .lighthouserc.json (Lighthouse CI)
{
"ci": {
"collect": {
"url": ["http://localhost:3000"],
"numberOfRuns": 3
},
"assert": {
"assertions": {
"categories:accessibility": ["error", {"minScore": 0.9}],
"aria-allowed-attr": "error",
"aria-required-attr": "error",
"button-name": "error",
"color-contrast": "error",
"duplicate-id-aria": "error",
"html-has-lang": "error",
"image-alt": "error",
"label": "error",
"link-name": "error",
"list": "error"
}
}
}
}
// GitHub Actions workflow (.github/workflows/a11y.yml)
name: Accessibility Tests
on: [push, pull_request]
jobs:
accessibility:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Lint accessibility
run: npm run lint:a11y
- name: Build app
run: npm run build
- name: Start server
run: npm start &
- name: Wait for server
run: npx wait-on http://localhost:3000
- name: Run Pa11y CI
run: npm run audit:a11y
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
- name: Upload results
uses: actions/upload-artifact@v3
if: always()
with:
name: accessibility-reports
path: |
pa11y-results/
.lighthouseci/
// Storybook a11y addon (.storybook/preview.js)
import { withA11y } from '@storybook/addon-a11y';
export const decorators = [withA11y];
export const parameters = {
a11y: {
config: {
rules: [
{
id: 'color-contrast',
enabled: true
}
]
}
}
};
CI/CD Gotchas: Automated tools catch only 30-50% of issues - still need manual testing. Don't
block deployments for minor warnings. Set appropriate thresholds (e.g., 90% Lighthouse score). Test with
production-like data. Run tests on every PR, not just main branch.
Testing and Validation Quick Reference
- Automated Tools: Use axe-core (jest-axe, @axe-core/playwright, cypress-axe) for ~30-50% coverage; combine with manual testing
- Screen Readers: Test with NVDA (Windows free), JAWS (commercial), VoiceOver (macOS/iOS), TalkBack (Android)
- Keyboard: Tab through entire page, verify focus visibility (3:1 contrast), test Enter/Space/Escape, check no keyboard traps
- Contrast: Use WebAIM checker, Chrome DevTools, or CCA; verify 4.5:1 for text, 3:1 for UI components
- Manual Checklist: Keyboard navigation, screen reader, zoom/reflow, color/contrast, forms, media, mobile, dynamics
- Linting: eslint-plugin-jsx-a11y for React, stylelint-a11y for CSS, run in pre-commit hooks and CI
- CI/CD: Pa11y CI for automated scans, Lighthouse CI for metrics, axe in test suites, Storybook addon for components
- Testing Strategy: Shift-left (test early), automate what you can, manual test critical flows, retest after fixes
- Browser DevTools: Chrome Accessibility tree, Firefox Accessibility Inspector, Edge Accessibility Insights
- Best Practice: Test with real users with disabilities when possible; document all test results and remediation plans