a11y-playwright-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePlaywright 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
前置条件
| Requirement | Details |
|---|---|
| Node.js | v18+ recommended |
| Playwright | |
| axe-core | |
| TypeScript | Configured in project |
| 要求 | 详情 |
|---|---|
| Node.js | 推荐v18+版本 |
| Playwright | 已安装 |
| axe-core | 已安装 |
| TypeScript | 项目中已配置 |
Quick Setup
快速设置
bash
undefinedbash
undefinedAdd axe-core to existing Playwright project
为现有Playwright项目添加axe-core
npm install -D @axe-core/playwright axe-core
undefinednpm install -D @axe-core/playwright axe-core
undefinedFirst Questions to Ask
测试前需明确的问题
Before writing accessibility tests, clarify:
- Scope: Which pages/flows are in scope? What's explicitly excluded?
- Standard: WCAG 2.1 AA (default) or specific organizational policy?
- Priority: Which components are highest risk (forms, modals, navigation, checkout)?
- Exceptions: Known constraints (legacy markup, third-party widgets)?
- Assistive Tech: Which screen readers/browsers need manual testing?
编写可访问性测试前,请先明确以下内容:
- 范围:哪些页面/流程在测试范围内?哪些明确排除?
- 标准:采用WCAG 2.1 AA(默认)还是特定组织政策?
- 优先级:哪些组件风险最高(表单、弹窗、导航、结账流程)?
- 例外情况:已知的限制(遗留标记、第三方组件)?
- 辅助技术:哪些屏幕阅读器/浏览器需要手动测试?
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 Success | Accessibility Signal |
|---|---|
| Button has accessible name |
| Input properly labeled |
| Landmark exists |
| May lack accessible name |
如果无法通过角色或标签定位元素,这通常是一个可访问性缺陷。
| 定位成功情况 | 可访问性信号 |
|---|---|
| 按钮拥有可访问名称 |
| 输入框已正确设置标签 |
| 地标已存在 |
| 可能缺少可访问名称 |
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原则参考
| Principle | Focus Areas | Example Tests |
|---|---|---|
| Perceivable | Alt text, captions, contrast, structure | Image alternatives, color contrast ratio |
| Operable | Keyboard, focus, timing, navigation | Tab order, focus visibility, skip links |
| Understandable | Labels, instructions, errors, consistency | Form labels, error messages, predictable behavior |
| Robust | Valid HTML, ARIA, name/role/value | Semantic structure, accessible names |
| 原则 | 关注领域 | 测试示例 |
|---|---|---|
| 可感知(Perceivable) | 替代文本、字幕、对比度、结构 | 图片替代文本、颜色对比度比例 |
| 可操作(Operable) | 键盘、焦点、时间限制、导航 | Tab顺序、焦点可见性、跳转链接 |
| 可理解(Understandable) | 标签、说明、错误提示、一致性 | 表单标签、错误信息、可预测行为 |
| 健壮性(Robust) | 有效的HTML、ARIA、名称/角色/值 | 语义结构、可访问名称 |
Axe-Core Tags Reference
Axe-Core标签参考
| Tag | WCAG Level | Use Case |
|---|---|---|
| Level A | Minimum compliance |
| Level AA | Standard target |
| Level AAA | Enhanced (rarely full) |
| 2.1 Level A | WCAG 2.1 specific A |
| 2.1 Level AA | WCAG 2.1 standard |
| Beyond WCAG | Additional recommendations |
| 标签 | WCAG级别 | 使用场景 |
|---|---|---|
| A级 | 最低合规要求 |
| AA级 | 标准目标 |
| AAA级 | 增强级(很少完全达标) |
| 2.1 A级 | WCAG 2.1特定A级要求 |
| 2.1 AA级 | WCAG 2.1标准 |
| 超出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:
- Scope narrowly - specific component/route only
- Document impact - which WCAG criterion, user impact
- Set expiration - owner + remediation date
- 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();当无法避免例外情况时:
- 缩小范围 - 仅针对特定组件/路由
- 记录影响 - 涉及的WCAG准则、对用户的影响
- 设置截止日期 - 负责人+修复日期
- 跟踪工单 - 关联修复问题的链接
typescript
// ❌ 避免:全局禁用规则
new AxeBuilder({ page }).disableRules(['color-contrast']);
// ✅ 推荐:带文档的范围化排除
new AxeBuilder({ page })
.exclude('#third-party-widget') // 已知问题:JIRA-1234,Q2前修复
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();Troubleshooting
故障排除
| Problem | Cause | Solution |
|---|---|---|
| Axe finds 0 violations but app fails manual audit | Automation covers ~30-40% | Add manual testing checklist |
| False positive on dynamic content | Content not fully rendered | Wait for stable state before scan |
| Color contrast fails incorrectly | Background image/gradient | Use |
| Cannot find element by role | Missing semantic HTML | Fix markup - this is a real bug |
| Focus not visible | Missing | Add visible focus indicator CSS |
| Dialog focus not trapped | Missing focus trap logic | Implement focus trap (see snippets) |
| Skip link doesn't work | Target missing | Add tabindex to main content |
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Axe未检测到违规,但手动审核发现问题 | 自动化仅覆盖约30-40% | 添加手动测试清单 |
| 动态内容出现误报 | 内容未完全渲染 | 扫描前等待状态稳定 |
| 颜色对比度检测错误失败 | 背景为图片/渐变 | 对已知误报使用 |
| 无法通过角色定位元素 | 缺少语义化HTML | 修复标记 - 这是真实缺陷 |
| 焦点不可见 | 缺少 | 添加可见的焦点指示器CSS |
| 弹窗未捕获焦点 | 缺少焦点捕获逻辑 | 实现焦点捕获(参考代码片段) |
| 跳转链接无效 | 目标元素缺少 | 为主要内容添加tabindex |
CLI Quick Reference
CLI快速参考
| Command | Description |
|---|---|
| Run accessibility tests only |
| Run with visible browser for debugging |
| Step through with Inspector |
| Debug mode with pause |
| 命令 | 描述 |
|---|---|
| 仅运行可访问性测试 |
| 以可见浏览器模式运行测试(用于调试) |
| 使用调试器逐步执行 |
| 带暂停的调试模式 |
References
参考资料
| Document | Content |
|---|---|
| Snippets | axe-core setup, helpers, keyboard/focus patterns |
| WCAG 2.1 AA Checklist | Manual audit checklist by POUR principle |
| ARIA Patterns | Common ARIA widget patterns and validations |
| 文档 | 内容 |
|---|---|
| Snippets | axe-core设置、辅助工具、键盘/焦点模式 |
| WCAG 2.1 AA Checklist | 按POUR原则分类的手动审核清单 |
| ARIA Patterns | 常见ARIA组件模式及验证方法 |
External Resources
外部资源
| Resource | URL |
|---|---|
| WCAG 2.1 Specification | https://www.w3.org/TR/WCAG21/ |
| WCAG Quick Reference | https://www.w3.org/WAI/WCAG21/quickref/ |
| WAI-ARIA Authoring Practices | https://www.w3.org/WAI/ARIA/apg/ |
| axe-core Rules | https://dequeuniversity.com/rules/axe/ |
| 资源 | 链接 |
|---|---|
| WCAG 2.1 规范 | https://www.w3.org/TR/WCAG21/ |
| WCAG 快速参考 | https://www.w3.org/WAI/WCAG21/quickref/ |
| WAI-ARIA 作者实践指南 | https://www.w3.org/WAI/ARIA/apg/ |
| axe-core 规则 | https://dequeuniversity.com/rules/axe/ |