首页
/ VoiceOver Testing Checklist

VoiceOver Testing Checklist

2026-09-05 19:15:50作者:宗隆裙

VoiceOver Testing Checklist

Page Load

  • [ ] Page title announced
  • [ ] Main landmark found
  • [ ] Skip link works

Navigation

  • [ ] All headings discoverable via rotor
  • [ ] Heading levels logical (H1 → H2 → H3)
  • [ ] Landmarks properly labeled
  • [ ] Skip links functional

Links & Buttons

  • [ ] Link purpose clear
  • [ ] Button actions described
  • [ ] New window/tab announced

Forms

  • [ ] All labels read with inputs
  • [ ] Required fields announced
  • [ ] Error messages read
  • [ ] Instructions available
  • [ ] Focus moves to errors

Dynamic Content

  • [ ] Alerts announced immediately
  • [ ] Loading states communicated
  • [ ] Content updates announced
  • [ ] Modals trap focus correctly

Tables

  • [ ] Headers associated with cells
  • [ ] Table navigation works
  • [ ] Complex tables have captions

### 常见问题与修复

技能文档给出了三组典型“问题代码 → 修复代码”对照:

```html
<!-- Issue: Button not announcing purpose -->
<button><svg>...</svg></button>

<!-- Fix -->
<button aria-label="Close dialog"><svg aria-hidden="true">...</svg></button>

<!-- Issue: Dynamic content not announced -->
<div id="results">New results loaded</div>

<!-- Fix -->
<div id="results" role="status" aria-live="polite">New results loaded</div>

<!-- Issue: Form error not read -->
<input type="email" />
<span class="error">Invalid email</span>

<!-- Fix -->
<input type="email" aria-invalid="true" aria-describedby="email-error" />
<span id="email-error" role="alert">Invalid email</span>

三处修复分别对应屏幕阅读器领域的三类经典缺陷:

  1. 图标按钮无名称:纯 SVG 的 <button> 没有可访问名称,屏幕阅读器只会播报 “button”。修复方式为按钮加 aria-label 声明用途,同时给 SVG 加 aria-hidden="true" 防止其内部文本被重复播报。
  2. 动态内容静默更新:JS 直接改写 <div> 内容不会触发播报,需将该区域标记为 role="status"(等价于 aria-live="polite" 的隐式声明)或显式加 aria-live
  3. 表单错误不播报:错误提示须通过 aria-describedby 与输入控件关联(使播报名称中包含错误),用 aria-invalid="true" 标记无效状态,并给提示元素 role="alert" 使其作为断言型实时区域即时播报。

NVDA(Windows)完整测试手册

环境配置

Download: nvaccess.org
Start: Ctrl + Alt + N
Stop: Insert + Q

常用命令全集

NVDA 修饰键为 Insert,常用命令如下:

Navigation:
Insert = NVDA modifier

Down Arrow         Next line
Up Arrow           Previous line
Tab                Next focusable
Shift + Tab        Previous focusable

Reading:
NVDA + Down Arrow  Say all
Ctrl               Stop speech
NVDA + Up Arrow    Current line

Headings:
H                  Next heading
Shift + H          Previous heading
1-6                Heading level 1-6

Forms:
F                  Next form field
B                  Next button
E                  Next edit field
X                  Next checkbox
C                  Next combo box

Links:
K                  Next link
U                  Next unvisited link
V                  Next visited link

Landmarks:
D                  Next landmark
Shift + D          Previous landmark

Tables:
T                  Next table
Ctrl + Alt + Arrows Navigate cells

Elements List (NVDA + F7):
Shows all links, headings, form fields, landmarks

几个值得注意的命令:NVDA + F7 打开元素列表(Elements List),可一次性列出页面全部链接、标题、表单字段、地标,适合快速核对页面结构;Ctrl + Alt + 方向键 用于表格单元格间导航,是检验表头关联是否正确的直接手段。

Browse 模式与 Focus 模式

NVDA 会在两种模式间自动切换,这是 Windows 侧测试必须观察的行为:

NVDA automatically switches modes:
- Browse Mode: Arrow keys navigate content
- Focus Mode: Arrow keys control interactive elements

Manual switch: NVDA + Space

Watch for:
- "Browse mode" announcement when navigating
- "Focus mode" when entering form fields
- Application role forces forms mode

排查“方向键行为突然变了”这类问题,先确认播报里出现的是 “Browse mode” 还是 “Focus mode”;特别地,role="application" 会强制进入表单模式,这在自绘 Canvas/富文本组件中影响很大。

NVDA 测试脚本

技能文档提供了一套按步骤执行的 NVDA 测试脚本,从初始加载到动态内容共六个环节:

## NVDA Test Script

### Initial Load

1. Navigate to page
2. Let page finish loading
3. Press Insert + Down to read all
4. Note: Page title, main content identified?

### Landmark Navigation

1. Press D repeatedly
2. Check: All main areas reachable?
3. Check: Landmarks properly labeled?

### Heading Navigation

1. Press Insert + F7 → Headings
2. Check: Logical heading structure?
3. Press H to navigate headings
4. Check: All sections discoverable?

### Form Testing

1. Press F to find first form field
2. Check: Label read?
3. Fill in invalid data
4. Submit form
5. Check: Errors announced?
6. Check: Focus moved to error?

### Interactive Elements

1. Tab through all interactive elements
2. Check: Each announces role and state
3. Activate buttons with Enter/Space
4. Check: Result announced?

### Dynamic Content

1. Trigger content update
2. Check: Change announced?
3. Open modal
4. Check: Focus trapped?
5. Close modal
6. Check: Focus returns?

该脚本的验证顺序暗含一条依赖链:地标 → 标题 → 表单 → 交互元素 → 动态内容,前一级通过是后一级有效测试的前提(例如标题结构混乱时,Insert + F7 的结果就不可信)。

JAWS(Windows)与 TalkBack(Android)要点

JAWS 常用命令

Start: Desktop shortcut or Ctrl + Alt + J
Virtual Cursor: Auto-enabled in browsers

Navigation:
Arrow keys         Navigate content
Tab                Next focusable
Insert + Down      Read all
Ctrl               Stop speech

Quick Keys:
H                  Next heading
T                  Next table
F                  Next form field
B                  Next button
G                  Next graphic
L                  Next list
;                  Next landmark

Forms Mode:
Enter              Enter forms mode
Numpad +           Exit forms mode
F5                 List form fields

Lists:
Insert + F7        Link list
Insert + F6        Heading list
Insert + F5        Form field list

Tables:
Ctrl + Alt + Arrows Table navigation

JAWS 在浏览器中默认启用虚拟光标(Virtual Cursor),其“元素列表”三件套(Insert + F7 链接列表、Insert + F6 标题列表、Insert + F5 表单字段列表)与 NVDA 的元素列表功能对应,是两份环境共用的结构核对手段。

TalkBack(Android)

Enable: Settings → Accessibility → TalkBack
Toggle: Hold both volume buttons 3 seconds

移动端全部依赖手势完成:

Explore: Drag finger across screen
Next: Swipe right
Previous: Swipe left
Activate: Double tap
Scroll: Two finger swipe

Reading Controls (swipe up then right):
- Headings
- Links
- Controls
- Characters
- Words
- Lines
- Paragraphs

TalkBack 的 “Reading Controls”(上滑再右滑唤出)提供按标题/链接/控件等粒度的导航,其作用等价于 VoiceOver 的 Rotor 和 NVDA/JAWS 的元素列表——移动端测试时应确认页面在按标题导航时呈现的层级与桌面端一致。

三大典型场景的参考实现

以下三组“HTML + JavaScript”参考实现均出自技能文档,是屏幕阅读器验证中最常出问题的三个组件类型。

场景 1:模态对话框(Modal Dialog)

模态框必须满足三件事:角色与标题关联(role="dialog" + aria-modal="true" + aria-labelledby)、焦点被约束在框内(focus trap)、关闭后焦点归还触发元素。

<!-- Accessible modal structure -->
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc"
>
  <h2 id="dialog-title">Confirm Delete</h2>
  <p id="dialog-desc">This action cannot be undone.</p>
  <button>Cancel</button>
  <button>Delete</button>
</div>
// Focus management
function openModal(modal) {
  // Store last focused element
  lastFocus = document.activeElement;

  // Move focus to modal
  modal.querySelector("h2").focus();

  // Trap focus
  modal.addEventListener("keydown", trapFocus);
}

function closeModal(modal) {
  // Return focus
  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);
  }
}

用屏幕阅读器验证时对应检查点:打开后 NVDA 是否播报对话框标题(VoiceOver 会播报 “Confirm Delete, dialog”)、Tab 循环是否困在框内、关闭后焦点是否回到触发按钮——这正是 NVDA 测试脚本 “Dynamic Content” 环节的 3~6 步。

场景 2:实时区域(Live Regions)

动态内容的播报策略由 aria-live 的礼貌等级与角色决定:

<!-- Status messages (polite) -->
<div role="status" aria-live="polite" aria-atomic="true">
  <!-- Content updates will be announced after current speech -->
</div>

<!-- Alerts (assertive) -->
<div role="alert" aria-live="assertive">
  <!-- Content updates interrupt current speech -->
</div>

<!-- Progress updates -->
<div
  role="progressbar"
  aria-valuenow="75"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-label="Upload progress"
></div>

<!-- Log (additions only) -->
<div role="log" aria-live="polite" aria-relevant="additions">
  <!-- New messages announced, removals not -->
</div>

选择原则:role="status"(polite)用于“结果已加载”这类常规状态更新,不打断当前播报;role="alert"(assertive)用于表单校验失败、系统错误这类必须立刻感知的事件;role="log" 配合 aria-relevant="additions" 只播报追加内容,适合消息流、通知面板;进度类内容用 role="progressbar" 并维护 aria-valuenow

场景 3:标签页界面(Tab Interface)

标签页需要完整的 tablist / tab / tabpanel 角色链,配合 aria-selectedaria-controlstabindex 管理,并实现方向键 / Home / End 键盘导航:

<div role="tablist" aria-label="Product information">
  <button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">
    Description
  </button>
  <button
    role="tab"
    id="tab-2"
    aria-selected="false"
    aria-controls="panel-2"
    tabindex="-1"
  >
    Reviews
  </button>
</div>

<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  Product description content...
</div>

<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  Reviews content...
</div>
// Tab keyboard navigation
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();
});

实现要点:非选中的 tab 使用 tabindex="-1" 使 Tab 键只进出 tablist 一次、方向键在内部切换(roving tabindex 模式);隐藏的 panel 用 hidden 属性从可访问性树中移除,避免屏幕阅读器读到已隐藏内容。用 NVDA 验证时,焦点停在 tab 上应听到 “Tab, 2 of 2, Description, unselected” 之类的角色、序号与状态播报。

调试技巧:查看屏幕阅读器“看到”的内容

当播报不符合预期时,技能文档提供了一个控制台辅助函数,用于打印元素的等效角色、可访问名称与状态,帮助对齐 DOM 与屏幕阅读器认知:

// Log what screen reader sees
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",
  });
}
登录后查看全文
热门项目推荐
相关项目推荐