Интеграция axe-core
Updated Jul 2026
Отраслевой стандарт
axe-core (от Deque Systems) — наиболее широко используемый движок тестирования доступности, доступный как библиотека, расширение браузера и инструмент CI. Он реализует более 100 правил, охватывающих WCAG 2.0, 2.1 и 2.2 на уровнях A и AA.
Playwright + axe-core
// tests/accessibility/axe-audit.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility Audit', () => {
test('homepage has no critical accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa']) // WCAG 2.1 AA
.exclude('#third-party-widget') // Exclude content you don't control
.analyze();
// Report all violations with details
if (results.violations.length > 0) {
const report = results.violations.map(v => ({
rule: v.id,
impact: v.impact,
description: v.description,
helpUrl: v.helpUrl,
nodes: v.nodes.length,
elements: v.nodes.map(n => n.html).slice(0, 3),
}));
console.log('Accessibility violations:', JSON.stringify(report, null, 2));
}
expect(results.violations).toEqual([]);
});
test('form page meets WCAG 2.1 AA', async ({ page }) => {
await page.goto('/checkout');
const results = await new AxeBuilder({ page })
.withTags(['wcag2aa'])
.analyze();
// Separate critical from minor violations
const critical = results.violations.filter(v =>
v.impact === 'critical' || v.impact === 'serious'
);
const minor = results.violations.filter(v =>
v.impact === 'moderate' || v.impact === 'minor'
);
// Critical violations fail the build
expect(critical).toEqual([]);
// Minor violations are logged as warnings
if (minor.length > 0) {
console.warn(`${minor.length} minor accessibility issues found`);
minor.forEach(v => console.warn(` - ${v.id}: ${v.description}`));
}
});
test('all pages pass accessibility audit', async ({ page }) => {
const pages = [
'/', '/login', '/register', '/products',
'/products/1', '/cart', '/checkout', '/account',
'/help', '/about', '/privacy', '/terms'
];
const allViolations: Record<string, any[]> = {};
for (const pagePath of pages) {
await page.goto(pagePath);
const results = await new AxeBuilder({ page })
.withTags(['wcag2aa'])
.analyze();
if (results.violations.length > 0) {
allViolations[pagePath] = results.violations;
}
}
const totalViolations = Object.values(allViolations)
.reduce((sum, v) => sum + v.length, 0);
if (totalViolations > 0) {
console.error('Accessibility violations by page:');
for (const [pagePath, violations] of Object.entries(allViolations)) {
console.error(`\n${pagePath}:`);
violations.forEach(v =>
console.error(` [${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} elements)`)
);
}
}
expect(totalViolations).toBe(0);
});
});
Деревья доступности через Playwright
Playwright может обращаться к дереву доступности браузера, которое представляет то, что фактически видят вспомогательные технологии:
// tests/accessibility/tree-inspection.spec.ts
import { test, expect } from '@playwright/test';
test('navigation has correct accessible structure', async ({ page }) => {
await page.goto('/');
// Get the full accessibility tree snapshot
const snapshot = await page.accessibility.snapshot();
// Find the navigation landmark
const nav = findNode(snapshot, { role: 'navigation' });
expect(nav).not.toBeNull();
expect(nav.name).toBe('Main navigation');
// Verify all nav links are present and accessible
const links = nav.children.filter((child: any) => child.role === 'link');
const linkNames = links.map((l: any) => l.name);
expect(linkNames).toContain('Home');
expect(linkNames).toContain('Products');
expect(linkNames).toContain('Cart');
expect(linkNames).toContain('Account');
});
test('form has proper label associations', async ({ page }) => {
await page.goto('/register');
const snapshot = await page.accessibility.snapshot();
// Find all text inputs in the accessibility tree
const inputs = findAllNodes(snapshot, { role: 'textbox' });
for (const input of inputs) {
// Every text input must have a name (label)
expect(input.name).toBeTruthy();
expect(input.name.length).toBeGreaterThan(0);
// Name should not be a placeholder (common mistake)
expect(input.name).not.toMatch(/enter|type here/i);
}
});
function findNode(root: any, criteria: any): any {
if (Object.entries(criteria).every(([k, v]) => root[k] === v)) return root;
for (const child of root.children || []) {
const found = findNode(child, criteria);
if (found) return found;
}
return null;
}
function findAllNodes(root: any, criteria: any): any[] {
const results: any[] = [];
if (Object.entries(criteria).every(([k, v]) => root[k] === v)) results.push(root);
for (const child of root.children || []) {
results.push(...findAllNodes(child, criteria));
}
return results;
}
Тестирование интерактивных состояний
Проблемы доступности часто появляются в интерактивных состояниях, которые стандартное сканирование axe-core пропускает:
test('modal accessibility after opening', async ({ page }) => {
await page.goto('/');
// axe scan of the page BEFORE modal
const beforeResults = await new AxeBuilder({ page }).analyze();
expect(beforeResults.violations).toEqual([]);
// Open modal
await page.click('[data-testid="open-modal"]');
await page.waitForSelector('[role="dialog"]');
// axe scan AFTER modal opens -- catches issues in modal content
const afterResults = await new AxeBuilder({ page })
.include('[role="dialog"]')
.analyze();
expect(afterResults.violations).toEqual([]);
});
test('form error state accessibility', async ({ page }) => {
await page.goto('/register');
// Submit empty form to trigger validation
await page.click('[data-testid="submit-btn"]');
// Scan the page in its error state
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
// Verify error messages are linked to inputs via aria-describedby
const errorInputs = await page.locator('[aria-invalid="true"]').all();
for (const input of errorInputs) {
const describedby = await input.getAttribute('aria-describedby');
expect(describedby).toBeTruthy();
// Verify the referenced error message exists and is visible
const errorMsg = page.locator(`#${describedby}`);
await expect(errorMsg).toBeVisible();
}
});
Генерация отчётов о доступности
// tests/accessibility/report-generator.ts
import AxeBuilder from '@axe-core/playwright';
interface A11yReport {
url: string;
violations: number;
critical: number;
serious: number;
moderate: number;
minor: number;
details: any[];
}
async function generateReport(page, url: string): Promise<A11yReport> {
await page.goto(url);
const results = await new AxeBuilder({ page })
.withTags(['wcag2aa'])
.analyze();
return {
url,
violations: results.violations.length,
critical: results.violations.filter(v => v.impact === 'critical').length,
serious: results.violations.filter(v => v.impact === 'serious').length,
moderate: results.violations.filter(v => v.impact === 'moderate').length,
minor: results.violations.filter(v => v.impact === 'minor').length,
details: results.violations.map(v => ({
id: v.id,
impact: v.impact,
description: v.description,
helpUrl: v.helpUrl,
affectedElements: v.nodes.length,
})),
};
}
axe-core — основа любой стратегии тестирования доступности. Он выполняется за миллисекунды, выявляет наиболее распространённые нарушения и интегрируется с каждым основным фреймворком тестирования. Запускайте его на каждой странице при каждом PR как необсуждаемый контрольный шлюз качества.