QA Engineer Skills 2026QA-2026axe-core Integration

axe-core Integration

The Industry Standard

axe-core (by Deque Systems) is the most widely used accessibility testing engine, available as a library, browser extension, and CI tool. It implements over 100 rules covering WCAG 2.0, 2.1, and 2.2 at both A and AA levels.


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);
    });
});

Accessibility Trees via Playwright

Playwright can access the browser's accessibility tree, which represents what assistive technologies actually see:

// 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;
}

Testing Interactive States

Accessibility issues often appear in interactive states that axe-core's default scan misses:

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();
    }
});

Generating Accessibility Reports

// 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 is the foundation of any accessibility testing strategy. It runs in milliseconds, catches the most common violations, and integrates with every major test framework. Run it on every page on every PR as a non-negotiable quality gate.