Setting Up CI/CD Pipeline for Accessibility Testing
Automated accessibility checking in CI/CD prevents regressions: every PR undergoes audit, and if a new WCAG violation appears, the build fails. Multiple tools are combined: axe-core for component testing, Playwright for E2E, Lighthouse CI for page-level audit.
Coverage Strategy
Component unit tests (Jest + jest-axe)
→ 100% of React components via Storybook
→ Run on each commit
E2E page tests (Playwright + axe-core)
→ Key pages: homepage, catalog, form, checkout
→ Run on PR to main
Lighthouse CI (page-level audit)
→ Budget: minimum 90/100 on accessibility
→ Run on PR to main
Weekly full audit (Pa11y + sitemap)
→ All public pages
→ Report to Slack
GitHub Actions: Full Pipeline
# .github/workflows/accessibility.yml
name: Accessibility Tests
on:
pull_request:
branches: [main]
schedule:
- cron: '0 9 * * 1' # Every Monday at 9:00
jobs:
component-a11y:
name: Component Accessibility (Jest)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm test -- --testPathPattern="a11y|accessibility" --coverage=false
env:
CI: true
e2e-a11y:
name: E2E Accessibility (Playwright)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npx playwright install chromium --with-deps
- name: Start app
run: npm run build && npm start &
- name: Wait for app
run: npx wait-on http://localhost:3000 --timeout 60000
- name: Run accessibility tests
run: npx playwright test tests/accessibility/
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
lighthouse-a11y:
name: Lighthouse Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci && npm run build
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v10
with:
urls: |
http://localhost:3000
http://localhost:3000/catalog
budgetPath: .lighthouserc.json
temporaryPublicStorage: true
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
Playwright: Page Accessibility Test
// tests/accessibility/pages.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
const PAGES_TO_TEST = [
{ path: '/', name: 'Homepage' },
{ path: '/catalog', name: 'Catalog' },
{ path: '/contact', name: 'Contact' },
];
for (const { path, name } of PAGES_TO_TEST) {
test(`${name}: WCAG 2.1 AA`, async ({ page }) => {
await page.goto(path);
await page.waitForLoadState('networkidle');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
// Detailed violation output
if (results.violations.length > 0) {
const summary = results.violations.map(v =>
`[${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} elements)`
).join('\n');
console.error(`Violations on "${name}":\n${summary}`);
}
expect(results.violations).toHaveLength(0);
});
}
Pull Request Reporting
# Comment on PR with audit results
- name: Comment PR
uses: thollander/actions-comment-pull-request@v2
if: always()
with:
message: |
## Accessibility Report
| Tool | Status |
|---|---|
| Jest + axe | ${{ job.status == 'success' && '✅ Passed' || '❌ Errors' }} |
| Playwright | ${{ needs.e2e-a11y.result == 'success' && '✅ Passed' || '❌ Errors' }} |
| Lighthouse | ${{ needs.lighthouse-a11y.result == 'success' && '✅ ≥ 90' || '❌ < 90' }} |
Known Exclusions
Fix false positives in config:
// axe-config.js — exclusions for third-party widgets
const AXE_CONFIG = {
rules: [
// Chat widget (third-party, not controlled)
{ id: 'color-contrast', selector: '#intercom-frame', enabled: false },
],
};
Timeline
Full CI/CD accessibility pipeline with Jest, Playwright, Lighthouse, and PR reporting: 3–4 business days.







