Building an Accessibility Testing Pipeline — practical accessibility testing techniques.
Building an Accessibility Testing Pipeline is an essential part of WCAG 2.1 AA compliance. This lesson covers the techniques, tools, and test patterns needed to verify accessible implementations in production React applications.
Testing building an accessibility testing pipeline requires a systematic approach combining automated tools (axe-core, Lighthouse) with manual verification (keyboard testing, screen reader review).
1import { render, screen } from '@testing-library/react';
2import { axe, toHaveNoViolations } from 'jest-axe';
3
4expect.extend(toHaveNoViolations);
5
6describe('Building an Accessibility Testing Pipeline', () => {
7 it('meets WCAG 2.1 AA requirements', async () => {
8 const { container } = render(<YourComponent />);
9 const results = await axe(container, {
10 runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] },
11 });
12 expect(results).toHaveNoViolations();
13 });
14
15 it('has proper semantic structure', () => {
16 render(<YourComponent />);
17 // Verify landmarks, headings, and semantic elements
18 expect(screen.getByRole('main')).toBeInTheDocument();
19 expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
20 });
21});Key implementation patterns for building an accessibility testing pipeline:
1// Accessible component pattern
2function AccessibleComponent({ label, error }: Props) {
3 const inputId = useId();
4 const errorId = useId();
5
6 return (
7 <div>
8 <label htmlFor={inputId}>{label}</label>
9 <input
10 id={inputId}
11 aria-describedby={error ? errorId : undefined}
12 aria-invalid={error ? 'true' : undefined}
13 />
14 {error && (
15 <span id={errorId} role="alert">
16 {error}
17 </span>
18 )}
19 </div>
20 );
21}End-to-end accessibility tests with Playwright:
1import { test, expect } from '@playwright/test';
2import AxeBuilder from '@axe-core/playwright';
3
4test('building an accessibility testing pipeline meets WCAG AA', async ({ page }) => {
5 await page.goto('/your-page');
6 await page.waitForLoadState('networkidle');
7
8 const results = await new AxeBuilder({ page })
9 .withTags(['wcag2a', 'wcag2aa'])
10 .analyze();
11
12 const critical = results.violations.filter(
13 v => v.impact === 'critical' || v.impact === 'serious'
14 );
15
16 if (critical.length > 0) {
17 critical.forEach(v => console.error('[' + v.impact + '] ' + v.description));
18 }
19
20 expect(critical).toHaveLength(0);
21});Integrate building an accessibility testing pipeline into CI:
1# .github/workflows/a11y.yml
2name: Accessibility Tests
3on: [push, pull_request]
4jobs:
5 a11y:
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v4
9 - uses: actions/setup-node@v4
10 with: { node-version: '20', cache: 'npm' }
11 - run: npm ci
12 - run: npm run test:a11y
13 - uses: actions/upload-artifact@v4
14 if: always()
15 with:
16 name: a11y-results
17 path: test-results/Most common building an accessibility testing pipeline violations and their fixes:
1// Violation: Missing form label
2// BAD:
3<input type="email" placeholder="Email" />
4
5// GOOD:
6<label htmlFor="email">Email address</label>
7<input id="email" type="email" aria-required="true" />
8
9// Violation: Non-descriptive link text
10// BAD:
11<a href="/courses/1">Read more</a>
12
13// GOOD:
14<a href="/courses/typescript-mastery">
15 View TypeScript Mastery course
16</a>
17
18// Violation: Color as only differentiator
19// BAD:
20<span style={{ color: 'red' }}>Error</span>
21
22// GOOD:
23<span style={{ color: 'red' }} role="alert">
24 <svg aria-hidden="true"><path /></svg>
25 Error: Invalid email format
26</span>A working test implementation covering automated and manual accessibility testing patterns.
1// tests/a11y/accessibility-best-practices.spec.ts
2import { test, expect } from '@playwright/test';
3import AxeBuilder from '@axe-core/playwright';
4
5test.describe('Building an Accessibility Testing Pipeline', () => {
6 test('no critical axe violations', async ({ page }) => {
7 await page.goto('/');
8 await page.waitForLoadState('networkidle');
9
10 const results = await new AxeBuilder({ page })
11 .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
12 .analyze();
13
14 const critical = results.violations.filter(
15 v => v.impact === 'critical' || v.impact === 'serious'
16 );
17 expect(critical, 'No critical/serious a11y violations').toHaveLength(0);
18 });
19
20 test('keyboard navigation works', async ({ page }) => {
21 await page.goto('/');
22 // First Tab stop should be skip link
23 await page.keyboard.press('Tab');
24 const isSkipLink = await page.evaluate(() =>
25 document.activeElement?.textContent?.toLowerCase().includes('skip') ||
26 (document.activeElement as HTMLAnchorElement)?.href?.includes('#main')
27 );
28 expect(isSkipLink).toBe(true);
29 });
30
31 test('all images have alt text', async ({ page }) => {
32 await page.goto('/courses');
33 const missing = await page.evaluate(() =>
34 Array.from(document.querySelectorAll('img:not([aria-hidden])'))
35 .filter(img => !(img as HTMLImageElement).alt)
36 .map(img => (img as HTMLImageElement).src)
37 );
38 expect(missing, 'All images must have alt text').toHaveLength(0);
39 });
40
41 test('color contrast meets WCAG AA (4.5:1)', async ({ page }) => {
42 await page.goto('/');
43 const results = await new AxeBuilder({ page })
44 .withRules(['color-contrast'])
45 .analyze();
46 expect(results.violations).toHaveLength(0);
47 });
48});This test suite covers the four key building an accessibility testing pipeline areas: automated axe-core scanning for structural violations, keyboard navigation verification, image alt text completeness, and color contrast checking. The axe-core approach catches 30-40% of WCAG issues automatically; the remaining violations require the manual keyboard testing and screen reader verification covered in subsequent lessons.
Continue with the next accessibility topic to build a comprehensive WCAG 2.1 AA test suite.
Your progress is saved in this browser, on this device only.