Modern QA2026Интеграция axe-core
Join

Course10 Visual & Accessibility Testing

Cutting-edge · Chapter 10

Интеграция 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 как необсуждаемый контрольный шлюз качества.