Files
2026-07-13 21:37:15 +08:00

12 KiB
Raw Permalink Blame History

name, description
name description
screen-reader-testing 使用屏幕阅读器(包括 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 + FirefoxWindows
2. VoiceOver + SafarimacOS
3. VoiceOver + SafariiOS

全面覆盖:
+ JAWS + ChromeWindows
+ TalkBack + ChromeAndroid
+ Narrator + EdgeWindows

3. 屏幕阅读器模式

模式 用途 使用场景
浏览/虚拟 阅读内容 默认阅读
焦点/表单 与控件交互 填写表单
应用程序 自定义微件 ARIA 应用程序

VoiceOvermacOS

设置

启用:系统设置 → 辅助功能 → VoiceOver
开关:Cmd + F5
快速开关:三按 Touch ID

常用命令

导航:
VO = Ctrl + OptionVoiceOver 修饰键)

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    下一个表格

测试清单

## VoiceOver 测试清单

### 页面加载

- [ ] 页面标题已播报
- [ ] 找到主要地标
- [ ] 跳过链接可用

### 导航

- [ ] 所有标题可通过转子发现
- [ ] 标题层级合理(H1 → H2 → H3)
- [ ] 地标已正确标注
- [ ] 跳过链接功能正常

### 链接与按钮

- [ ] 链接用途清晰
- [ ] 按钮操作已描述
- [ ] 新窗口/标签页已播报

### 表单

- [ ] 所有标签随输入框一并朗读
- [ ] 必填字段已播报
- [ ] 错误信息已朗读
- [ ] 操作指引可用
- [ ] 焦点移至错误处

### 动态内容

- [ ] 弹窗立即播报
- [ ] 加载状态已传达
- [ ] 内容更新已播报
- [ ] 模态框正确锁定焦点

### 表格

- [ ] 表头与单元格关联
- [ ] 表格导航正常
- [ ] 复杂表格有标题

常见问题与修复

<!-- 问题:按钮未播报用途 -->
<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>

NVDAWindows

设置

下载: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 角色强制进入表单模式

测试脚本

## 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. 检查:焦点是否返回?

JAWSWindows

常用命令

启动:桌面快捷方式或 Ctrl + Alt + J
虚拟光标:浏览器中自动启用

导航:
箭头键           导航内容
Tab              下一个可聚焦元素
Insert + 下箭头键 朗读全部
Ctrl             停止语音

快速键:
H                下一个标题
T                下一个表格
F                下一个表单字段
B                下一个按钮
G                下一个图形
L                下一个列表
;                下一个地标

表单模式:
Enter            进入表单模式
Numpad +         退出表单模式
F5               列出表单字段

列表:
Insert + F7      链接列表
Insert + F6      标题列表
Insert + F5      表单字段列表

表格:
Ctrl + Alt + 箭头键  表格导航

TalkBackAndroid

设置

启用:设置 → 辅助功能 → TalkBack
开关:同时按住两个音量键 3 秒

手势

探索:在屏幕上拖动手指
下一个:向右滑动
上一个:向左滑动
激活:双击
滚动:双指滑动

阅读控制(向上滑动再向右):
- 标题
- 链接
- 控件
- 字符
- 单词
- 行
- 段落

常见测试场景

1. 模态对话框

<!-- 无障碍模态框结构 -->
<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>
// 焦点管理
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. 动态区域

<!-- 状态消息(礼貌式) -->
<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. 标签页界面

<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>
// 标签页键盘导航
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();
});

调试技巧

// 记录屏幕阅读器所看到的内容
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 是补充手段
  • 在浏览模式和焦点模式下分别测试——体验不同
  • 验证焦点管理——尤其是单页应用
  • 先测试纯键盘操作——这是屏幕阅读器测试的基础

不应做事项

  • 不要认为一个屏幕阅读器就够了——请测试多个
  • 不要忽略移动端——用户群体在持续增长
  • 不要只测试正常流程——也要测试错误状态
  • 不要跳过动态内容——这是最常见的问题来源
  • 不要依赖视觉测试——用户体验完全不同