539 lines
12 KiB
Markdown
539 lines
12 KiB
Markdown
---
|
||
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 是补充手段
|
||
- **在浏览模式和焦点模式下分别测试**——体验不同
|
||
- **验证焦点管理**——尤其是单页应用
|
||
- **先测试纯键盘操作**——这是屏幕阅读器测试的基础
|
||
|
||
### 不应做事项
|
||
|
||
- **不要认为一个屏幕阅读器就够了**——请测试多个
|
||
- **不要忽略移动端**——用户群体在持续增长
|
||
- **不要只测试正常流程**——也要测试错误状态
|
||
- **不要跳过动态内容**——这是最常见的问题来源
|
||
- **不要依赖视觉测试**——用户体验完全不同
|