level 15 / axe-core-playwright
axe-core with Playwright
Integrate axe-core into Playwright tests to automatically detect WCAG violations on any page or component.
What axe-core Does
axe-core is an automated accessibility testing engine developed by Deque. It analyses the DOM and reports WCAG violations with severity, element reference, and remediation guidance.
Coverage: axe-core catches ~30-40% of WCAG issues automatically. The rest require manual testing (keyboard navigation, screen reader experience, colour contrast in context).
Setup
npm install -D @axe-core/playwright
// playwright.config.ts — no special config needed, axe runs per-test
Basic Usage
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('home page has no WCAG violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
Scoping Axe Analysis
// Analyse a specific component only
test('login form is accessible', async ({ page }) => {
await page.goto('/login');
const results = await new AxeBuilder({ page })
.include('#login-form') // only analyse this element
.analyze();
expect(results.violations).toEqual([]);
});
// Exclude a known third-party widget you can't fix
test('page is accessible excluding chat widget', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.exclude('#third-party-chat')
.analyze();
expect(results.violations).toEqual([]);
});
Targeting Specific WCAG Tags
// Only check WCAG 2.1 Level AA rules
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
// Available tags:
// wcag2a, wcag2aa, wcag2aaa → WCAG 2.0 levels
// wcag21a, wcag21aa → WCAG 2.1 additions
// wcag22aa → WCAG 2.2 additions
// best-practice → additional best practices
Readable Failure Output
By default, a failure dumps the full violations array. Make it readable:
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility', () => {
test('no violations on checkout page', async ({ page }, testInfo) => {
await page.goto('/checkout');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
// Attach full results to test report for investigation
await testInfo.attach('accessibility-scan-results', {
body: JSON.stringify(results, null, 2),
contentType: 'application/json',
});
// Readable failure message
const violationSummary = results.violations.map(v =>
`[${v.impact}] ${v.id}: ${v.description}\n Elements: ${v.nodes.map(n => n.target).join(', ')}`
).join('\n\n');
expect(results.violations, violationSummary).toEqual([]);
});
});
Axe Fixture
// fixtures/axe.fixture.ts
import { test as base } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
type A11yFixtures = {
checkA11y: (selector?: string) => Promise<void>;
};
export const test = base.extend<A11yFixtures>({
checkA11y: async ({ page }, use, testInfo) => {
await use(async (selector?: string) => {
const builder = new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']);
if (selector) builder.include(selector);
const results = await builder.analyze();
await testInfo.attach('axe-results', {
body: JSON.stringify(results.violations, null, 2),
contentType: 'application/json',
});
expect(results.violations).toEqual([]);
});
},
});
// Usage in test
test('product page is accessible', async ({ page, checkA11y }) => {
await page.goto('/product/123');
await checkA11y('#product-details');
});
Understanding Violation Output
{
"id": "color-contrast",
"impact": "serious",
"description": "Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds",
"helpUrl": "https://dequeuniversity.com/rules/axe/4.7/color-contrast",
"nodes": [
{
"target": ["#submit-btn"],
"failureSummary": "Fix any of the following: Element has insufficient color contrast of 2.5:1 (foreground: #999999, background: #ffffff, font size: 14pt). Expected contrast ratio of 4.5:1"
}
]
}
Impact levels: critical → serious → moderate → minor