Introduction to Accessibility Testing with Playwright
Accessibility testing is a fundamental aspect of modern web development. It's not just a legal or moral requirement — it's a technical imperative that concerns the inclusivity and reachability of your digital products. In this guide from the Playwright Component Testing Avanzato series, you'll discover how to automate accessibility testing using Playwright integrated with @axe-core, the most widespread open-source library for detecting accessibility vulnerabilities in your code.
We'll cover the integration step by step, learn to write meaningful tests, and identify best practices to ensure your components are accessible to all users — regardless of their physical or cognitive abilities.
Why Accessibility Testing is Critical
According to global statistics, approximately 16% of the world's population lives with some form of disability. For websites, this means:
📊 Impact of Accessibility
- Legal compliance: Regulations like WCAG 2.1 and accessibility laws require compliance
- Wider market: Reach users with screen readers, keyboards, adaptive controllers
- Better SEO: Accessibility practices align with SEO best practices
- Maintainability: Accessible code is generally cleaner and better-structured code
- Brand reputation: Demonstrates commitment to inclusivity
Initial Setup: Playwright + @axe-core
To get started, you need Playwright and the official library for integrating axe-core.
npm install --save-dev @playwright/test @axe-core axe-playwright
The axe-playwright package provides direct integration between Playwright and axe-core, simplifying injection and test execution.
Basic Structure of an Accessibility Test
Here's the minimal structure for an accessibility test:
import { test, expect } from '@playwright/test';
import { injectAxe, checkA11y, getViolations } from 'axe-playwright';
test('home page should pass accessibility checks', async ({ page }) => {
// Navigation
await page.goto('http://localhost:4200');
// Inject axe
await injectAxe(page);
// Run check
await checkA11y(page, null, {
detailedReport: true,
detailedReportOptions: {
html: true,
},
});
});
Targeting and Specific Scopes
Sometimes you want to check only part of the page. Use the selector parameter to limit the scope:
import { test } from '@playwright/test';
import { injectAxe, checkA11y } from 'axe-playwright';
test('navbar should pass accessibility checks', async ({ page }) => {
await page.goto('http://localhost:4200');
await injectAxe(page);
// Test only the navbar
await checkA11y(page, 'nav[role="navigation"]', {
detailedReport: true,
});
});
Advanced Results Management
Not all accessibility issues are critical. You can customize how you handle results:
import { test, expect } from '@playwright/test';
import { injectAxe, getViolations } from 'axe-playwright';
test('component with custom violation handling', async ({ page }) => {
await page.goto('http://localhost:4200/components/button');
await injectAxe(page);
// Get violations without failing the test
const violations = await getViolations(page);
// Filter only critical ones
const criticalViolations = violations.filter(v => v.impact === 'critical');
// Detailed logging
if (criticalViolations.length > 0) {
console.log('Critical violations found:');
criticalViolations.forEach(v => {
console.log(`- ${v.id}: ${v.description}`);
v.nodes.forEach(n => {
console.log(` Element: ${n.html}`);
console.log(` Message: ${n.failureSummary}`);
});
});
}
// Expectation
expect(criticalViolations).toHaveLength(0);
});
Tip: Excluding Known Violations
Sometimes a violation is a false-positive or intentional. You can exclude it:
await checkA11y(page, null, {
rules: {
'color-contrast': { enabled: false }, // Disable contrast check
'aria-required-attr': { enabled: false },
},
});
Testing Interactive Components
Many components require interaction. Test accessibility after every state change:
test('modal dialog should be accessible in all states', async ({ page }) => {
await page.goto('http://localhost:4200/components/modal');
await injectAxe(page);
// Test initial state (modal closed)
await checkA11y(page, null, { detailedReport: true });
// Open modal
await page.click('button:has-text("Open Modal")');
await page.waitForSelector('[role="dialog"]');
// Test modal open state
await checkA11y(page, '[role="dialog"]', { detailedReport: true });
// Focus trap test (focus must not escape modal)
await page.press('[role="dialog"]', 'Tab');
const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
expect(['BUTTON', 'INPUT', 'A']).toContain(focusedElement);
// Close with Escape
await page.press('[role="dialog"]', 'Escape');
await page.waitForSelector('[role="dialog"]', { state: 'hidden' });
// Verify focus returned to button
const returnFocus = await page.evaluate(() => document.activeElement?.textContent);
expect(returnFocus).toContain('Open Modal');
});
Test Suite Level Automation
You can create a reusable helper that applies accessibility tests across multiple components:
import { Page } from '@playwright/test';
import { injectAxe, checkA11y, getViolations } from 'axe-playwright';
export async function testComponentAccessibility(
page: Page,
componentUrl: string,
selector?: string
): Promise {
await page.goto(componentUrl);
await injectAxe(page);
const violations = await getViolations(page);
const critical = violations.filter(v => v.impact === 'critical');
if (critical.length > 0) {
throw new Error(
`${critical.length} critical a11y violations found on ${componentUrl}`
);
}
await checkA11y(page, selector || null, { detailedReport: true });
}
// Usage
test('all component pages pass a11y', async ({ page }) => {
const components = [
'/components/button',
'/components/input',
'/components/select',
'/components/dropdown',
];
for (const comp of components) {
await testComponentAccessibility(page, comp);
}
});
Best Practices for Accessibility Testing
Here are the recommended practices to apply in your tests:
✅ Best Practices
- Test early and often: Include a11y tests from the development phase
- Integrate with CI/CD: Fail the build if critical violations are detected
- Test without false expectations: Don't disable rules arbitrarily; understand why
- Vary tests across devices: Test both keyboard navigation and screen reader
- Document exclusions: If you ignore a violation, explain the reason in your code
- Maintain access data: Use reports to track improvements over time
CI/CD Configuration for Accessibility
Integrate tests into your build pipeline to ensure no commit degrades accessibility:
name: Accessibility Tests
on: [push, pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run build
- run: npm run test:a11y
- name: Upload a11y report
if: failure()
uses: actions/upload-artifact@v3
with:
name: a11y-report
path: ./a11y-reports/
Common Troubleshooting
Here are the most frequently encountered issues and how to resolve them:
🔧 Common Issues
- axe won't inject: Verify the page is fully loaded before
injectAxe() - False positives on dynamic elements: Add
await page.waitForLoadState('networkidle')before tests - Screen reader doesn't detect content: Use
roleandaria-labelcorrectly - Flaky tests: Add
waitForSelectorbefore testing dynamic components
FAQ - Frequently Asked Questions
Q: What's the difference between axe-core and WAVE?
axe-core is a free open-source library that's programmatically testable. WAVE is a browser tool primarily manual (though it has an API). For automation, axe-core is the best choice.
Q: Can I use axe-core with non-Playwright tests?
Yes! axe-core works with Cypress, Selenium, Puppeteer, and even directly in the browser via console. Each framework has its own adapter.
Q: If I have a legacy site with a11y issues, where do I start?
Start with critical issues (contrast, form labels). Use `getViolations()` to sort by impact. Address WCAG AA criteria first, then AAA. It's not a race; it's continuous improvement.
Q: Should I test every component individually?
It depends on complexity. For simple components (button, input), integration testing on the page is enough. For complex components (table, tree, modal), test each state separately.
Q: Does axe-core cover everything in WCAG 2.1?
No. axe-core covers approximately 30-40% of WCAG 2.1 criteria automatically. The rest requires manual testing (zoom, keyboard navigation, screen reader) and human review.

Commenti
Caricamento commenti...
Sign in to comment