Steven's Knowledge
Accessibility

Testing

Automated accessibility testing, manual testing techniques, and screen reader testing

Accessibility Testing

Automated tools catch about 30-50% of accessibility issues. Comprehensive testing requires a combination of automated scans, manual checks, and assistive technology testing.

Testing Strategy

Accessibility Testing Pyramid
├── Automated (CI/CD) — catch regressions fast
│   ├── Linters (eslint-plugin-jsx-a11y)
│   ├── Unit tests (jest-axe, Testing Library)
│   └── Integration tests (axe-core, Lighthouse CI)
├── Manual — catch what automation misses
│   ├── Keyboard navigation walkthrough
│   ├── Zoom and text spacing tests
│   └── Content and reading order review
└── Assistive Technology — validate real experience
    ├── Screen reader testing (NVDA, VoiceOver, JAWS)
    ├── Voice control testing (Dragon, Voice Control)
    └── Magnification software testing

Automated Testing

ESLint Plugin (Static Analysis)

Catches common mistakes during development.

npm install --save-dev eslint-plugin-jsx-a11y
// .eslintrc.js
module.exports = {
  extends: ['plugin:jsx-a11y/recommended'],
  plugins: ['jsx-a11y'],
};

Common issues caught:

  • Images without alt attributes
  • Click handlers without keyboard handlers
  • Missing htmlFor on labels
  • Invalid ARIA attributes
  • Autofocus usage

Unit Testing with jest-axe

npm install --save-dev jest-axe
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

describe('Button', () => {
  it('has no accessibility violations', async () => {
    const { container } = render(
      <button onClick={() => {}}>Submit</button>
    );
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

describe('Form', () => {
  it('has no accessibility violations', async () => {
    const { container } = render(
      <form>
        <label htmlFor="email">Email</label>
        <input id="email" type="email" required />
        <button type="submit">Submit</button>
      </form>
    );
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

Testing Library (Accessibility-First Queries)

Testing Library encourages accessible patterns by prioritizing queries that reflect how users interact with the page.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// Query priority (most to least preferred):
// 1. getByRole — accessible role
// 2. getByLabelText — form fields
// 3. getByPlaceholderText — when no label
// 4. getByText — non-interactive text
// 5. getByDisplayValue — current input value
// 6. getByAltText — images
// 7. getByTitle — title attribute
// 8. getByTestId — last resort

test('form submission', async () => {
  const user = userEvent.setup();
  render(<LoginForm />);

  // Uses accessible queries
  const emailInput = screen.getByRole('textbox', { name: /email/i });
  const passwordInput = screen.getByLabelText(/password/i);
  const submitButton = screen.getByRole('button', { name: /sign in/i });

  await user.type(emailInput, 'user@example.com');
  await user.type(passwordInput, 'password123');
  await user.click(submitButton);

  expect(screen.getByRole('alert')).toHaveTextContent('Welcome!');
});

test('dialog keyboard interaction', async () => {
  const user = userEvent.setup();
  render(<ConfirmDialog />);

  // Open dialog
  await user.click(screen.getByRole('button', { name: /delete/i }));

  // Verify dialog is open
  const dialog = screen.getByRole('dialog');
  expect(dialog).toBeInTheDocument();

  // Close with Escape
  await user.keyboard('{Escape}');
  expect(dialog).not.toBeInTheDocument();
});

E2E Testing with axe-core

npm install --save-dev @axe-core/playwright
// Playwright + axe-core
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('homepage has no accessibility violations', async ({ page }) => {
  await page.goto('/');

  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
    .analyze();

  expect(results.violations).toEqual([]);
});

test('navigation is keyboard accessible', async ({ page }) => {
  await page.goto('/');

  // Tab through main navigation
  await page.keyboard.press('Tab'); // Skip link
  await page.keyboard.press('Enter'); // Activate skip link

  // Verify focus moved to main content
  const focused = await page.evaluate(() => document.activeElement?.id);
  expect(focused).toBe('main-content');
});

// Scan specific page sections
test('form section is accessible', async ({ page }) => {
  await page.goto('/contact');

  const results = await new AxeBuilder({ page })
    .include('#contact-form')
    .analyze();

  expect(results.violations).toEqual([]);
});

Lighthouse CI

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000/', 'http://localhost:3000/about'],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        'categories:accessibility': ['error', { minScore: 0.9 }],
        // Specific audits
        'color-contrast': 'error',
        'document-title': 'error',
        'html-has-lang': 'error',
        'image-alt': 'error',
        'label': 'error',
        'link-name': 'error',
        'list': 'error',
        'meta-viewport': 'error',
      },
    },
    upload: {
      target: 'temporary-public-storage',
    },
  },
};
# Run Lighthouse CI
npx lhci autorun

Manual Testing

Keyboard Testing Checklist

CheckWhat to Verify
Tab through entire pageAll interactive elements reachable, logical order
Shift+TabReverse navigation works
Enter/Space on buttonsAll buttons activate
Escape on modals/popupsDialogs close, focus returns
Arrow keys in widgetsTabs, menus, dropdowns navigate correctly
Focus visibilityFocus indicator always visible
No keyboard trapsCan always Tab away from any element
Skip linkBypasses navigation, moves focus to main content

Zoom Testing

TestExpected Result
Browser zoom to 200%Content reflows, no horizontal scroll
Browser zoom to 400%Content remains readable in single column
Text-only zoom (Firefox)Text enlarges without breaking layout
Text spacing overrideContent remains visible, no overlap or clipping

Content Review

  • Reading order matches visual order
  • Heading hierarchy is logical (no skipped levels)
  • Links have descriptive text (not just "click here" or "read more")
  • Error messages explain how to fix the problem
  • Color is not the sole indicator of information
  • Images have appropriate alt text

Screen Reader Testing

Screen Reader + Browser Combinations

Screen ReaderPlatformBrowserUsage
NVDAWindowsFirefox, ChromeMost popular free screen reader
JAWSWindowsChrome, EdgeMost popular commercial screen reader
VoiceOvermacOSSafariBuilt into macOS
VoiceOveriOSSafariBuilt into iOS
TalkBackAndroidChromeBuilt into Android
NarratorWindowsEdgeBuilt into Windows

VoiceOver Quick Reference (macOS)

ActionKeys
Turn on/offCmd + F5
Navigate nextVO + Right Arrow (Ctrl + Option + Right)
Navigate previousVO + Left Arrow
Activate elementVO + Space
Read allVO + A
Open rotor (landmarks, headings)VO + U
Navigate by headingVO + Cmd + H

NVDA Quick Reference (Windows)

ActionKeys
Turn onCtrl + Alt + N
Stop speakingCtrl
Navigate nextTab or Down Arrow
Navigate headingsH
Navigate landmarksD
List elementsNVDA + F7
Read current lineNVDA + Up Arrow

What to Test with Screen Readers

Screen Reader Testing Checklist
├── Page Structure
│   ├── Page title is announced
│   ├── Headings create logical outline
│   ├── Landmarks are present and labeled
│   └── Language is correctly identified
├── Navigation
│   ├── Skip link works
│   ├── Navigation items are announced with count
│   ├── Current page is indicated (aria-current)
│   └── Focus management on route changes
├── Forms
│   ├── Labels announced with each input
│   ├── Required fields indicated
│   ├── Error messages announced
│   └── Groups (fieldset/legend) announced
├── Dynamic Content
│   ├── Live regions announce updates
│   ├── Modals trap focus and announce
│   ├── Loading states communicated
│   └── Status messages announced
└── Interactive Widgets
    ├── Custom components announce role, name, state
    ├── State changes announced (expanded, checked, selected)
    ├── Keyboard patterns work as expected
    └── Disabled state communicated

Accessibility Audit Workflow

Audit Workflow
├── 1. Automated Scan
│   ├── Run axe-core or Lighthouse on all pages
│   ├── Fix all automatically detected issues
│   └── Re-scan to confirm fixes
├── 2. Keyboard Audit
│   ├── Tab through every page
│   ├── Test all interactive components
│   └── Verify focus management
├── 3. Visual Review
│   ├── Check contrast with browser DevTools
│   ├── Test at 200% zoom
│   ├── Test with text spacing overrides
│   └── Test in forced-colors mode
├── 4. Screen Reader Audit
│   ├── Test with VoiceOver (Mac) or NVDA (Windows)
│   ├── Navigate by headings, landmarks, forms
│   └── Test dynamic content and notifications
└── 5. Report & Prioritize
    ├── Document issues by WCAG criterion
    ├── Rate severity (critical, serious, moderate, minor)
    └── Create remediation plan with priorities

Issue Severity Levels

SeverityDescriptionExample
CriticalBlocks access for a user groupNo keyboard access, missing form labels
SeriousMajor barrier, workaround existsPoor contrast, missing skip link
ModerateSome difficulty, content still usableMissing landmark labels, ambiguous link text
MinorAnnoyance, doesn't block accessInconsistent focus styles, missing lang on inline text

Tool Reference

ToolTypeUse Case
eslint-plugin-jsx-a11yLinterStatic analysis during development
jest-axeUnit testComponent-level accessibility testing
@axe-core/playwrightE2E testPage-level accessibility scanning
LighthouseAuditPerformance and accessibility scoring
axe DevTools (browser extension)ManualInteractive accessibility inspection
WAVE (browser extension)ManualVisual accessibility evaluation
Colour Contrast AnalyserManualColor contrast checking (desktop app)
Accessibility InsightsManualGuided manual assessment (Microsoft)
pa11yCLI / CIAutomated accessibility testing
Storybook a11y addonDevelopmentComponent accessibility testing in Storybook

Best Practices

Accessibility Testing Guidelines

  1. Integrate automated testing (axe, Lighthouse) into CI/CD
  2. Use Testing Library queries that enforce accessible patterns
  3. Test keyboard navigation manually on every feature
  4. Test with at least one screen reader before release
  5. Audit contrast, zoom, and text spacing during design review
  6. Prioritize fixes by severity — critical blockers first
  7. Automate what you can, but never skip manual testing
  8. Include accessibility in your Definition of Done

On this page