5.8 KiB
5.8 KiB
Accessibility Testing Guide
Comprehensive guide for implementing accessibility testing in Next.js applications.
Overview
Accessibility testing ensures applications are usable by people with disabilities and comply with WCAG standards.
Tools
axe-core
Industry-standard accessibility testing engine that detects WCAG violations.
Installation:
npm install -D @axe-core/playwright jest-axe
@axe-core/playwright
Playwright integration for axe-core enabling E2E accessibility testing.
jest-axe
Jest/Vitest matcher for accessibility assertions in component tests.
Component-Level Testing
Setup
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
Basic Usage
it('has no accessibility violations', async () => {
const { container } = render(<MyComponent />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
Testing Specific Elements
it('form has no violations', async () => {
const { container } = render(<SignupForm />)
const form = container.querySelector('form')
const results = await axe(form)
expect(results).toHaveNoViolations()
})
Custom Rules
const results = await axe(container, {
rules: {
'color-contrast': { enabled: true },
'valid-aria-role': { enabled: true }
}
})
E2E Accessibility Testing
Setup
import AxeBuilder from '@axe-core/playwright'
Page-Level Scanning
test('homepage meets a11y standards', async ({ page }) => {
await page.goto('/')
const accessibilityScanResults = await new AxeBuilder({ page }).analyze()
expect(accessibilityScanResults.violations).toEqual([])
})
Scanning Specific Regions
test('navigation is accessible', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.include('#navigation')
.analyze()
expect(results.violations).toEqual([])
})
Excluding Elements
const results = await new AxeBuilder({ page })
.exclude('#third-party-widget')
.analyze()
Custom Tags
Test specific WCAG levels:
// WCAG 2.1 Level AA
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()
Common Violations and Fixes
Missing Alt Text
Violation: Images without alt attributes
Fix:
// Bad
<img src="/avatar.jpg" />
// Good
<img src="/avatar.jpg" alt="User avatar" />
// Decorative images
<img src="/divider.png" alt="" />
Form Labels
Violation: Form inputs without labels
Fix:
// Bad
<input type="text" placeholder="Name" />
// Good
<label htmlFor="name">Name</label>
<input id="name" type="text" />
// Or use aria-label
<input type="text" aria-label="Name" />
Color Contrast
Violation: Insufficient contrast ratio
Fix:
- Use contrast ratio of at least 4.5:1 for normal text
- Use contrast ratio of at least 3:1 for large text
- Test with tools like WebAIM Contrast Checker
Heading Hierarchy
Violation: Skipped heading levels
Fix:
// Bad
<h1>Page Title</h1>
<h3>Section</h3>
// Good
<h1>Page Title</h1>
<h2>Section</h2>
Keyboard Navigation
Violation: Interactive elements not keyboard accessible
Fix:
// Bad
<div onClick={handleClick}>Click me</div>
// Good
<button onClick={handleClick}>Click me</button>
// Or add keyboard handlers
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
Click me
</div>
Focus Indicators
Violation: Invisible focus indicators
Fix:
/* Ensure visible focus */
:focus-visible {
outline: 2px solid blue;
outline-offset: 2px;
}
ARIA Best Practices
Landmarks
<header role="banner">
<nav role="navigation">
<main role="main">
<aside role="complementary">
<footer role="contentinfo">
Live Regions
<div role="status" aria-live="polite">
Form submitted successfully
</div>
<div role="alert" aria-live="assertive">
Error: Please correct the following fields
</div>
Dynamic Content
<button
aria-expanded={isOpen}
aria-controls="dropdown-menu"
>
Menu
</button>
<div id="dropdown-menu" aria-hidden={!isOpen}>
{/* Menu items */}
</div>
Testing Checklist
- All images have alt text
- Form inputs have labels
- Heading hierarchy is logical
- Color contrast meets WCAG AA
- Keyboard navigation works
- Focus indicators are visible
- ARIA attributes are correct
- Dynamic content announces properly
- No violations in axe scans
- Screen reader tested (optional but recommended)
CI Integration
GitHub Actions
- name: Run accessibility tests
run: npm run test:e2e -- --grep @a11y
- name: Upload a11y results
uses: actions/upload-artifact@v3
with:
name: accessibility-results
path: test-results/
Failed Test Reporting
test('check accessibility', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page }).analyze()
if (results.violations.length > 0) {
console.log('Accessibility violations:')
results.violations.forEach(violation => {
console.log(`- ${violation.id}: ${violation.description}`)
console.log(` Impact: ${violation.impact}`)
console.log(` Elements: ${violation.nodes.length}`)
})
}
expect(results.violations).toEqual([])
})