Skip to main content

Testing Accessibility

Automated testing, manual testing, axe-core, Lighthouse, and accessibility testing strategies.

Question: How do you test accessibility in frontend applications?​

Difficulty: 🟑 Medium | Frequency: ⭐⭐⭐⭐ | Companies: Google, Airbnb

Testing Strategy​

Automated tools catch approximately 30% of accessibility issues. The rest require manual testing and real user validation.

LayerToolsCatches
Unit/Componentjest-axe, @testing-library/reactMissing labels, role issues
Integrationcypress-axePage-level contrast, structure
Browseraxe DevTools, WAVEAll automated checks
ManualKeyboard + screen readerFocus management, announcements

Automated Testing​

jest-axe (Component Tests)​

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

expect.extend(toHaveNoViolations);

test('should have no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

test('form is accessible', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

@testing-library Accessible Queries​

Testing Library encourages accessible queries that mirror how screen readers discover elements:

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

test('login form is accessible', () => {
render(<LoginForm />);

// Find by label (screen-reader friendly)
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /sign in/i });

expect(emailInput).toBeInTheDocument();
expect(passwordInput).toBeInTheDocument();
expect(submitButton).toBeInTheDocument();
});

// Query priority (most to least accessible):
// getByRole β†’ getByLabelText β†’ getByPlaceholderText β†’ getByText β†’ getByTestId

cypress-axe (E2E Tests)​

// cypress/support/commands.js
import 'cypress-axe';

describe('Home page', () => {
it('has no accessibility violations', () => {
cy.visit('/');
cy.injectAxe();
cy.checkA11y(); // Checks entire page
});

it('modal has no violations', () => {
cy.visit('/');
cy.injectAxe();
cy.get('[data-cy="open-modal"]').click();
cy.checkA11y('[role="dialog"]'); // Check only the modal
});
});

Storybook a11y Addon​

// .storybook/main.js
module.exports = {
addons: ['@storybook/addon-a11y']
};

// Automatically checks all stories β€” see violations in Accessibility tab

Browser-Based Tools​

axe DevTools (Browser Extension)​

  1. Install from Chrome Web Store
  2. Open DevTools β†’ axe DevTools tab
  3. Click "Scan ALL of my page"
  4. Review violations with issue descriptions and fix suggestions

Catches: missing labels, insufficient contrast, missing landmarks, ARIA misuse, keyboard traps.

WAVE (Web Accessibility Evaluation Tool)​

https://wave.webaim.org/

Or install the browser extension. Provides visual overlay showing:

  • Structural elements (headings, landmarks)
  • ARIA attributes
  • Color contrast
  • Errors and alerts

Lighthouse (Chrome DevTools)​

  1. Open DevTools β†’ Lighthouse tab
  2. Check "Accessibility" category
  3. Click "Generate report"
  4. Scores 0–100, shows specific failures with WCAG references

Manual Testing Checklist​

Keyboard Navigation​

β–‘ Tab moves forward through all interactive elements
β–‘ Shift+Tab moves backward
β–‘ Enter activates links and buttons
β–‘ Space activates buttons
β–‘ Escape closes modals and dropdowns
β–‘ Arrow keys work within widgets (tabs, menus, grids)
β–‘ Focus indicator is visible at all times
β–‘ No keyboard traps (can always Tab away)
β–‘ Skip link appears on first Tab press

Screen Reader Testing​

β–‘ Page title announced on load
β–‘ Images have appropriate alt text
β–‘ Form labels announced with inputs
β–‘ Error messages announced immediately
β–‘ Modal announces title when opened
β–‘ Dynamic updates announced via live regions
β–‘ Navigation landmarks work (D key in NVDA)
β–‘ Headings form logical hierarchy (H key in NVDA)
β–‘ Custom widgets announce state changes

Visual Checks​

β–‘ Color contrast meets AA (4.5:1 normal, 3:1 large text)
β–‘ Links underlined or distinguishable without color
β–‘ Focus indicator visible on all elements
β–‘ Error states use more than just color
β–‘ Text scales to 200% without loss of content
β–‘ No content lost on small screens (320px width)

How axe-core Works​

axe-core analyzes the DOM + accessibility tree and applies WCAG rules:

// Simplified concept of what axe-core does
class AccessibilityAuditor {
audit(element) {
const tree = buildAccessibilityTree(element);
const violations = [];

// Rule: images must have alt text
tree.images.forEach(img => {
if (!img.alt && !img.ariaLabel && !img.ariaLabelledBy) {
violations.push({
id: 'image-alt',
impact: 'critical',
help: 'Images must have alternate text',
nodes: [img]
});
}
});

// Rule: color contrast
tree.textElements.forEach(el => {
const ratio = calculateContrastRatio(el.color, el.backgroundColor);
if (ratio < 4.5 && el.fontSize < 24) {
violations.push({
id: 'color-contrast',
impact: 'serious',
help: 'Elements must have sufficient color contrast',
nodes: [el]
});
}
});

return violations;
}
}

CI/CD Integration​

# GitHub Actions: run accessibility checks on every PR
name: Accessibility
on: [pull_request]

jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm install
- run: npm test -- --testPathPattern="a11y|accessibility"
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
urls: http://localhost:3000
budgetPath: ./lighthouse-budget.json

Automated vs. Manual Coverage​

Issue TypeAutomatedManual Required
Missing alt textβœ…
Missing form labelsβœ…
Color contrastβœ…
Missing landmarksβœ…
Keyboard trapβŒβœ…
Focus managementβŒβœ…
Screen reader announcementsβŒβœ…
Confusing UX for AT usersβŒβœ…

Always combine automated and manual testing β€” automated tools are a starting point, not a guarantee.


Content from Frontend-Master-Prep-Series β€” 08-accessibility