a11y-playwright-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Playwright Accessibility Testing (TypeScript)

Playwright 可访问性测试(TypeScript)

Comprehensive toolkit for automated accessibility testing using Playwright with TypeScript and axe-core. Enables WCAG 2.1 Level AA compliance verification, keyboard operability testing, semantic validation, and accessibility regression prevention.
Activation: This skill is triggered when working with accessibility testing, WCAG compliance, axe-core scans, keyboard navigation tests, focus management, ARIA validation, or screen reader compatibility.
这是一套使用Playwright结合TypeScript和axe-core进行自动化可访问性测试的综合工具包,可用于验证WCAG 2.1 AA级合规性、键盘可操作性测试、语义验证以及预防可访问性回归问题。
触发场景: 当处理可访问性测试、WCAG合规性、axe-core扫描、键盘导航测试、焦点管理、ARIA验证或屏幕阅读器兼容性相关工作时,将激活此技能。

When to Use This Skill

适用场景

  • Automated a11y scans with axe-core for WCAG 2.1 AA compliance
  • Keyboard navigation tests for Tab/Enter/Space/Escape/Arrow key operability
  • Focus management validation for dialogs, menus, and dynamic content
  • Semantic structure assertions for landmarks, headings, and ARIA
  • Form accessibility testing for labels, errors, and instructions
  • Color contrast and visual accessibility verification
  • Screen reader compatibility testing patterns
  • 使用axe-core进行自动化可访问性扫描,验证WCAG 2.1 AA合规性
  • 键盘导航测试,验证Tab/Enter/Space/Escape/方向键的可操作性
  • 焦点管理验证,针对弹窗、菜单和动态内容
  • 语义结构断言,针对地标、标题和ARIA
  • 表单可访问性测试,针对标签、错误提示和说明文字
  • 颜色对比度及视觉可访问性验证
  • 屏幕阅读器兼容性测试模式

Prerequisites

前置条件

RequirementDetails
Node.jsv18+ recommended
Playwright
@playwright/test
installed
axe-core
@axe-core/playwright
package
TypeScriptConfigured in project
要求详情
Node.js推荐v18+版本
Playwright已安装
@playwright/test
axe-core已安装
@axe-core/playwright
TypeScript项目中已配置

Quick Setup

快速设置

bash
undefined
bash
undefined

Add axe-core to existing Playwright project

为现有Playwright项目添加axe-core

npm install -D @axe-core/playwright axe-core
undefined
npm install -D @axe-core/playwright axe-core
undefined

First Questions to Ask

测试前需明确的问题

Before writing accessibility tests, clarify:
  1. Scope: Which pages/flows are in scope? What's explicitly excluded?
  2. Standard: WCAG 2.1 AA (default) or specific organizational policy?
  3. Priority: Which components are highest risk (forms, modals, navigation, checkout)?
  4. Exceptions: Known constraints (legacy markup, third-party widgets)?
  5. Assistive Tech: Which screen readers/browsers need manual testing?

编写可访问性测试前,请先明确以下内容:
  1. 范围:哪些页面/流程在测试范围内?哪些明确排除?
  2. 标准:采用WCAG 2.1 AA(默认)还是特定组织政策?
  3. 优先级:哪些组件风险最高(表单、弹窗、导航、结账流程)?
  4. 例外情况:已知的限制(遗留标记、第三方组件)?
  5. 辅助技术:哪些屏幕阅读器/浏览器需要手动测试?

Core Principles

核心原则

1. Automation Limitations

1. 自动化的局限性

⚠️ Critical: Automated tooling can detect ~30-40% of accessibility issues. Use automation to prevent regressions and catch common failures; manual audits are required for full WCAG conformance.
⚠️ 关键提示:自动化工具仅能检测约30-40%的可访问性问题。使用自动化工具预防回归并捕捉常见问题;完整的WCAG合规性需要手动审核

2. Semantic HTML First

2. 优先使用语义化HTML

Prefer native HTML semantics over ARIA. Use ARIA only when native elements cannot achieve the required semantics.
typescript
// ✅ Semantic HTML - inherently accessible
await page.getByRole('button', { name: 'Submit' }).click();

// ❌ ARIA override - requires manual keyboard/focus handling
await page.locator('[role="button"]').click(); // Often a <div>
优先使用原生HTML语义而非ARIA。仅当原生元素无法实现所需语义时才使用ARIA。
typescript
// ✅ 语义化HTML - 天生具备可访问性
await page.getByRole('button', { name: 'Submit' }).click();

// ❌ ARIA覆盖 - 需要手动处理键盘/焦点
await page.locator('[role="button"]').click(); // 通常是<div>元素

3. Locator Strategy as A11y Signal

3. 定位策略作为可访问性信号

If you cannot locate an element by role or label, it's often an accessibility defect.
Locator SuccessAccessibility Signal
getByRole('button', { name: 'Submit' })
Button has accessible name
getByLabel('Email')
Input properly labeled
getByRole('navigation')
Landmark exists
locator('.submit-btn')
⚠️
May lack accessible name

如果无法通过角色或标签定位元素,这通常是一个可访问性缺陷。
定位成功情况可访问性信号
getByRole('button', { name: 'Submit' })
按钮拥有可访问名称
getByLabel('Email')
输入框已正确设置标签
getByRole('navigation')
地标已存在
locator('.submit-btn')
⚠️
可能缺少可访问名称

Key Workflows

核心工作流

Automated Axe Scan (WCAG 2.1 AA)

自动化Axe扫描(WCAG 2.1 AA)

typescript
import AxeBuilder from '@axe-core/playwright';
import { test, expect } from '@playwright/test';

test('page has no WCAG 2.1 AA violations', async ({ page }) => {
  await page.goto('/');
  
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze();
  
  expect(results.violations).toEqual([]);
});
typescript
import AxeBuilder from '@axe-core/playwright';
import { test, expect } from '@playwright/test';

test('页面无WCAG 2.1 AA违规问题', async ({ page }) => {
  await page.goto('/');
  
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze();
  
  expect(results.violations).toEqual([]);
});

Scoped Axe Scan (Component-Level)

范围化Axe扫描(组件级)

typescript
test('form component is accessible', async ({ page }) => {
  await page.goto('/contact');
  
  const results = await new AxeBuilder({ page })
    .include('#contact-form') // Scope to specific component
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze();
  
  expect(results.violations).toEqual([]);
});
typescript
test('表单组件具备可访问性', async ({ page }) => {
  await page.goto('/contact');
  
  const results = await new AxeBuilder({ page })
    .include('#contact-form') // 限定到特定组件
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze();
  
  expect(results.violations).toEqual([]);
});

Keyboard Navigation Test

键盘导航测试

typescript
test('form is keyboard navigable', async ({ page }) => {
  await page.goto('/login');
  
  // Tab to first field
  await page.keyboard.press('Tab');
  await expect(page.getByLabel('Email')).toBeFocused();
  
  // Tab to password
  await page.keyboard.press('Tab');
  await expect(page.getByLabel('Password')).toBeFocused();
  
  // Tab to submit button
  await page.keyboard.press('Tab');
  await expect(page.getByRole('button', { name: 'Sign in' })).toBeFocused();
  
  // Submit with Enter
  await page.keyboard.press('Enter');
  await expect(page).toHaveURL(/dashboard/);
});
typescript
test('表单支持键盘导航', async ({ page }) => {
  await page.goto('/login');
  
  // Tab切换到第一个输入框
  await page.keyboard.press('Tab');
  await expect(page.getByLabel('Email')).toBeFocused();
  
  // Tab切换到密码框
  await page.keyboard.press('Tab');
  await expect(page.getByLabel('Password')).toBeFocused();
  
  // Tab切换到提交按钮
  await page.keyboard.press('Tab');
  await expect(page.getByRole('button', { name: 'Sign in' })).toBeFocused();
  
  // 按Enter提交
  await page.keyboard.press('Enter');
  await expect(page).toHaveURL(/dashboard/);
});

Dialog Focus Management

弹窗焦点管理测试

typescript
test('dialog traps and returns focus', async ({ page }) => {
  await page.goto('/settings');
  const trigger = page.getByRole('button', { name: 'Delete account' });
  
  // Open dialog
  await trigger.click();
  const dialog = page.getByRole('dialog');
  await expect(dialog).toBeVisible();
  
  // Focus should be inside dialog
  await expect(dialog.getByRole('button', { name: 'Cancel' })).toBeFocused();
  
  // Tab should stay trapped in dialog
  await page.keyboard.press('Tab');
  await expect(dialog.getByRole('button', { name: 'Confirm' })).toBeFocused();
  await page.keyboard.press('Tab');
  await expect(dialog.getByRole('button', { name: 'Cancel' })).toBeFocused();
  
  // Escape closes and returns focus to trigger
  await page.keyboard.press('Escape');
  await expect(dialog).toBeHidden();
  await expect(trigger).toBeFocused();
});
typescript
test('弹窗可捕获并返回焦点', async ({ page }) => {
  await page.goto('/settings');
  const trigger = page.getByRole('button', { name: 'Delete account' });
  
  // 打开弹窗
  await trigger.click();
  const dialog = page.getByRole('dialog');
  await expect(dialog).toBeVisible();
  
  // 焦点应位于弹窗内
  await expect(dialog.getByRole('button', { name: 'Cancel' })).toBeFocused();
  
  // Tab切换应保持在弹窗内
  await page.keyboard.press('Tab');
  await expect(dialog.getByRole('button', { name: 'Confirm' })).toBeFocused();
  await page.keyboard.press('Tab');
  await expect(dialog.getByRole('button', { name: 'Cancel' })).toBeFocused();
  
  // 按Escape关闭弹窗并将焦点返回触发按钮
  await page.keyboard.press('Escape');
  await expect(dialog).toBeHidden();
  await expect(trigger).toBeFocused();
});

Skip Link Validation

跳转链接验证

typescript
test('skip link moves focus to main content', async ({ page }) => {
  await page.goto('/');
  
  // First Tab should focus skip link
  await page.keyboard.press('Tab');
  const skipLink = page.getByRole('link', { name: /skip to (main|content)/i });
  await expect(skipLink).toBeFocused();
  
  // Activating skip link moves focus to main
  await page.keyboard.press('Enter');
  await expect(page.locator('#main, [role="main"]').first()).toBeFocused();
});

typescript
test('跳转链接可将焦点移至主要内容', async ({ page }) => {
  await page.goto('/');
  
  // 第一次按Tab应聚焦到跳转链接
  await page.keyboard.press('Tab');
  const skipLink = page.getByRole('link', { name: /skip to (main|content)/i });
  await expect(skipLink).toBeFocused();
  
  // 激活跳转链接将焦点移至主要内容
  await page.keyboard.press('Enter');
  await expect(page.locator('#main, [role="main"]').first()).toBeFocused();
});

POUR Principles Reference

POUR原则参考

PrincipleFocus AreasExample Tests
PerceivableAlt text, captions, contrast, structureImage alternatives, color contrast ratio
OperableKeyboard, focus, timing, navigationTab order, focus visibility, skip links
UnderstandableLabels, instructions, errors, consistencyForm labels, error messages, predictable behavior
RobustValid HTML, ARIA, name/role/valueSemantic structure, accessible names

原则关注领域测试示例
可感知(Perceivable)替代文本、字幕、对比度、结构图片替代文本、颜色对比度比例
可操作(Operable)键盘、焦点、时间限制、导航Tab顺序、焦点可见性、跳转链接
可理解(Understandable)标签、说明、错误提示、一致性表单标签、错误信息、可预测行为
健壮性(Robust)有效的HTML、ARIA、名称/角色/值语义结构、可访问名称

Axe-Core Tags Reference

Axe-Core标签参考

TagWCAG LevelUse Case
wcag2a
Level AMinimum compliance
wcag2aa
Level AAStandard target
wcag2aaa
Level AAAEnhanced (rarely full)
wcag21a
2.1 Level AWCAG 2.1 specific A
wcag21aa
2.1 Level AAWCAG 2.1 standard
best-practice
Beyond WCAGAdditional recommendations
标签WCAG级别使用场景
wcag2a
A级最低合规要求
wcag2aa
AA级标准目标
wcag2aaa
AAA级增强级(很少完全达标)
wcag21a
2.1 A级WCAG 2.1特定A级要求
wcag21aa
2.1 AA级WCAG 2.1标准
best-practice
超出WCAG范围额外推荐规范

Default Tags (WCAG 2.1 AA)

默认标签(WCAG 2.1 AA)

typescript
const WCAG21AA_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];

typescript
const WCAG21AA_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];

Exception Handling

异常处理

When exceptions are unavoidable:
  1. Scope narrowly - specific component/route only
  2. Document impact - which WCAG criterion, user impact
  3. Set expiration - owner + remediation date
  4. Track ticket - link to remediation issue
typescript
// ❌ Avoid: Global rule disable
new AxeBuilder({ page }).disableRules(['color-contrast']);

// ✅ Better: Scoped exclusion with documentation
new AxeBuilder({ page })
  .exclude('#third-party-widget') // Known issue: JIRA-1234, fix by Q2
  .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
  .analyze();

当无法避免例外情况时:
  1. 缩小范围 - 仅针对特定组件/路由
  2. 记录影响 - 涉及的WCAG准则、对用户的影响
  3. 设置截止日期 - 负责人+修复日期
  4. 跟踪工单 - 关联修复问题的链接
typescript
// ❌ 避免:全局禁用规则
new AxeBuilder({ page }).disableRules(['color-contrast']);

// ✅ 推荐:带文档的范围化排除
new AxeBuilder({ page })
  .exclude('#third-party-widget') // 已知问题:JIRA-1234,Q2前修复
  .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
  .analyze();

Troubleshooting

故障排除

ProblemCauseSolution
Axe finds 0 violations but app fails manual auditAutomation covers ~30-40%Add manual testing checklist
False positive on dynamic contentContent not fully renderedWait for stable state before scan
Color contrast fails incorrectlyBackground image/gradientUse
exclude
for known false positives
Cannot find element by roleMissing semantic HTMLFix markup - this is a real bug
Focus not visibleMissing
:focus
styles
Add visible focus indicator CSS
Dialog focus not trappedMissing focus trap logicImplement focus trap (see snippets)
Skip link doesn't workTarget missing
tabindex="-1"
Add tabindex to main content

问题原因解决方案
Axe未检测到违规,但手动审核发现问题自动化仅覆盖约30-40%添加手动测试清单
动态内容出现误报内容未完全渲染扫描前等待状态稳定
颜色对比度检测错误失败背景为图片/渐变对已知误报使用
exclude
无法通过角色定位元素缺少语义化HTML修复标记 - 这是真实缺陷
焦点不可见缺少
:focus
样式
添加可见的焦点指示器CSS
弹窗未捕获焦点缺少焦点捕获逻辑实现焦点捕获(参考代码片段)
跳转链接无效目标元素缺少
tabindex="-1"
为主要内容添加tabindex

CLI Quick Reference

CLI快速参考

CommandDescription
npx playwright test --grep "a11y"
Run accessibility tests only
npx playwright test --headed
Run with visible browser for debugging
npx playwright test --debug
Step through with Inspector
PWDEBUG=1 npx playwright test
Debug mode with pause

命令描述
npx playwright test --grep "a11y"
仅运行可访问性测试
npx playwright test --headed
以可见浏览器模式运行测试(用于调试)
npx playwright test --debug
使用调试器逐步执行
PWDEBUG=1 npx playwright test
带暂停的调试模式

References

参考资料

DocumentContent
Snippetsaxe-core setup, helpers, keyboard/focus patterns
WCAG 2.1 AA ChecklistManual audit checklist by POUR principle
ARIA PatternsCommon ARIA widget patterns and validations
文档内容
Snippetsaxe-core设置、辅助工具、键盘/焦点模式
WCAG 2.1 AA Checklist按POUR原则分类的手动审核清单
ARIA Patterns常见ARIA组件模式及验证方法

External Resources

外部资源