chore: import zh skill screen-reader-testing
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- Skill 名称:`screen-reader-testing`
|
||||
- 中文类目:屏幕阅读器与设计指南
|
||||
- 上游仓库:`wshobson__agents`
|
||||
- 上游路径:`plugins/accessibility-compliance/skills/screen-reader-testing/SKILL.md`
|
||||
- 上游链接:https://github.com/wshobson/agents/blob/HEAD/plugins/accessibility-compliance/skills/screen-reader-testing/SKILL.md
|
||||
- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理
|
||||
- 原作者、版权和许可证信息以上游仓库为准
|
||||
@@ -0,0 +1,538 @@
|
||||
---
|
||||
name: screen-reader-testing
|
||||
description: 使用屏幕阅读器(包括 VoiceOver、NVDA 和 JAWS)测试 Web 应用程序。适用于验证屏幕阅读器兼容性、调试无障碍问题或确保辅助技术支持。
|
||||
---
|
||||
|
||||
# 屏幕阅读器测试
|
||||
|
||||
使用屏幕阅读器测试 Web 应用程序的实用指南,用于全面的无障碍验证。
|
||||
|
||||
## 何时使用本技能
|
||||
|
||||
- 验证屏幕阅读器兼容性
|
||||
- 测试 ARIA 实现
|
||||
- 调试辅助技术问题
|
||||
- 验证表单无障碍性
|
||||
- 测试动态内容播报
|
||||
- 确保导航无障碍性
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 1. 主流屏幕阅读器
|
||||
|
||||
| 屏幕阅读器 | 平台 | 浏览器 | 使用占比 |
|
||||
| -------------- | ---------- | ------------------- | -------- |
|
||||
| **VoiceOver** | macOS/iOS | Safari | ~15% |
|
||||
| **NVDA** | Windows | Firefox/Chrome | ~31% |
|
||||
| **JAWS** | Windows | Chrome/IE | ~40% |
|
||||
| **TalkBack** | Android | Chrome | ~10% |
|
||||
| **Narrator** | Windows | Edge | ~4% |
|
||||
|
||||
### 2. 测试优先级
|
||||
|
||||
```
|
||||
最低覆盖:
|
||||
1. NVDA + Firefox(Windows)
|
||||
2. VoiceOver + Safari(macOS)
|
||||
3. VoiceOver + Safari(iOS)
|
||||
|
||||
全面覆盖:
|
||||
+ JAWS + Chrome(Windows)
|
||||
+ TalkBack + Chrome(Android)
|
||||
+ Narrator + Edge(Windows)
|
||||
```
|
||||
|
||||
### 3. 屏幕阅读器模式
|
||||
|
||||
| 模式 | 用途 | 使用场景 |
|
||||
| ---------------------- | -------------- | ------------------ |
|
||||
| **浏览/虚拟** | 阅读内容 | 默认阅读 |
|
||||
| **焦点/表单** | 与控件交互 | 填写表单 |
|
||||
| **应用程序** | 自定义微件 | ARIA 应用程序 |
|
||||
|
||||
## VoiceOver(macOS)
|
||||
|
||||
### 设置
|
||||
|
||||
```
|
||||
启用:系统设置 → 辅助功能 → VoiceOver
|
||||
开关:Cmd + F5
|
||||
快速开关:三按 Touch ID
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
|
||||
```
|
||||
导航:
|
||||
VO = Ctrl + Option(VoiceOver 修饰键)
|
||||
|
||||
VO + 右箭头键 下一个元素
|
||||
VO + 左箭头键 上一个元素
|
||||
VO + Shift + 下箭头键 进入群组
|
||||
VO + Shift + 上箭头键 退出群组
|
||||
|
||||
阅读:
|
||||
VO + A 从光标处开始朗读所有内容
|
||||
Ctrl 停止朗读
|
||||
VO + B 朗读当前段落
|
||||
|
||||
交互:
|
||||
VO + Space 激活元素
|
||||
VO + Shift + M 打开菜单
|
||||
Tab 下一个可聚焦元素
|
||||
Shift + Tab 上一个可聚焦元素
|
||||
|
||||
转子(VO + U):
|
||||
按以下类别导航:标题、链接、表单、地标
|
||||
左/右箭头键 切换转子类别
|
||||
上/下箭头键 在类别内导航
|
||||
Enter 跳转到项目
|
||||
|
||||
网页专用:
|
||||
VO + Cmd + H 下一个标题
|
||||
VO + Cmd + J 下一个表单控件
|
||||
VO + Cmd + L 下一个链接
|
||||
VO + Cmd + T 下一个表格
|
||||
```
|
||||
|
||||
### 测试清单
|
||||
|
||||
```markdown
|
||||
## VoiceOver 测试清单
|
||||
|
||||
### 页面加载
|
||||
|
||||
- [ ] 页面标题已播报
|
||||
- [ ] 找到主要地标
|
||||
- [ ] 跳过链接可用
|
||||
|
||||
### 导航
|
||||
|
||||
- [ ] 所有标题可通过转子发现
|
||||
- [ ] 标题层级合理(H1 → H2 → H3)
|
||||
- [ ] 地标已正确标注
|
||||
- [ ] 跳过链接功能正常
|
||||
|
||||
### 链接与按钮
|
||||
|
||||
- [ ] 链接用途清晰
|
||||
- [ ] 按钮操作已描述
|
||||
- [ ] 新窗口/标签页已播报
|
||||
|
||||
### 表单
|
||||
|
||||
- [ ] 所有标签随输入框一并朗读
|
||||
- [ ] 必填字段已播报
|
||||
- [ ] 错误信息已朗读
|
||||
- [ ] 操作指引可用
|
||||
- [ ] 焦点移至错误处
|
||||
|
||||
### 动态内容
|
||||
|
||||
- [ ] 弹窗立即播报
|
||||
- [ ] 加载状态已传达
|
||||
- [ ] 内容更新已播报
|
||||
- [ ] 模态框正确锁定焦点
|
||||
|
||||
### 表格
|
||||
|
||||
- [ ] 表头与单元格关联
|
||||
- [ ] 表格导航正常
|
||||
- [ ] 复杂表格有标题
|
||||
```
|
||||
|
||||
### 常见问题与修复
|
||||
|
||||
```html
|
||||
<!-- 问题:按钮未播报用途 -->
|
||||
<button><svg>...</svg></button>
|
||||
|
||||
<!-- 修复 -->
|
||||
<button aria-label="关闭对话框"><svg aria-hidden="true">...</svg></button>
|
||||
|
||||
<!-- 问题:动态内容未播报 -->
|
||||
<div id="results">新结果已加载</div>
|
||||
|
||||
<!-- 修复 -->
|
||||
<div id="results" role="status" aria-live="polite">新结果已加载</div>
|
||||
|
||||
<!-- 问题:表单错误未被朗读 -->
|
||||
<input type="email" />
|
||||
<span class="error">无效的电子邮件</span>
|
||||
|
||||
<!-- 修复 -->
|
||||
<input type="email" aria-invalid="true" aria-describedby="email-error" />
|
||||
<span id="email-error" role="alert">无效的电子邮件</span>
|
||||
```
|
||||
|
||||
## NVDA(Windows)
|
||||
|
||||
### 设置
|
||||
|
||||
```
|
||||
下载:nvaccess.org
|
||||
启动:Ctrl + Alt + N
|
||||
停止:Insert + Q
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
|
||||
```
|
||||
导航:
|
||||
Insert = NVDA 修饰键
|
||||
|
||||
下箭头键 下一行
|
||||
上箭头键 上一行
|
||||
Tab 下一个可聚焦元素
|
||||
Shift + Tab 上一个可聚焦元素
|
||||
|
||||
阅读:
|
||||
NVDA + 下箭头键 朗读全部
|
||||
Ctrl 停止语音
|
||||
NVDA + 上箭头键 当前行
|
||||
|
||||
标题:
|
||||
H 下一个标题
|
||||
Shift + H 上一个标题
|
||||
1-6 标题级别 1-6
|
||||
|
||||
表单:
|
||||
F 下一个表单字段
|
||||
B 下一个按钮
|
||||
E 下一个编辑框
|
||||
X 下一个复选框
|
||||
C 下一个组合框
|
||||
|
||||
链接:
|
||||
K 下一个链接
|
||||
U 下一个未访问链接
|
||||
V 下一个已访问链接
|
||||
|
||||
地标:
|
||||
D 下一个地标
|
||||
Shift + D 上一个地标
|
||||
|
||||
表格:
|
||||
T 下一个表格
|
||||
Ctrl + Alt + 箭头键 导航单元格
|
||||
|
||||
元素列表(NVDA + F7):
|
||||
显示所有链接、标题、表单字段、地标
|
||||
```
|
||||
|
||||
### 浏览模式与焦点模式
|
||||
|
||||
```
|
||||
NVDA 自动切换模式:
|
||||
- 浏览模式:箭头键导航内容
|
||||
- 焦点模式:箭头键控制交互元素
|
||||
|
||||
手动切换:NVDA + Space
|
||||
|
||||
注意观察:
|
||||
- 导航时听到「浏览模式」提示
|
||||
- 进入表单字段时听到「焦点模式」
|
||||
- application 角色强制进入表单模式
|
||||
```
|
||||
|
||||
### 测试脚本
|
||||
|
||||
```markdown
|
||||
## NVDA 测试脚本
|
||||
|
||||
### 初始加载
|
||||
|
||||
1. 导航到页面
|
||||
2. 等待页面加载完成
|
||||
3. 按 Insert + 下箭头键朗读全部内容
|
||||
4. 注意:页面标题、主要内容是否已识别?
|
||||
|
||||
### 地标导航
|
||||
|
||||
1. 反复按 D
|
||||
2. 检查:所有主要区域是否可达?
|
||||
3. 检查:地标是否已正确标注?
|
||||
|
||||
### 标题导航
|
||||
|
||||
1. 按 Insert + F7 → 标题
|
||||
2. 检查:标题结构是否合理?
|
||||
3. 按 H 导航标题
|
||||
4. 检查:所有章节是否可发现?
|
||||
|
||||
### 表单测试
|
||||
|
||||
1. 按 F 找到第一个表单字段
|
||||
2. 检查:标签是否朗读?
|
||||
3. 输入无效数据
|
||||
4. 提交表单
|
||||
5. 检查:错误是否已播报?
|
||||
6. 检查:焦点是否移至错误处?
|
||||
|
||||
### 交互元素
|
||||
|
||||
1. 用 Tab 遍历所有交互元素
|
||||
2. 检查:每个元素是否播报角色和状态
|
||||
3. 用 Enter/Space 激活按钮
|
||||
4. 检查:结果是否已播报?
|
||||
|
||||
### 动态内容
|
||||
|
||||
1. 触发内容更新
|
||||
2. 检查:变化是否已播报?
|
||||
3. 打开模态框
|
||||
4. 检查:焦点是否锁定?
|
||||
5. 关闭模态框
|
||||
6. 检查:焦点是否返回?
|
||||
```
|
||||
|
||||
## JAWS(Windows)
|
||||
|
||||
### 常用命令
|
||||
|
||||
```
|
||||
启动:桌面快捷方式或 Ctrl + Alt + J
|
||||
虚拟光标:浏览器中自动启用
|
||||
|
||||
导航:
|
||||
箭头键 导航内容
|
||||
Tab 下一个可聚焦元素
|
||||
Insert + 下箭头键 朗读全部
|
||||
Ctrl 停止语音
|
||||
|
||||
快速键:
|
||||
H 下一个标题
|
||||
T 下一个表格
|
||||
F 下一个表单字段
|
||||
B 下一个按钮
|
||||
G 下一个图形
|
||||
L 下一个列表
|
||||
; 下一个地标
|
||||
|
||||
表单模式:
|
||||
Enter 进入表单模式
|
||||
Numpad + 退出表单模式
|
||||
F5 列出表单字段
|
||||
|
||||
列表:
|
||||
Insert + F7 链接列表
|
||||
Insert + F6 标题列表
|
||||
Insert + F5 表单字段列表
|
||||
|
||||
表格:
|
||||
Ctrl + Alt + 箭头键 表格导航
|
||||
```
|
||||
|
||||
## TalkBack(Android)
|
||||
|
||||
### 设置
|
||||
|
||||
```
|
||||
启用:设置 → 辅助功能 → TalkBack
|
||||
开关:同时按住两个音量键 3 秒
|
||||
```
|
||||
|
||||
### 手势
|
||||
|
||||
```
|
||||
探索:在屏幕上拖动手指
|
||||
下一个:向右滑动
|
||||
上一个:向左滑动
|
||||
激活:双击
|
||||
滚动:双指滑动
|
||||
|
||||
阅读控制(向上滑动再向右):
|
||||
- 标题
|
||||
- 链接
|
||||
- 控件
|
||||
- 字符
|
||||
- 单词
|
||||
- 行
|
||||
- 段落
|
||||
```
|
||||
|
||||
## 常见测试场景
|
||||
|
||||
### 1. 模态对话框
|
||||
|
||||
```html
|
||||
<!-- 无障碍模态框结构 -->
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="dialog-title"
|
||||
aria-describedby="dialog-desc"
|
||||
>
|
||||
<h2 id="dialog-title">确认删除</h2>
|
||||
<p id="dialog-desc">此操作不可撤销。</p>
|
||||
<button>取消</button>
|
||||
<button>删除</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 焦点管理
|
||||
function openModal(modal) {
|
||||
// 保存最后聚焦的元素
|
||||
lastFocus = document.activeElement;
|
||||
|
||||
// 将焦点移到模态框
|
||||
modal.querySelector("h2").focus();
|
||||
|
||||
// 锁定焦点
|
||||
modal.addEventListener("keydown", trapFocus);
|
||||
}
|
||||
|
||||
function closeModal(modal) {
|
||||
// 恢复焦点
|
||||
lastFocus.focus();
|
||||
}
|
||||
|
||||
function trapFocus(e) {
|
||||
if (e.key === "Tab") {
|
||||
const focusable = modal.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
last.focus();
|
||||
e.preventDefault();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
first.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "Escape") {
|
||||
closeModal(modal);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 动态区域
|
||||
|
||||
```html
|
||||
<!-- 状态消息(礼貌式) -->
|
||||
<div role="status" aria-live="polite" aria-atomic="true">
|
||||
<!-- 内容更新将在当前语音播报完成后播报 -->
|
||||
</div>
|
||||
|
||||
<!-- 弹窗(打断式) -->
|
||||
<div role="alert" aria-live="assertive">
|
||||
<!-- 内容更新会打断当前语音播报 -->
|
||||
</div>
|
||||
|
||||
<!-- 进度更新 -->
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-valuenow="75"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-label="上传进度"
|
||||
></div>
|
||||
|
||||
<!-- 日志(仅新增) -->
|
||||
<div role="log" aria-live="polite" aria-relevant="additions">
|
||||
<!-- 新消息播报,移除的不播报 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 3. 标签页界面
|
||||
|
||||
```html
|
||||
<div role="tablist" aria-label="产品信息">
|
||||
<button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">
|
||||
描述
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
id="tab-2"
|
||||
aria-selected="false"
|
||||
aria-controls="panel-2"
|
||||
tabindex="-1"
|
||||
>
|
||||
评论
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
|
||||
产品描述内容...
|
||||
</div>
|
||||
|
||||
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
|
||||
评论内容...
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 标签页键盘导航
|
||||
tablist.addEventListener("keydown", (e) => {
|
||||
const tabs = [...tablist.querySelectorAll('[role="tab"]')];
|
||||
const index = tabs.indexOf(document.activeElement);
|
||||
|
||||
let newIndex;
|
||||
switch (e.key) {
|
||||
case "ArrowRight":
|
||||
newIndex = (index + 1) % tabs.length;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
newIndex = (index - 1 + tabs.length) % tabs.length;
|
||||
break;
|
||||
case "Home":
|
||||
newIndex = 0;
|
||||
break;
|
||||
case "End":
|
||||
newIndex = tabs.length - 1;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
tabs[newIndex].focus();
|
||||
activateTab(tabs[newIndex]);
|
||||
e.preventDefault();
|
||||
});
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
```javascript
|
||||
// 记录屏幕阅读器所看到的内容
|
||||
function logAccessibleName(element) {
|
||||
const computed = window.getComputedStyle(element);
|
||||
console.log({
|
||||
role: element.getAttribute("role") || element.tagName,
|
||||
name:
|
||||
element.getAttribute("aria-label") ||
|
||||
element.getAttribute("aria-labelledby") ||
|
||||
element.textContent,
|
||||
state: {
|
||||
expanded: element.getAttribute("aria-expanded"),
|
||||
selected: element.getAttribute("aria-selected"),
|
||||
checked: element.getAttribute("aria-checked"),
|
||||
disabled: element.disabled,
|
||||
},
|
||||
visible: computed.display !== "none" && computed.visibility !== "hidden",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 应做事项
|
||||
|
||||
- **使用真实的屏幕阅读器进行测试**——而不仅仅是模拟器
|
||||
- **优先使用语义化 HTML**——ARIA 是补充手段
|
||||
- **在浏览模式和焦点模式下分别测试**——体验不同
|
||||
- **验证焦点管理**——尤其是单页应用
|
||||
- **先测试纯键盘操作**——这是屏幕阅读器测试的基础
|
||||
|
||||
### 不应做事项
|
||||
|
||||
- **不要认为一个屏幕阅读器就够了**——请测试多个
|
||||
- **不要忽略移动端**——用户群体在持续增长
|
||||
- **不要只测试正常流程**——也要测试错误状态
|
||||
- **不要跳过动态内容**——这是最常见的问题来源
|
||||
- **不要依赖视觉测试**——用户体验完全不同
|
||||
Reference in New Issue
Block a user