Loading...
Loading...
Accessibility testing for web applications using Playwright (@playwright/test) with TypeScript and axe-core. Use when asked to write, run, or debug automated accessibility checks, keyboard navigation tests, focus management, ARIA/semantic validations, screen reader compatibility, or WCAG 2.1 Level AA compliance testing. Covers axe-core integration, POUR principles (perceivable, operable, understandable, robust), color contrast, form labels, landmarks, and accessible names.
npx skill4agent add fugazi/test-automation-skills-agents a11y-playwright-testingActivation: This skill is triggered when working with accessibility testing, WCAG compliance, axe-core scans, keyboard navigation tests, focus management, ARIA validation, or screen reader compatibility.
| Requirement | Details |
|---|---|
| Node.js | v18+ recommended |
| Playwright | |
| axe-core | |
| TypeScript | Configured in project |
# Add axe-core to existing Playwright project
npm install -D @axe-core/playwright axe-core⚠️ Critical: Automated tooling can detect ~30-40% of accessibility issues. Use automation to prevent regressions and catch common failures; manual audits are required for full WCAG conformance.
// ✅ Semantic HTML - inherently accessible
await page.getByRole('button', { name: 'Submit' }).click();
// ❌ ARIA override - requires manual keyboard/focus handling
await page.locator('[role="button"]').click(); // Often a <div>| Locator Success | Accessibility Signal |
|---|---|
| Button has accessible name |
| Input properly labeled |
| Landmark exists |
| May lack accessible name |
import AxeBuilder from '@axe-core/playwright';
import { test, expect } from '@playwright/test';
test('page has no WCAG 2.1 AA violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});test('form component is accessible', async ({ page }) => {
await page.goto('/contact');
const results = await new AxeBuilder({ page })
.include('#contact-form') // Scope to specific component
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(results.violations).toEqual([]);
});test('form is keyboard navigable', async ({ page }) => {
await page.goto('/login');
// Tab to first field
await page.keyboard.press('Tab');
await expect(page.getByLabel('Email')).toBeFocused();
// Tab to password
await page.keyboard.press('Tab');
await expect(page.getByLabel('Password')).toBeFocused();
// Tab to submit button
await page.keyboard.press('Tab');
await expect(page.getByRole('button', { name: 'Sign in' })).toBeFocused();
// Submit with Enter
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/dashboard/);
});test('dialog traps and returns focus', async ({ page }) => {
await page.goto('/settings');
const trigger = page.getByRole('button', { name: 'Delete account' });
// Open dialog
await trigger.click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
// Focus should be inside dialog
await expect(dialog.getByRole('button', { name: 'Cancel' })).toBeFocused();
// Tab should stay trapped in dialog
await page.keyboard.press('Tab');
await expect(dialog.getByRole('button', { name: 'Confirm' })).toBeFocused();
await page.keyboard.press('Tab');
await expect(dialog.getByRole('button', { name: 'Cancel' })).toBeFocused();
// Escape closes and returns focus to trigger
await page.keyboard.press('Escape');
await expect(dialog).toBeHidden();
await expect(trigger).toBeFocused();
});test('skip link moves focus to main content', async ({ page }) => {
await page.goto('/');
// First Tab should focus skip link
await page.keyboard.press('Tab');
const skipLink = page.getByRole('link', { name: /skip to (main|content)/i });
await expect(skipLink).toBeFocused();
// Activating skip link moves focus to main
await page.keyboard.press('Enter');
await expect(page.locator('#main, [role="main"]').first()).toBeFocused();
});| Principle | Focus Areas | Example Tests |
|---|---|---|
| Perceivable | Alt text, captions, contrast, structure | Image alternatives, color contrast ratio |
| Operable | Keyboard, focus, timing, navigation | Tab order, focus visibility, skip links |
| Understandable | Labels, instructions, errors, consistency | Form labels, error messages, predictable behavior |
| Robust | Valid HTML, ARIA, name/role/value | Semantic structure, accessible names |
| Tag | WCAG Level | Use Case |
|---|---|---|
| Level A | Minimum compliance |
| Level AA | Standard target |
| Level AAA | Enhanced (rarely full) |
| 2.1 Level A | WCAG 2.1 specific A |
| 2.1 Level AA | WCAG 2.1 standard |
| Beyond WCAG | Additional recommendations |
const WCAG21AA_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];// ❌ Avoid: Global rule disable
new AxeBuilder({ page }).disableRules(['color-contrast']);
// ✅ Better: Scoped exclusion with documentation
new AxeBuilder({ page })
.exclude('#third-party-widget') // Known issue: JIRA-1234, fix by Q2
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();| Problem | Cause | Solution |
|---|---|---|
| Axe finds 0 violations but app fails manual audit | Automation covers ~30-40% | Add manual testing checklist |
| False positive on dynamic content | Content not fully rendered | Wait for stable state before scan |
| Color contrast fails incorrectly | Background image/gradient | Use |
| Cannot find element by role | Missing semantic HTML | Fix markup - this is a real bug |
| Focus not visible | Missing | Add visible focus indicator CSS |
| Dialog focus not trapped | Missing focus trap logic | Implement focus trap (see snippets) |
| Skip link doesn't work | Target missing | Add tabindex to main content |
| Command | Description |
|---|---|
| Run accessibility tests only |
| Run with visible browser for debugging |
| Step through with Inspector |
| Debug mode with pause |
| Document | Content |
|---|---|
| Snippets | axe-core setup, helpers, keyboard/focus patterns |
| WCAG 2.1 AA Checklist | Manual audit checklist by POUR principle |
| ARIA Patterns | Common ARIA widget patterns and validations |
| Resource | URL |
|---|---|
| WCAG 2.1 Specification | https://www.w3.org/TR/WCAG21/ |
| WCAG Quick Reference | https://www.w3.org/WAI/WCAG21/quickref/ |
| WAI-ARIA Authoring Practices | https://www.w3.org/WAI/ARIA/apg/ |
| axe-core Rules | https://dequeuniversity.com/rules/axe/ |