Accessibility Testing: Tools, Automation & CI/CD
Accessibility testing catches barriers before they reach users — combining automated tools (which catch ~30% of issues) with manual testing (keyboard, screen reader, mobile) to ensure your site works for everyone.
What You’ll Learn
By the end of this tutorial, you’ll understand automated testing tools (axe-core, Lighthouse, WAVE, Pa11y), how to integrate accessibility checks into CI/CD pipelines with Lighthouse CI and axe GitHub Actions, a manual testing checklist, screen reader and keyboard testing procedures, mobile accessibility testing, and how to write automated accessibility tests with jest-axe and cypress-axe.
Why Accessibility Testing Matters
Accessibility bugs are like security bugs: they’re cheap to fix during development and expensive to fix in production. Automated testing catches regressions before they ship, manual testing catches the remaining 70% of issues, and CI/CD integration ensures accessibility stays high as your codebase evolves. At DodaTech, Doda Browser’s DevTools include an accessibility audit panel, and every pull request is automatically checked for WCAG violations before merging.
Accessibility Testing Learning Path
flowchart LR
A[Accessibility Overview] --> B[WCAG Compliance]
B --> C[ARIA Basics]
C --> D[Keyboard Navigation]
D --> E[Screen Readers]
E --> F[Color Contrast]
F --> G[Accessible Forms]
G --> H[Accessible Images]
H --> I[Accessible Navigation]
I --> J[Accessibility Testing]
J:::current
classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Automated Testing Tools
axe-core
axe-core is the industry standard for automated accessibility testing. It runs in the browser, CLI, and testing frameworks:
# Install axe-core CLI
npm install -g @axe-core/cli
# Run an audit on a local URL
npx axe http://localhost:3000 --save report.json
# Run on a production URL
npx axe https://example.com --exit// Programmatic usage in Node.js
const { axe } = require('axe-core');
const { JSDOM } = require('jsdom');
async function auditPage(html) {
const { window } = new JSDOM(html);
const results = await axe.run(window.document, {
runOnly: ['wcag2a', 'wcag2aa', 'wcag22aa'],
resultTypes: ['violations', 'incomplete'],
});
console.log(`Found ${results.violations.length} violations`);
results.violations.forEach(v => {
console.log(`\n[${v.impact.toUpperCase()}] ${v.help}`);
console.log(` WCAG: ${v.tags.filter(t => t.startsWith('wcag')).join(', ')}`);
console.log(` Nodes: ${v.nodes.length}`);
v.nodes.forEach(n => {
console.log(` → ${n.target.join(', ')}`);
console.log(` ${n.failureSummary}`);
});
});
return results;
}Lighthouse
Lighthouse is built into Chrome DevTools and provides an accessibility score with actionable recommendations:
# Lighthouse CLI
npx lighthouse https://example.com --output=json --output-path=report.json
# Only run accessibility audit
npx lighthouse https://example.com --only-categories=accessibilityWAVE
WAVE (Web Accessibility Evaluation Tool) is a browser extension that provides visual overlays of accessibility issues. It’s excellent for quick visual audits:
- Red icons: Errors (missing alt text, empty links)
- Green icons: Alerts (possible issues needing manual review)
- Blue icons: ARIA features
- Yellow icons: Contrast errors
Pa11y
Pa11y is a command-line accessibility tool that integrates well with CI/CD:
# Install Pa11y
npm install -g pa11y
# Run a single-page audit
pa11y https://example.com
# Run with configuration
pa11y --config .pa11yrc https://example.com{
"standard": "WCAG2AA",
"runners": ["axe", "htmlcs"],
"hideElements": ".cookie-banner",
"ignore": [
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
]
}Comparison of Automated Tools
| Tool | Strengths | Best For |
|---|---|---|
| axe-core | Deep analysis, CI integration, all frameworks | Primary automated scanner |
| Lighthouse | Performance + a11y, Core Web Vitals | Quick audits, performance teams |
| WAVE | Visual overlay, easy to understand | Designers, visual audits |
| Pa11y | Multiple runners, dashboard support | Continuous monitoring |
CI/CD Integration
Lighthouse CI
# .github/workflows/lighthouse-ci.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: npm ci && npm run build
- name: Start server
run: npm run start & sleep 5
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun --config=./lighthouserc.js// lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000'],
numberOfRuns: 3,
settings: {
onlyCategories: ['accessibility'],
},
},
assert: {
assertions: {
'categories:accessibility': ['error', { minScore: 0.9 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};axe GitHub Action
# .github/workflows/axe-a11y.yml
name: Accessibility - axe-core
on: [pull_request]
jobs:
axe:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: npm ci && npm run build
- name: Start preview
run: npx serve out &
- name: Run axe
uses: dequelabs/axe-github-action@v3
with:
urls: |
http://localhost:3000
http://localhost:3000/products
http://localhost:3000/contact
output: axe-report.json
- name: Upload report
uses: actions/upload-artifact@v4
with:
name: axe-report
path: axe-report.jsonPa11y CI
# .github/workflows/pa11y-ci.yml
name: Accessibility - Pa11y CI
on: [pull_request]
jobs:
pa11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup
run: npm ci
- name: Build & Start
run: |
npm run build
npx serve out &
sleep 5
- name: Run Pa11y CI
run: npx pa11y-ci --config .pa11yci.json{
"defaults": {
"timeout": 30000,
"standard": "WCAG2AA",
"runners": ["axe"]
},
"urls": [
"http://localhost:3000/",
"http://localhost:3000/products",
"http://localhost:3000/blog",
"http://localhost:3000/contact"
]
}Automated Accessibility Tests
jest-axe
Integrate axe into your unit tests:
// Component.test.js
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
import ProductCard from './ProductCard';
expect.extend(toHaveNoViolations);
describe('ProductCard accessibility', () => {
it('should have no accessibility violations', async () => {
const { container } = render(
<ProductCard
title="Doda Browser"
description="A fast, secure browser"
price="$0"
/>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should announce price changes', async () => {
const { container, rerender } = render(
<ProductCard title="Test" price="$10" />
);
rerender(
<ProductCard title="Test" price="$15" />
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});cypress-axe
Integrate axe into your E2E tests:
// cypress/e2e/a11y.cy.js
describe('Accessibility tests', () => {
beforeEach(() => {
cy.visit('/');
cy.injectAxe();
});
it('Home page has no violations', () => {
cy.checkA11y(null, {
runOnly: ['wcag2a', 'wcag2aa', 'wcag22aa'],
});
});
it('Navigation is accessible', () => {
cy.checkA11y('nav', {
runOnly: ['wcag2a', 'wcag2aa'],
});
});
it('Form page has no violations', () => {
cy.visit('/contact');
cy.injectAxe();
cy.checkA11y();
// Test after form submission with errors
cy.get('button[type="submit"]').click();
cy.checkA11y(null, {
includedImpacts: ['critical', 'serious'],
});
});
});Playwright with axe
// a11y.spec.js
const { test, expect } = require('@playwright/test');
const { AxeBuilder } = require('@axe-core/playwright');
test.describe('Accessibility', () => {
test('homepage should not have accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
test('product page should have no critical violations', async ({ page }) => {
await page.goto('/products/1');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.options({ resultTypes: ['violations'] })
.analyze();
const criticalViolations = results.violations.filter(
v => v.impact === 'critical' || v.impact === 'serious'
);
expect(criticalViolations).toEqual([]);
});
});Manual Testing Checklist
Automated tools catch about 30% of issues. The remaining 70% require manual testing:
## Manual Accessibility Testing Checklist
### Keyboard Testing
- [ ] Can I Tab through all interactive elements?
- [ ] Is the focus indicator always visible?
- [ ] Can I activate all elements with Enter/Space?
- [ ] Can I navigate dropdowns with arrow keys?
- [ ] Can I close modals with Escape?
- [ ] Does focus return to the trigger after closing a modal?
- [ ] Is there a skip link and does it work?
- [ ] Is there no keyboard trap (focus stuck)?
### Screen Reader Testing
- [ ] Does the page title describe the content?
- [ ] Can I navigate by headings (H1-H6)?
- [ ] Can I navigate by landmarks?
- [ ] Are all images described via alt text?
- [ ] Are form fields announced with labels?
- [ ] Are error messages announced?
- [ ] Is dynamic content (modal, update) announced?
- [ ] Does the reading order match the visual order?
### Zoom Testing
- [ ] Can I zoom to 200% without losing content?
- [ ] Can I zoom to 400% and still read text?
- [ ] Does the layout remain usable at all zoom levels?
- [ ] Does text reflow without horizontal scrolling?
### Color and Contrast
- [ ] Does all text pass 4.5:1 contrast minimum?
- [ ] Are error states conveyed with more than color?
- [ ] Are links distinguishable from body text?
- [ ] Do focus indicators have 3:1 contrast with background?
### Mobile Testing
- [ ] Are touch targets at least 24×24px (ideally 44×44px)?
- [ ] Is content not obscured by notch/status bar?
- [ ] Can the site be used in both portrait and landscape?
- [ ] Does VoiceOver/TalkBack read everything correctly?Mobile Accessibility Testing
Mobile accessibility testing is often overlooked but critical since 60% of web traffic comes from mobile devices.
iOS — VoiceOver Testing
## iOS Accessibility Testing (VoiceOver)
### Setup
1. Enable: Settings → Accessibility → VoiceOver → On
2. Practice gestures: swipe right (next), swipe left (previous),
double-tap (activate), two-finger scroll
3. Use Rotor (twist two fingers): navigate by headings, links,
form controls, landmarks
### Test Checklist
- [ ] Can I complete a purchase with VoiceOver only?
- [ ] Are all elements reachable by swiping?
- [ ] Do custom gestures work?
- [ ] Does the Rotor show appropriate navigation options?
- [ ] Is dynamic content announced?Android — TalkBack Testing
## Android Accessibility Testing (TalkBack)
### Setup
1. Enable: Settings → Accessibility → TalkBack → On
2. Similar gestures to VoiceOver
### Test Checklist
- [ ] Can I navigate with swipe left/right?
- [ ] Are all form fields labeled?
- [ ] Is the reading order correct?
- [ ] Do touch targets have sufficient spacing?Remediation Prioritization
Not all accessibility issues are equally urgent. Prioritize fixes using this framework:
flowchart TD
A[Issue Found] --> B{Level?}
B -->|Level A| C[Critical — Blocking]
B -->|Level AA| D[Serious — Significant]
B -->|Level AAA| E[Minor — Enhancement]
C --> F[Fix immediately]
D --> G[Fix within sprint]
E --> H[Add to backlog]
F --> I[Re-test with automated tools]
G --> I
H --> I
I --> J{Pass?}
J -->|No| F
J -->|Yes| K[Close issue]
# .github/ISSUE_TEMPLATE/a11y-bug.yml
name: Accessibility Issue
description: Report an accessibility violation
labels: [accessibility]
body:
- type: dropdown
id: severity
attributes:
label: Severity
options:
- Critical (Level A — blocks users)
- Serious (Level AA — significant impact)
- Moderate (Level AA — inconvenience)
- Minor (Level AAA — enhancement)
validations:
required: true
- type: input
id: wcag
attributes:
label: WCAG Criterion
description: e.g., SC 1.1.1 Non-text Content (Level A)
- type: textarea
id: description
attributes:
label: Description
description: What is the issue and where does it occur?
- type: textarea
id: screen-reader-output
attributes:
label: Screen Reader Output
description: What does the screen reader say?
- type: textarea
id: expected
attributes:
label: Expected Behavior
- type: textarea
id: remediation
attributes:
label: Proposed FixSetting Up a Full Accessibility Pipeline
Here’s a complete workflow that combines all the tools:
# .github/workflows/full-a11y.yml
name: Full Accessibility Pipeline
on:
pull_request:
paths:
- 'src/**'
- 'public/**'
jobs:
# Layer 1: Unit-level accessibility checks
unit-a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:a11y # jest-axe tests
# Layer 2: E2E accessibility checks
e2e-a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- run: npm run test:e2e:a11y # cypress-axe tests
# Layer 3: Full-page scans
scan-a11y:
runs-on: ubuntu-latest
needs: [unit-a11y, e2e-a11y]
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: npx serve out &
- uses: dequelabs/axe-github-action@v3
with:
urls: |
http://localhost:3000/
http://localhost:3000/products
http://localhost:3000/contact
# Layer 4: Lighthouse score
lighthouse:
runs-on: ubuntu-latest
needs: [scan-a11y]
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: npx serve out &
- run: |
npm install -g @lhci/cli
lhci autorun --config=./lighthouserc.jsCommon Testing Mistakes
1. Relying Only on Automated Tools
“Lighthouse gave me a score of 100, so the site is accessible.” Automated tools catch only 30% of issues. Manual testing is essential.
2. Testing Only One Page
Accessibility varies per page. A perfect homepage doesn’t mean the entire site is accessible. Test every unique template and dynamic state.
3. Testing Only One Browser
Different browsers and assistive technology combinations behave differently. Test with Chrome + NVDA, Firefox + NVDA, and Safari + VoiceOver.
4. Not Testing Error States
Modals open, form errors appear, navigation menus expand — test every state, not just the default view.
5. Ignoring the Testing Setup
“If the page has no violations but has no headings, there’s clearly an issue.” Make sure your test configurations include all relevant WCAG tags.
6. Not Prioritizing Fixes
Fixing AAA issues while Level A issues remain is wasted effort. Fix by severity: Level A first, then AA, then AAA.
7. Testing Too Late
Accessibility testing should be integrated from day one of a project, not bolted on at the end. Shift-left your testing.
Practice Questions
1. What percentage of accessibility issues do automated tools catch?
Approximately 30%. The remaining 70% require manual testing with keyboard, screen readers, and real users.
2. What’s the difference between axe-core and Lighthouse for accessibility?
axe-core provides deep, standards-based analysis of WCAG violations. Lighthouse provides a broader audit including performance and SEO, with an accessibility score based on a subset of axe rules.
3. How do you integrate accessibility testing into CI/CD?
Use GitHub Actions (or your CI provider) to run axe-core scans, Pa11y CI, Lighthouse CI, and jest-axe/cypress-axe tests on every pull request.
4. What should you test manually that automated tools cannot catch?
Meaningful alt text, logical focus order, screen reader announcement quality, reading order, keyboard navigation flow, and real-world usability.
5. Challenge: Set up a complete accessibility pipeline for a project. Include unit tests (jest-axe), E2E tests (cypress-axe), CI scans (axe GitHub Action), and a Lighthouse score assertion. Break the build on any critical or serious violations.
Real-World Task
Run a full accessibility audit on your project. Use all four layers: automated scans, manual keyboard testing, screen reader testing, and mobile testing. Document every issue with severity, WCAG criterion, expected behavior, and proposed fix. Fix all Level A issues first, then Level AA.
FAQ
Try It Yourself
Create a custom accessibility test reporter:
// a11y-reporter.js — custom reporter combining multiple tools
const { axe } = require('axe-core');
const { JSDOM } = require('jsdom');
async function runFullAudit(urls) {
const report = {
timestamp: new Date().toISOString(),
tool: 'DodaTech A11y Reporter v1.0',
summary: { passed: 0, failed: 0, total: 0 },
pages: [],
};
for (const url of urls) {
console.log(`\nAuditing: ${url}`);
const response = await fetch(url);
const html = await response.text();
const { window } = new JSDOM(html);
const results = await axe.run(window.document, {
runOnly: ['wcag2a', 'wcag2aa', 'wcag22aa'],
});
const pageReport = {
url,
violations: results.violations.map(v => ({
id: v.id,
impact: v.impact,
help: v.help,
wcag: v.tags.filter(t => t.startsWith('wcag')),
nodes: v.nodes.length,
elements: v.nodes.map(n => n.target.join(', ')),
})),
passes: results.passes.length,
incomplete: results.incomplete.length,
};
pageReport.score = Math.round(
(pageReport.passes / (pageReport.passes + pageReport.violations.length)) * 100
);
report.pages.push(pageReport);
report.summary.total += pageReport.violations.length + pageReport.passes;
report.summary.failed += pageReport.violations.length;
report.summary.passed += pageReport.passes;
console.log(` Violations: ${pageReport.violations.length}`);
console.log(` Passes: ${pageReport.passes}`);
console.log(` Score: ${pageReport.score}%`);
}
return report;
}
// Usage
runFullAudit([
'http://localhost:3000/',
'http://localhost:3000/products',
'http://localhost:3000/contact',
]).then(report => {
console.log('\n=== FINAL REPORT ===');
console.log(`Total pages: ${report.pages.length}`);
console.log(`Passed checks: ${report.summary.passed}`);
console.log(`Failed checks: ${report.summary.failed}`);
console.log(`Overall: ${Math.round(report.summary.passed / report.summary.total * 100)}%`);
});Expected output: A structured JSON report showing violations per page, categorized by impact and WCAG criterion, with an overall score.
What’s Next
Congratulations on completing the Accessibility Testing tutorial and the entire Accessibility (a11y) learning path! Here’s where to go from here:
- Practice daily — Keep accessibility at the forefront of every feature you build
- Build a project — Set up a full CI/CD accessibility pipeline for your team
- Explore related topics — Review any tutorial in the path you’d like to deepen
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro