Answer
Accessibility Testing in Playwright
Built-in Accessibility Snapshot
Playwright can scan the page's accessibility tree:
TypeScript
import { test, expect } from '@playwright/test';
test('check accessibility tree', async ({ page }) => {
await page.goto('/login');
// Get the full accessibility snapshot
const snapshot = await page.accessibility.snapshot();
console.log(JSON.stringify(snapshot, null, 2));
// Shows: role, name, children for every accessible element
});
Integration with axe-core (WCAG Compliance)
Bash
npm install --save-dev @axe-core/playwright
TypeScript
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 }).analyze();
expect(results.violations).toHaveLength(0);
});
test('login page is accessible (WCAG 2.1 AA)', async ({ page }) => {
await page.goto('/login');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
if (results.violations.length > 0) {
console.log('Violations found:');
results.violations.forEach(v => {
console.log(` [${v.impact}] ${v.id}: ${v.description}`);
v.nodes.forEach(n => console.log(` - ${n.html}`));
});
}
expect(results.violations).toHaveLength(0);
});
Exclude Elements from Scan
TypeScript
const results = await new AxeBuilder({ page })
.withTags(['wcag2a'])
.exclude('.third-party-widget') // Exclude third-party widgets
.exclude('#cookie-banner')
.analyze();
Run on Specific Element
TypeScript
const results = await new AxeBuilder({ page })
.include('#main-form') // Only scan the form
.withTags(['wcag2aa'])
.analyze();
Role-Based Locators as Accessibility Tests
Using getByRole() inherently validates accessible HTML structure:
TypeScript
test('navigation is accessible', async ({ page }) => {
await page.goto('/');
// If these work, the HTML has correct ARIA roles
await expect(page.getByRole('navigation')).toBeVisible();
await expect(page.getByRole('main')).toBeVisible();
await expect(page.getByRole('banner')).toBeVisible();
// Verify interactive elements are correctly labeled
await expect(page.getByRole('button', { name: 'Close' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Skip to main content' })).toBeVisible();
});
Keyboard Navigation Test
TypeScript
test('form is keyboard navigable', async ({ page }) => {
await page.goto('/contact');
// Tab through form fields
await page.keyboard.press('Tab');
await expect(page.getByLabel('Name')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByLabel('Email')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByLabel('Message')).toBeFocused();
// Submit via keyboard
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await expect(page.getByText('Message sent')).toBeVisible();
});
Color Contrast Test
TypeScript
// axe-core checks color contrast automatically with wcag2aa tag
const results = await new AxeBuilder({ page })
.withTags(['wcag2aa'])
.analyze();
const contrastViolations = results.violations.filter(v => v.id === 'color-contrast');
expect(contrastViolations).toHaveLength(0);
