Skip to content
Accessibility Testing: Tools, Automation & CI/CD

Accessibility Testing: Tools, Automation & CI/CD

DodaTech Updated Jun 20, 2026 13 min read

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
  
Prerequisites: Completion of previous accessibility tutorials. Familiarity with Jest, Cypress, and GitHub Actions. Understanding of WCAG success criteria.

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=accessibility

WAVE

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

ToolStrengthsBest For
axe-coreDeep analysis, CI integration, all frameworksPrimary automated scanner
LighthousePerformance + a11y, Core Web VitalsQuick audits, performance teams
WAVEVisual overlay, easy to understandDesigners, visual audits
Pa11yMultiple runners, dashboard supportContinuous 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.json

Pa11y 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 Fix

Setting 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.js

Common 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

What’s the best single tool for accessibility testing?
axe-core is the industry standard. It’s used by Google, Microsoft, and Deque, and integrates with nearly every testing framework.
How often should I run accessibility tests?
On every pull request (automated) and every sprint (manual). Accessibility is not a one-time audit — it requires ongoing maintenance.
Can I automate screen reader testing?
Partially. You can check the accessibility tree and ARIA attributes, but you can’t automate whether the speech output sounds natural or whether users can complete tasks efficiently.
What should I do when my build breaks due to accessibility violations?
Fix the violation. If it’s a false positive, add it to an ignore list with a documented reason. Accessibility is as important as passing tests.
How do I convince my team to invest in accessibility testing?
Share the business case (1.3B users, $8T market, legal requirements) and start small — add one axe test to one component and show the team how easy it is.

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