Accessibility testing with Playwright and axe-core
Playwright can test applications for accessibility issues using the @axe-core/playwright package, which runs the axe accessibility testing engine. This can detect problems like poor color contrast, UI controls without labels, and duplicate element IDs. Automated testing can find some common issues, but manual testing and inclusive user testing are also recommended.
AxeBuilder.analyze() scans page in current state
The AxeBuilder.analyze() method scans a page in whatever state it is currently in when the method is called. To scan parts of a page that appear after user interactions, use Locators to interact with the page and wait for elements to appear before calling analyze().
AxeBuilder methods for filtering scans
AxeBuilder supports several methods to configure scans: include(selector) constrains a scan to specific parts of a page; withTags(array) filters rules by WCAG tags such as wcag2a, wcag2aa, wcag21a, wcag21aa; exclude(selector) removes elements from scans; disableRules(array) temporarily disables specific rules by ID.
exclude() method downsides
AxeBuilder.exclude() excludes the specified elements and all their descendants from scanning. Avoid using it with components that have many children. The method prevents all rules from running against the specified elements, not just rules for known issues.
Snapshot accessibility violations with fingerprints
When using snapshots to track known accessibility issues, create a fingerprint containing only the rule ID and CSS selectors of affected elements, rather than snapshotting the entire violations array. The full violations object contains implementation details like rendered HTML that change frequently and make tests fragile.
Attach accessibility scan results to tests
Use testInfo.attach() to include full axe scan results as a test attachment for debugging. The scan results contain more than just violations—they also include information about passed rules and inconclusive results. Pass an object with body as stringified JSON and contentType as 'application/json'.
Example: scan entire page for accessibility violations
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('homepage', () => {
test('should not have any automatically detectable accessibility issues', async ({ page }) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
});
Example: scan specific page section after interaction
test('navigation menu should not have automatically detectable accessibility violations', async ({ page }) => {
await page.goto('https://your-site.com/');
await page.getByRole('button', { name: 'Navigation Menu' }).click();
await page.locator('#navigation-menu-flyout').waitFor();
const accessibilityScanResults = await new AxeBuilder({ page })
.include('#navigation-menu-flyout')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
Example: scan for WCAG A and AA violations only
test('should not have any automatically detectable WCAG A or AA violations', async ({ page }) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
Example: exclude element from accessibility scan
test('should not have any accessibility violations outside of elements with known issues', async ({ page }) => {
await page.goto('https://your-site.com/page-with-known-issues');
const accessibilityScanResults = await new AxeBuilder({ page })
.exclude('#element-with-known-issue')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
Example: disable specific accessibility rules
test('should not have any accessibility violations outside of rules with known issues', async ({ page }) => {
await page.goto('https://your-site.com/page-with-known-issues');
const accessibilityScanResults = await new AxeBuilder({ page })
.disableRules(['duplicate-id'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
Example: attach scan results to test for debugging
test('example with attachment', async ({ page }, testInfo) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
await testInfo.attach('accessibility-scan-results', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json'
});
expect(accessibilityScanResults.violations).toEqual([]);
});
Example: create reusable AxeBuilder test fixture
import { test as base } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
type AxeFixture = {
makeAxeBuilder: () => AxeBuilder;
};
export const test = base.extend<AxeFixture>({
makeAxeBuilder: async ({ page }, use) => {
const makeAxeBuilder = () => new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('#commonly-reused-element-with-known-issue');
await use(makeAxeBuilder);
}
});
export { expect } from '@playwright/test';
Example: use custom AxeBuilder fixture in test
const { test, expect } = require('./axe-test');
test('example using custom fixture', async ({ page, makeAxeBuilder }) => {
await page.goto('https://your-site.com/');
const accessibilityScanResults = await makeAxeBuilder()
.include('#specific-element-under-test')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
Test fixtures for shared AxeBuilder configuration
Test fixtures are useful for sharing common AxeBuilder configuration across many tests. Use cases include applying the same set of rules to all tests, suppressing known violations in elements that appear on many pages, and consistently attaching accessibility reports for multiple scans.