1. Automated β axe-core in every test
~30 % of issuesRun axe-core (or equivalent) on every page load in every E2E test. Catches the long tail of mechanical violations.
What it catches
Color contrast, missing alt, bad heading order, invalid ARIA, missing form labels β the rule-bound issues a tool can decide on.
Tools
@axe-core/playwright, pa11y, Lighthouse, Deque axe DevTools
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage has no WCAG violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
// Fail on any violation β keep your baseline at zero
expect(results.violations, formatViolations(results.violations))
.toHaveLength(0);
});
function formatViolations(v: import('axe-core').Result[]) {
return v
.map((r) => `${r.id} (${r.impact}): ${r.help}`)
.join('\n');
}Gotcha: Tools only catch what they can rule-match. A div with role='button' onClick={...} passes axe-core BUT lacks keyboard support β and axe can't tell. Automated layer is necessary, never sufficient.