accessibility-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Accessibility Testing

无障碍测试

Overview

概述

Accessibility testing ensures web applications are usable by people with disabilities, including those using screen readers, keyboard navigation, or other assistive technologies. It validates compliance with WCAG (Web Content Accessibility Guidelines) and identifies barriers to accessibility.
无障碍测试确保Web应用可供残障人士使用,包括使用屏幕阅读器、键盘导航或其他辅助技术的用户。它验证是否符合WCAG(Web内容无障碍指南),并识别无障碍访问障碍。

When to Use

适用场景

  • Validating WCAG 2.1/2.2 compliance
  • Testing keyboard navigation
  • Verifying screen reader compatibility
  • Testing color contrast ratios
  • Validating ARIA attributes
  • Testing form accessibility
  • Ensuring focus management
  • Testing with assistive technologies
  • 验证WCAG 2.1/2.2合规性
  • 测试键盘导航
  • 验证屏幕阅读器兼容性
  • 测试色彩对比度
  • 验证ARIA属性
  • 测试表单无障碍性
  • 确保焦点管理
  • 使用辅助技术测试

WCAG Levels

WCAG 等级

  • Level A: Basic accessibility (must have)
  • Level AA: Intermediate accessibility (should have, legal requirement in many jurisdictions)
  • Level AAA: Advanced accessibility (nice to have)
  • Level A:基础无障碍要求(必须满足)
  • Level AA:中级无障碍要求(建议满足,在许多地区属于法律强制要求)
  • Level AAA:高级无障碍要求(锦上添花)

Instructions

操作指南

1. axe-core with Playwright

1. 结合Playwright使用axe-core

typescript
// tests/accessibility/homepage.a11y.test.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Homepage Accessibility', () => {
  test('should not have any automatically detectable WCAG A or AA violations', async ({
    page,
  }) => {
    await page.goto('/');

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

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

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

    const results = await new AxeBuilder({ page })
      .include('nav')
      .analyze();

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

  test('form should be accessible', async ({ page }) => {
    await page.goto('/contact');

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

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

    // Additional form checks
    const inputs = await page.locator('input, select, textarea').all();
    for (const input of inputs) {
      const id = await input.getAttribute('id');
      const ariaLabel = await input.getAttribute('aria-label');
      const ariaLabelledBy = await input.getAttribute('aria-labelledby');

      // Every input should have an associated label
      if (id) {
        const label = await page.locator(`label[for="${id}"]`).count();
        expect(
          label > 0 || ariaLabel || ariaLabelledBy,
          `Input with id="${id}" has no associated label`
        ).toBeTruthy();
      }
    }
  });

  test('images should have alt text', async ({ page }) => {
    await page.goto('/');

    const images = await page.locator('img').all();

    for (const img of images) {
      const alt = await img.getAttribute('alt');
      const role = await img.getAttribute('role');
      const ariaLabel = await img.getAttribute('aria-label');

      // Decorative images should have empty alt or role="presentation"
      // Content images must have descriptive alt text
      expect(
        alt !== null || role === 'presentation' || ariaLabel,
        'Image missing alt text'
      ).toBeTruthy();
    }
  });

  test('color contrast should meet AA standards', async ({ page }) => {
    await page.goto('/');

    const results = await new AxeBuilder({ page })
      .withTags(['cat.color'])
      .analyze();

    expect(results.violations).toEqual([]);
  });
});
typescript
// tests/accessibility/homepage.a11y.test.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Homepage Accessibility', () => {
  test('should not have any automatically detectable WCAG A or AA violations', async ({
    page,
  }) => {
    await page.goto('/');

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

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

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

    const results = await new AxeBuilder({ page })
      .include('nav')
      .analyze();

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

  test('form should be accessible', async ({ page }) => {
    await page.goto('/contact');

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

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

    // Additional form checks
    const inputs = await page.locator('input, select, textarea').all();
    for (const input of inputs) {
      const id = await input.getAttribute('id');
      const ariaLabel = await input.getAttribute('aria-label');
      const ariaLabelledBy = await input.getAttribute('aria-labelledby');

      // Every input should have an associated label
      if (id) {
        const label = await page.locator(`label[for="${id}"]`).count();
        expect(
          label > 0 || ariaLabel || ariaLabelledBy,
          `Input with id="${id}" has no associated label`
        ).toBeTruthy();
      }
    }
  });

  test('images should have alt text', async ({ page }) => {
    await page.goto('/');

    const images = await page.locator('img').all();

    for (const img of images) {
      const alt = await img.getAttribute('alt');
      const role = await img.getAttribute('role');
      const ariaLabel = await img.getAttribute('aria-label');

      // Decorative images should have empty alt or role="presentation"
      // Content images must have descriptive alt text
      expect(
        alt !== null || role === 'presentation' || ariaLabel,
        'Image missing alt text'
      ).toBeTruthy();
    }
  });

  test('color contrast should meet AA standards', async ({ page }) => {
    await page.goto('/');

    const results = await new AxeBuilder({ page })
      .withTags(['cat.color'])
      .analyze();

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

2. Keyboard Navigation Testing

2. 键盘导航测试

typescript
// tests/accessibility/keyboard-navigation.test.ts
import { test, expect } from '@playwright/test';

test.describe('Keyboard Navigation', () => {
  test('should navigate through focusable elements with Tab', async ({
    page,
  }) => {
    await page.goto('/');

    // Get all focusable elements
    const focusableSelectors =
      'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';

    const focusableElements = await page.locator(focusableSelectors).all();

    // Tab through all elements
    for (let i = 0; i < focusableElements.length; i++) {
      await page.keyboard.press('Tab');

      const focusedElement = await page.evaluate(() => {
        return {
          tagName: document.activeElement?.tagName,
          id: document.activeElement?.id,
          className: document.activeElement?.className,
        };
      });

      expect(focusedElement.tagName).toBeTruthy();
    }
  });

  test('should skip navigation with skip link', async ({ page }) => {
    await page.goto('/');

    // Tab to skip link (usually first focusable element)
    await page.keyboard.press('Tab');

    const skipLink = await page.locator('.skip-link');
    await expect(skipLink).toBeFocused();

    // Activate skip link
    await page.keyboard.press('Enter');

    // Focus should be on main content
    const focusedElement = await page.evaluate(() => {
      return document.activeElement?.id;
    });

    expect(focusedElement).toBe('main-content');
  });

  test('modal should trap focus', async ({ page }) => {
    await page.goto('/');

    // Open modal
    await page.click('[data-testid="open-modal"]');
    await page.waitForSelector('[role="dialog"]');

    const modal = page.locator('[role="dialog"]');
    const focusableInModal = modal.locator(
      'a[href], button, input, select, textarea'
    );

    const count = await focusableInModal.count();

    // Tab through all elements in modal
    for (let i = 0; i < count + 2; i++) {
      await page.keyboard.press('Tab');
    }

    // Focus should still be within modal
    const focusedElement = await page.evaluate(() => {
      const modal = document.querySelector('[role="dialog"]');
      return modal?.contains(document.activeElement);
    });

    expect(focusedElement).toBe(true);
  });

  test('dropdown menu should be keyboard accessible', async ({ page }) => {
    await page.goto('/');

    // Navigate to dropdown trigger
    await page.keyboard.press('Tab');
    const dropdown = page.locator('[data-testid="dropdown-menu"]');
    await dropdown.focus();

    // Open dropdown with Enter
    await page.keyboard.press('Enter');

    // Menu should be visible
    const menu = page.locator('[role="menu"]');
    await expect(menu).toBeVisible();

    // Navigate menu items with arrow keys
    await page.keyboard.press('ArrowDown');
    const firstItem = menu.locator('[role="menuitem"]').first();
    await expect(firstItem).toBeFocused();

    await page.keyboard.press('ArrowDown');
    const secondItem = menu.locator('[role="menuitem"]').nth(1);
    await expect(secondItem).toBeFocused();

    // Escape should close menu
    await page.keyboard.press('Escape');
    await expect(menu).not.toBeVisible();
    await expect(dropdown).toBeFocused();
  });

  test('form can be completed using keyboard only', async ({ page }) => {
    await page.goto('/contact');

    // Tab to first field
    await page.keyboard.press('Tab');
    await page.keyboard.type('John Doe');

    // Tab to email field
    await page.keyboard.press('Tab');
    await page.keyboard.type('john@example.com');

    // Tab to message
    await page.keyboard.press('Tab');
    await page.keyboard.type('Test message');

    // Tab to submit and activate
    await page.keyboard.press('Tab');
    await page.keyboard.press('Enter');

    // Check form was submitted
    await expect(page.locator('.success-message')).toBeVisible();
  });
});
typescript
// tests/accessibility/keyboard-navigation.test.ts
import { test, expect } from '@playwright/test';

test.describe('Keyboard Navigation', () => {
  test('should navigate through focusable elements with Tab', async ({
    page,
  }) => {
    await page.goto('/');

    // Get all focusable elements
    const focusableSelectors =
      'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';

    const focusableElements = await page.locator(focusableSelectors).all();

    // Tab through all elements
    for (let i = 0; i < focusableElements.length; i++) {
      await page.keyboard.press('Tab');

      const focusedElement = await page.evaluate(() => {
        return {
          tagName: document.activeElement?.tagName,
          id: document.activeElement?.id,
          className: document.activeElement?.className,
        };
      });

      expect(focusedElement.tagName).toBeTruthy();
    }
  });

  test('should skip navigation with skip link', async ({ page }) => {
    await page.goto('/');

    // Tab to skip link (usually first focusable element)
    await page.keyboard.press('Tab');

    const skipLink = await page.locator('.skip-link');
    await expect(skipLink).toBeFocused();

    // Activate skip link
    await page.keyboard.press('Enter');

    // Focus should be on main content
    const focusedElement = await page.evaluate(() => {
      return document.activeElement?.id;
    });

    expect(focusedElement).toBe('main-content');
  });

  test('modal should trap focus', async ({ page }) => {
    await page.goto('/');

    // Open modal
    await page.click('[data-testid="open-modal"]');
    await page.waitForSelector('[role="dialog"]');

    const modal = page.locator('[role="dialog"]');
    const focusableInModal = modal.locator(
      'a[href], button, input, select, textarea'
    );

    const count = await focusableInModal.count();

    // Tab through all elements in modal
    for (let i = 0; i < count + 2; i++) {
      await page.keyboard.press('Tab');
    }

    // Focus should still be within modal
    const focusedElement = await page.evaluate(() => {
      const modal = document.querySelector('[role="dialog"]');
      return modal?.contains(document.activeElement);
    });

    expect(focusedElement).toBe(true);
  });

  test('dropdown menu should be keyboard accessible', async ({ page }) => {
    await page.goto('/');

    // Navigate to dropdown trigger
    await page.keyboard.press('Tab');
    const dropdown = page.locator('[data-testid="dropdown-menu"]');
    await dropdown.focus();

    // Open dropdown with Enter
    await page.keyboard.press('Enter');

    // Menu should be visible
    const menu = page.locator('[role="menu"]');
    await expect(menu).toBeVisible();

    // Navigate menu items with arrow keys
    await page.keyboard.press('ArrowDown');
    const firstItem = menu.locator('[role="menuitem"]').first();
    await expect(firstItem).toBeFocused();

    await page.keyboard.press('ArrowDown');
    const secondItem = menu.locator('[role="menuitem"]').nth(1);
    await expect(secondItem).toBeFocused();

    // Escape should close menu
    await page.keyboard.press('Escape');
    await expect(menu).not.toBeVisible();
    await expect(dropdown).toBeFocused();
  });

  test('form can be completed using keyboard only', async ({ page }) => {
    await page.goto('/contact');

    // Tab to first field
    await page.keyboard.press('Tab');
    await page.keyboard.type('John Doe');

    // Tab to email field
    await page.keyboard.press('Tab');
    await page.keyboard.type('john@example.com');

    // Tab to message
    await page.keyboard.press('Tab');
    await page.keyboard.type('Test message');

    // Tab to submit and activate
    await page.keyboard.press('Tab');
    await page.keyboard.press('Enter');

    // Check form was submitted
    await expect(page.locator('.success-message')).toBeVisible();
  });
});

3. ARIA Testing

3. ARIA测试

typescript
// tests/accessibility/aria.test.ts
import { test, expect } from '@playwright/test';

test.describe('ARIA Attributes', () => {
  test('buttons should have accessible names', async ({ page }) => {
    await page.goto('/');

    const buttons = await page.locator('button').all();

    for (const button of buttons) {
      const text = await button.textContent();
      const ariaLabel = await button.getAttribute('aria-label');
      const ariaLabelledBy = await button.getAttribute('aria-labelledby');

      expect(
        text?.trim() || ariaLabel || ariaLabelledBy,
        'Button has no accessible name'
      ).toBeTruthy();
    }
  });

  test('icons should have aria-hidden or labels', async ({ page }) => {
    await page.goto('/');

    const icons = await page
      .locator('[class*="icon"], svg[class*="icon"]')
      .all();

    for (const icon of icons) {
      const ariaHidden = await icon.getAttribute('aria-hidden');
      const ariaLabel = await icon.getAttribute('aria-label');
      const title = await icon.locator('title').count();

      // Icon should be hidden from screen readers OR have a label
      expect(
        ariaHidden === 'true' || ariaLabel || title > 0,
        'Icon without aria-hidden or accessible name'
      ).toBeTruthy();
    }
  });

  test('custom widgets should have correct roles', async ({ page }) => {
    await page.goto('/components');

    // Tab widget
    const tablist = page.locator('[role="tablist"]');
    await expect(tablist).toHaveCount(1);

    const tabs = tablist.locator('[role="tab"]');
    const tabpanels = page.locator('[role="tabpanel"]');

    expect(await tabs.count()).toBeGreaterThan(0);
    expect(await tabs.count()).toBe(await tabpanels.count());

    // Check aria-selected
    const selectedTab = tabs.locator('[aria-selected="true"]');
    await expect(selectedTab).toHaveCount(1);

    // Check tab associations
    const firstTab = tabs.first();
    const ariaControls = await firstTab.getAttribute('aria-controls');
    const associatedPanel = page.locator(`[id="${ariaControls}"]`);
    await expect(associatedPanel).toHaveCount(1);
  });

  test('live regions announce changes', async ({ page }) => {
    await page.goto('/');

    // Find live region
    const liveRegion = page.locator('[role="status"], [aria-live]');

    // Trigger update
    await page.click('[data-testid="load-data"]');

    // Wait for content
    await liveRegion.waitFor({ state: 'visible' });

    const ariaLive = await liveRegion.getAttribute('aria-live');
    expect(['polite', 'assertive']).toContain(ariaLive);
  });
});
typescript
// tests/accessibility/aria.test.ts
import { test, expect } from '@playwright/test';

test.describe('ARIA Attributes', () => {
  test('buttons should have accessible names', async ({ page }) => {
    await page.goto('/');

    const buttons = await page.locator('button').all();

    for (const button of buttons) {
      const text = await button.textContent();
      const ariaLabel = await button.getAttribute('aria-label');
      const ariaLabelledBy = await button.getAttribute('aria-labelledby');

      expect(
        text?.trim() || ariaLabel || ariaLabelledBy,
        'Button has no accessible name'
      ).toBeTruthy();
    }
  });

  test('icons should have aria-hidden or labels', async ({ page }) => {
    await page.goto('/');

    const icons = await page
      .locator('[class*="icon"], svg[class*="icon"]')
      .all();

    for (const icon of icons) {
      const ariaHidden = await icon.getAttribute('aria-hidden');
      const ariaLabel = await icon.getAttribute('aria-label');
      const title = await icon.locator('title').count();

      // Icon should be hidden from screen readers OR have a label
      expect(
        ariaHidden === 'true' || ariaLabel || title > 0,
        'Icon without aria-hidden or accessible name'
      ).toBeTruthy();
    }
  });

  test('custom widgets should have correct roles', async ({ page }) => {
    await page.goto('/components');

    // Tab widget
    const tablist = page.locator('[role="tablist"]');
    await expect(tablist).toHaveCount(1);

    const tabs = tablist.locator('[role="tab"]');
    const tabpanels = page.locator('[role="tabpanel"]');

    expect(await tabs.count()).toBeGreaterThan(0);
    expect(await tabs.count()).toBe(await tabpanels.count());

    // Check aria-selected
    const selectedTab = tabs.locator('[aria-selected="true"]');
    await expect(selectedTab).toHaveCount(1);

    // Check tab associations
    const firstTab = tabs.first();
    const ariaControls = await firstTab.getAttribute('aria-controls');
    const associatedPanel = page.locator(`[id="${ariaControls}"]`);
    await expect(associatedPanel).toHaveCount(1);
  });

  test('live regions announce changes', async ({ page }) => {
    await page.goto('/');

    // Find live region
    const liveRegion = page.locator('[role="status"], [aria-live]');

    // Trigger update
    await page.click('[data-testid="load-data"]');

    // Wait for content
    await liveRegion.waitFor({ state: 'visible' });

    const ariaLive = await liveRegion.getAttribute('aria-live');
    expect(['polite', 'assertive']).toContain(ariaLive);
  });
});

4. Jest with jest-axe

4. 结合Jest使用jest-axe

typescript
// tests/components/Button.a11y.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { Button } from '../Button';

expect.extend(toHaveNoViolations);

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

  test('icon button should have aria-label', async () => {
    const { container } = render(
      <Button aria-label="Close">
        <CloseIcon />
      </Button>
    );
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  test('disabled button should be accessible', async () => {
    const { container } = render(<Button disabled>Disabled</Button>);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});
typescript
// tests/components/Button.a11y.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { Button } from '../Button';

expect.extend(toHaveNoViolations);

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

  test('icon button should have aria-label', async () => {
    const { container } = render(
      <Button aria-label="Close">
        <CloseIcon />
      </Button>
    );
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  test('disabled button should be accessible', async () => {
    const { container } = render(<Button disabled>Disabled</Button>);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

5. Cypress Accessibility Testing

5. Cypress无障碍测试

javascript
// cypress/e2e/accessibility.cy.js
describe('Accessibility Tests', () => {
  beforeEach(() => {
    cy.visit('/');
    cy.injectAxe();
  });

  it('has no detectable a11y violations on load', () => {
    cy.checkA11y();
  });

  it('navigation is accessible', () => {
    cy.checkA11y('nav');
  });

  it('focuses on first error when form submission fails', () => {
    cy.get('form').within(() => {
      cy.get('[type="submit"]').click();
    });

    cy.focused().should('have.attr', 'aria-invalid', 'true');
  });

  it('modal has correct focus management', () => {
    cy.get('[data-cy="open-modal"]').click();

    // Focus should be in modal
    cy.get('[role="dialog"]').should('exist');
    cy.focused().parents('[role="dialog"]').should('exist');

    // Close modal with Escape
    cy.get('body').type('{esc}');
    cy.get('[role="dialog"]').should('not.exist');

    // Focus returns to trigger
    cy.get('[data-cy="open-modal"]').should('have.focus');
  });
});
javascript
// cypress/e2e/accessibility.cy.js
describe('Accessibility Tests', () => {
  beforeEach(() => {
    cy.visit('/');
    cy.injectAxe();
  });

  it('has no detectable a11y violations on load', () => {
    cy.checkA11y();
  });

  it('navigation is accessible', () => {
    cy.checkA11y('nav');
  });

  it('focuses on first error when form submission fails', () => {
    cy.get('form').within(() => {
      cy.get('[type="submit"]').click();
    });

    cy.focused().should('have.attr', 'aria-invalid', 'true');
  });

  it('modal has correct focus management', () => {
    cy.get('[data-cy="open-modal"]').click();

    // Focus should be in modal
    cy.get('[role="dialog"]').should('exist');
    cy.focused().parents('[role="dialog"]').should('exist');

    // Close modal with Escape
    cy.get('body').type('{esc}');
    cy.get('[role="dialog"]').should('not.exist');

    // Focus returns to trigger
    cy.get('[data-cy="open-modal"]').should('have.focus');
  });
});

6. Python with Selenium and axe

6. 结合Selenium和axe的Python测试

python
undefined
python
undefined

tests/test_accessibility.py

tests/test_accessibility.py

import pytest from selenium import webdriver from axe_selenium_python import Axe
class TestAccessibility: @pytest.fixture def driver(self): """Setup Chrome driver.""" options = webdriver.ChromeOptions() options.add_argument('--headless') driver = webdriver.Chrome(options=options) yield driver driver.quit()
def test_homepage_accessibility(self, driver):
    """Test homepage for WCAG violations."""
    driver.get('http://localhost:3000')

    axe = Axe(driver)
    axe.inject()

    # Run axe accessibility tests
    results = axe.run()

    # Assert no violations
    assert len(results['violations']) == 0, \
        axe.report(results['violations'])

def test_form_accessibility(self, driver):
    """Test form accessibility."""
    driver.get('http://localhost:3000/contact')

    axe = Axe(driver)
    axe.inject()

    # Run with specific tags
    results = axe.run(options={
        'runOnly': {
            'type': 'tag',
            'values': ['wcag2a', 'wcag2aa', 'wcag21aa']
        }
    })

    violations = results['violations']
    assert len(violations) == 0, \
        f"Found {len(violations)} accessibility violations"

def test_keyboard_navigation(self, driver):
    """Test keyboard navigation."""
    from selenium.webdriver.common.keys import Keys
    from selenium.webdriver.common.by import By

    driver.get('http://localhost:3000')

    body = driver.find_element(By.TAG_NAME, 'body')

    # Tab through focusable elements
    for _ in range(10):
        body.send_keys(Keys.TAB)

        active = driver.switch_to.active_element
        tag_name = active.tag_name

        # Focused element should be interactive
        assert tag_name in ['a', 'button', 'input', 'select', 'textarea'], \
            f"Unexpected focused element: {tag_name}"
undefined
import pytest from selenium import webdriver from axe_selenium_python import Axe
class TestAccessibility: @pytest.fixture def driver(self): """Setup Chrome driver.""" options = webdriver.ChromeOptions() options.add_argument('--headless') driver = webdriver.Chrome(options=options) yield driver driver.quit()
def test_homepage_accessibility(self, driver):
    """Test homepage for WCAG violations."""
    driver.get('http://localhost:3000')

    axe = Axe(driver)
    axe.inject()

    # Run axe accessibility tests
    results = axe.run()

    # Assert no violations
    assert len(results['violations']) == 0, \
        axe.report(results['violations'])

def test_form_accessibility(self, driver):
    """Test form accessibility."""
    driver.get('http://localhost:3000/contact')

    axe = Axe(driver)
    axe.inject()

    # Run with specific tags
    results = axe.run(options={
        'runOnly': {
            'type': 'tag',
            'values': ['wcag2a', 'wcag2aa', 'wcag21aa']
        }
    })

    violations = results['violations']
    assert len(violations) == 0, \
        f"Found {len(violations)} accessibility violations"

def test_keyboard_navigation(self, driver):
    """Test keyboard navigation."""
    from selenium.webdriver.common.keys import Keys
    from selenium.webdriver.common.by import By

    driver.get('http://localhost:3000')

    body = driver.find_element(By.TAG_NAME, 'body')

    # Tab through focusable elements
    for _ in range(10):
        body.send_keys(Keys.TAB)

        active = driver.switch_to.active_element
        tag_name = active.tag_name

        # Focused element should be interactive
        assert tag_name in ['a', 'button', 'input', 'select', 'textarea'], \
            f"Unexpected focused element: {tag_name}"
undefined

Manual Testing Checklist

手动检查清单

Keyboard Navigation

键盘导航

  • All interactive elements accessible via Tab
  • Shift+Tab navigates backwards
  • Enter/Space activates buttons/links
  • Arrow keys work in custom widgets
  • Escape closes modals/dropdowns
  • Skip links present and functional
  • Focus visible on all elements
  • No keyboard traps
  • 所有交互元素均可通过Tab键访问
  • Shift+Tab可反向导航
  • Enter/Space可激活按钮/链接
  • 自定义组件支持方向键操作
  • Escape可关闭模态框/下拉菜单
  • 存在跳转链接且功能正常
  • 所有元素的焦点可见
  • 无键盘陷阱

Screen Readers

屏幕阅读器

  • Page has descriptive title
  • Headings form logical hierarchy
  • Images have alt text
  • Form inputs have labels
  • Error messages announced
  • Dynamic content changes announced
  • Links have descriptive text
  • Custom widgets have ARIA roles
  • 页面有描述性标题
  • 标题形成逻辑层级
  • 图片包含替代文本
  • 表单输入项有标签
  • 错误消息可被朗读
  • 动态内容变化可被朗读
  • 链接有描述性文本
  • 自定义组件有ARIA角色

Visual

视觉体验

  • Color contrast meets AA (4.5:1 normal, 3:1 large text)
  • Information not conveyed by color alone
  • Text can be resized 200% without breaking
  • Focus indicators visible
  • Content readable in dark/light modes
  • 色彩对比度符合AA级标准(普通文本4.5:1,大文本3:1)
  • 信息不单独通过颜色传达
  • 文本可放大200%且布局不混乱
  • 焦点指示器可见
  • 内容在深色/浅色模式下均可读

Best Practices

最佳实践

✅ DO

✅ 推荐做法

  • Test with real assistive technologies
  • Include keyboard-only users
  • Test color contrast
  • Use semantic HTML
  • Provide text alternatives
  • Test with screen readers
  • Run automated tests in CI
  • Follow WCAG 2.1 AA standards
  • 使用真实辅助技术测试
  • 覆盖仅使用键盘的用户
  • 测试色彩对比度
  • 使用语义化HTML
  • 提供文本替代方案
  • 使用屏幕阅读器测试
  • 在CI流程中运行自动化测试
  • 遵循WCAG 2.1 AA标准

❌ DON'T

❌ 避免做法

  • Rely only on automated tests (they catch ~30-40% of issues)
  • Use color alone to convey information
  • Skip keyboard navigation testing
  • Forget focus management in dynamic content
  • Use div/span for interactive elements
  • Hide focusable content with display:none
  • Ignore ARIA best practices
  • Skip manual testing
  • 仅依赖自动化测试(仅能检测约30-40%的问题)
  • 单独使用颜色传达信息
  • 跳过键盘导航测试
  • 忽略动态内容的焦点管理
  • 使用div/span实现交互元素
  • 用display:none隐藏可聚焦内容
  • 忽略ARIA最佳实践
  • 跳过手动测试

Tools

工具

  • Automated Testing: axe-core, Pa11y, Lighthouse, WAVE
  • Browser Extensions: axe DevTools, WAVE, Accessibility Insights
  • Screen Readers: NVDA, JAWS, VoiceOver, TalkBack
  • Color Contrast: WebAIM Contrast Checker, Stark
  • Frameworks: jest-axe, cypress-axe, axe-playwright
  • 自动化测试:axe-core、Pa11y、Lighthouse、WAVE
  • 浏览器扩展:axe DevTools、WAVE、Accessibility Insights
  • 屏幕阅读器:NVDA、JAWS、VoiceOver、TalkBack
  • 色彩对比度:WebAIM Contrast Checker、Stark
  • 测试框架:jest-axe、cypress-axe、axe-playwright

Standards

标准

  • WCAG 2.1: Web Content Accessibility Guidelines
  • WCAG 2.2: Latest version (2023)
  • Section 508: US federal accessibility standard
  • EN 301 549: European accessibility standard
  • ADA: Americans with Disabilities Act
  • WCAG 2.1:Web内容无障碍指南
  • WCAG 2.2:最新版本(2023年发布)
  • Section 508:美国联邦无障碍标准
  • EN 301 549:欧洲无障碍标准
  • ADA:美国残疾人法案

Examples

相关示例

See also: e2e-testing-automation, visual-regression-testing, accessibility-compliance for comprehensive accessibility implementation.
更多无障碍实现内容,请参考:e2e-testing-automation、visual-regression-testing、accessibility-compliance。