/**
* Axe Accessibility Analyzer (via Playwright + axe-core)
* Uses axe-core library to test WCAG compliance
* Open source and free to use
*/
import { chromium, Browser, Page } from 'playwright';
export interface AxeOptions {
/** WCAG level to test: 'wcag2a', 'wcag2aa', 'wcag2aaa', 'wcag21a', 'wcag21aa', 'wcag22aa' */
wcagLevel?: string;
/** Timeout in milliseconds (default: 60000 = 1 minute) */
timeout?: number;
}
export interface AxeViolation {
id: string;
impact: 'minor' | 'moderate' | 'serious' | 'critical';
description: string;
helpUrl: string;
nodes: number;
}
export interface AxeResult {
tool: 'axe';
success: boolean;
url: string;
wcagLevel: string;
violations: number;
critical: number;
serious: number;
moderate: number;
minor: number;
passes: number;
incomplete: number;
issues: AxeViolation[];
error?: string;
}
/**
* Analyze website accessibility using axe-core via Playwright
* Open source, free, finds ~57% of WCAG issues automatically with zero false positives
*/
export async function analyzeAxe(
url: string,
options: AxeOptions = {}
): Promise<AxeResult> {
let browser: Browser | null = null;
let page: Page | null = null;
try {
// Launch browser
browser = await chromium.launch({ headless: true });
page = await browser.newPage();
// Navigate to page
await page.goto(url, { timeout: options.timeout || 60000, waitUntil: 'networkidle' });
// Inject axe-core
await page.addScriptTag({
url: 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.10.2/axe.min.js',
});
// Run axe analysis
const wcagLevel = options.wcagLevel || 'wcag2aa';
const results = await page.evaluate((level) => {
return (window as any).axe.run({
runOnly: {
type: 'tag',
values: [level],
},
});
}, wcagLevel);
await browser.close();
// Count violations by severity
const critical = results.violations.filter((v: any) => v.impact === 'critical').length;
const serious = results.violations.filter((v: any) => v.impact === 'serious').length;
const moderate = results.violations.filter((v: any) => v.impact === 'moderate').length;
const minor = results.violations.filter((v: any) => v.impact === 'minor').length;
// Format violations
const issues: AxeViolation[] = results.violations.map((v: any) => ({
id: v.id,
impact: v.impact,
description: v.description,
helpUrl: v.helpUrl,
nodes: v.nodes.length,
}));
return {
tool: 'axe',
success: true,
url,
wcagLevel,
violations: results.violations.length,
critical,
serious,
moderate,
minor,
passes: results.passes.length,
incomplete: results.incomplete.length,
issues,
};
} catch (error) {
if (browser) {
await browser.close();
}
return {
tool: 'axe',
success: false,
url,
wcagLevel: options.wcagLevel || 'wcag2aa',
violations: 0,
critical: 0,
serious: 0,
moderate: 0,
minor: 0,
passes: 0,
incomplete: 0,
issues: [],
error: error instanceof Error ? error.message : String(error),
};
}
}