screen-reader-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseScreen Reader Testing
屏幕阅读器测试
Practical guide to testing web applications with screen readers for comprehensive accessibility validation.
使用屏幕阅读器测试Web应用程序的实用指南,用于全面的无障碍性验证。
When to Use This Skill
何时使用此技能
- Validating screen reader compatibility
- Testing ARIA implementations
- Debugging assistive technology issues
- Verifying form accessibility
- Testing dynamic content announcements
- Ensuring navigation accessibility
- 验证屏幕阅读器兼容性
- 测试ARIA实现效果
- 调试辅助技术相关问题
- 验证表单无障碍性
- 测试动态内容播报功能
- 确保导航无障碍性
Core Concepts
核心概念
1. Major Screen Readers
1. 主流屏幕阅读器
| Screen Reader | Platform | Browser | Usage |
|---|---|---|---|
| VoiceOver | macOS/iOS | Safari | ~15% |
| NVDA | Windows | Firefox/Chrome | ~31% |
| JAWS | Windows | Chrome/IE | ~40% |
| TalkBack | Android | Chrome | ~10% |
| Narrator | Windows | Edge | ~4% |
| 屏幕阅读器 | 平台 | 浏览器 | 使用率 |
|---|---|---|---|
| VoiceOver | macOS/iOS | Safari | ~15% |
| NVDA | Windows | Firefox/Chrome | ~31% |
| JAWS | Windows | Chrome/IE | ~40% |
| TalkBack | Android | Chrome | ~10% |
| Narrator | Windows | Edge | ~4% |
2. Testing Priority
2. 测试优先级
Minimum Coverage:
1. NVDA + Firefox (Windows)
2. VoiceOver + Safari (macOS)
3. VoiceOver + Safari (iOS)
Comprehensive Coverage:
+ JAWS + Chrome (Windows)
+ TalkBack + Chrome (Android)
+ Narrator + Edge (Windows)Minimum Coverage:
1. NVDA + Firefox (Windows)
2. VoiceOver + Safari (macOS)
3. VoiceOver + Safari (iOS)
Comprehensive Coverage:
+ JAWS + Chrome (Windows)
+ TalkBack + Chrome (Android)
+ Narrator + Edge (Windows)3. Screen Reader Modes
3. 屏幕阅读器模式
| Mode | Purpose | When Used |
|---|---|---|
| Browse/Virtual | Read content | Default reading |
| Focus/Forms | Interact with controls | Filling forms |
| Application | Custom widgets | ARIA applications |
| 模式 | 用途 | 使用场景 |
|---|---|---|
| 浏览/虚拟模式 | 读取内容 | 默认阅读场景 |
| 焦点/表单模式 | 与控件交互 | 填写表单时 |
| 应用模式 | 自定义组件交互 | ARIA应用场景 |
VoiceOver (macOS)
VoiceOver (macOS)
Setup
设置步骤
Enable: System Preferences → Accessibility → VoiceOver
Toggle: Cmd + F5
Quick Toggle: Triple-press Touch IDEnable: System Preferences → Accessibility → VoiceOver
Toggle: Cmd + F5
Quick Toggle: Triple-press Touch IDEssential Commands
核心命令
Navigation:
VO = Ctrl + Option (VoiceOver modifier)
VO + Right Arrow Next element
VO + Left Arrow Previous element
VO + Shift + Down Enter group
VO + Shift + Up Exit group
Reading:
VO + A Read all from cursor
Ctrl Stop speaking
VO + B Read current paragraph
Interaction:
VO + Space Activate element
VO + Shift + M Open menu
Tab Next focusable element
Shift + Tab Previous focusable element
Rotor (VO + U):
Navigate by: Headings, Links, Forms, Landmarks
Left/Right Arrow Change rotor category
Up/Down Arrow Navigate within category
Enter Go to item
Web Specific:
VO + Cmd + H Next heading
VO + Cmd + J Next form control
VO + Cmd + L Next link
VO + Cmd + T Next tableNavigation:
VO = Ctrl + Option (VoiceOver modifier)
VO + Right Arrow Next element
VO + Left Arrow Previous element
VO + Shift + Down Enter group
VO + Shift + Up Exit group
Reading:
VO + A Read all from cursor
Ctrl Stop speaking
VO + B Read current paragraph
Interaction:
VO + Space Activate element
VO + Shift + M Open menu
Tab Next focusable element
Shift + Tab Previous focusable element
Rotor (VO + U):
Navigate by: Headings, Links, Forms, Landmarks
Left/Right Arrow Change rotor category
Up/Down Arrow Navigate within category
Enter Go to item
Web Specific:
VO + Cmd + H Next heading
VO + Cmd + J Next form control
VO + Cmd + L Next link
VO + Cmd + T Next tableTesting Checklist
测试检查清单
markdown
undefinedmarkdown
undefinedVoiceOver Testing Checklist
VoiceOver Testing Checklist
Page Load
Page Load
- Page title announced
- Main landmark found
- Skip link works
- Page title announced
- Main landmark found
- Skip link works
Navigation
Navigation
- All headings discoverable via rotor
- Heading levels logical (H1 → H2 → H3)
- Landmarks properly labeled
- Skip links functional
- All headings discoverable via rotor
- Heading levels logical (H1 → H2 → H3)
- Landmarks properly labeled
- Skip links functional
Links & Buttons
Links & Buttons
- Link purpose clear
- Button actions described
- New window/tab announced
- Link purpose clear
- Button actions described
- New window/tab announced
Forms
Forms
- All labels read with inputs
- Required fields announced
- Error messages read
- Instructions available
- Focus moves to errors
- All labels read with inputs
- Required fields announced
- Error messages read
- Instructions available
- Focus moves to errors
Dynamic Content
Dynamic Content
- Alerts announced immediately
- Loading states communicated
- Content updates announced
- Modals trap focus correctly
- Alerts announced immediately
- Loading states communicated
- Content updates announced
- Modals trap focus correctly
Tables
Tables
- Headers associated with cells
- Table navigation works
- Complex tables have captions
undefined- Headers associated with cells
- Table navigation works
- Complex tables have captions
undefinedCommon Issues & Fixes
常见问题与修复方案
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>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>NVDA (Windows)
NVDA (Windows)
Setup
设置步骤
Download: nvaccess.org
Start: Ctrl + Alt + N
Stop: Insert + QDownload: nvaccess.org
Start: Ctrl + Alt + N
Stop: Insert + QEssential Commands
核心命令
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, landmarksNavigation:
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, landmarksBrowse vs Focus Mode
浏览模式与焦点模式
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 modeNVDA 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 modeTesting Script
测试脚本
markdown
undefinedmarkdown
undefinedNVDA Test Script
NVDA Test Script
Initial Load
Initial Load
- Navigate to page
- Let page finish loading
- Press Insert + Down to read all
- Note: Page title, main content identified?
- Navigate to page
- Let page finish loading
- Press Insert + Down to read all
- Note: Page title, main content identified?
Landmark Navigation
Landmark Navigation
- Press D repeatedly
- Check: All main areas reachable?
- Check: Landmarks properly labeled?
- Press D repeatedly
- Check: All main areas reachable?
- Check: Landmarks properly labeled?
Heading Navigation
Heading Navigation
- Press Insert + F7 → Headings
- Check: Logical heading structure?
- Press H to navigate headings
- Check: All sections discoverable?
- Press Insert + F7 → Headings
- Check: Logical heading structure?
- Press H to navigate headings
- Check: All sections discoverable?
Form Testing
Form Testing
- Press F to find first form field
- Check: Label read?
- Fill in invalid data
- Submit form
- Check: Errors announced?
- Check: Focus moved to error?
- Press F to find first form field
- Check: Label read?
- Fill in invalid data
- Submit form
- Check: Errors announced?
- Check: Focus moved to error?
Interactive Elements
Interactive Elements
- Tab through all interactive elements
- Check: Each announces role and state
- Activate buttons with Enter/Space
- Check: Result announced?
- Tab through all interactive elements
- Check: Each announces role and state
- Activate buttons with Enter/Space
- Check: Result announced?
Dynamic Content
Dynamic Content
- Trigger content update
- Check: Change announced?
- Open modal
- Check: Focus trapped?
- Close modal
- Check: Focus returns?
undefined- Trigger content update
- Check: Change announced?
- Open modal
- Check: Focus trapped?
- Close modal
- Check: Focus returns?
undefinedJAWS (Windows)
JAWS (Windows)
Essential Commands
核心命令
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 navigationStart: 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 navigationTalkBack (Android)
TalkBack (Android)
Setup
设置步骤
Enable: Settings → Accessibility → TalkBack
Toggle: Hold both volume buttons 3 secondsEnable: Settings → Accessibility → TalkBack
Toggle: Hold both volume buttons 3 secondsGestures
手势操作
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
- ParagraphsExplore: 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
- ParagraphsCommon Test Scenarios
常见测试场景
1. Modal Dialog
1. 模态对话框
html
<!-- 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>javascript
// 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);
}
}html
<!-- 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>javascript
// 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);
}
}2. Live Regions
2. 实时区域
html
<!-- 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>html
<!-- 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>3. Tab Interface
3. 标签页界面
html
<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>javascript
// 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();
});html
<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>javascript
// 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();
});Debugging Tips
调试技巧
javascript
// 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",
});
}javascript
// 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",
});
}Best Practices
最佳实践
Do's
建议
- Test with actual screen readers - Not just simulators
- Use semantic HTML first - ARIA is supplemental
- Test in browse and focus modes - Different experiences
- Verify focus management - Especially for SPAs
- Test keyboard only first - Foundation for SR testing
- 使用真实屏幕阅读器测试 - 不要仅依赖模拟器
- 优先使用语义化HTML - ARIA仅作为补充
- 测试浏览和焦点两种模式 - 两种模式体验不同
- 验证焦点管理 - 尤其是单页应用(SPA)
- 先测试纯键盘操作 - 这是屏幕阅读器测试的基础
Don'ts
禁忌
- Don't assume one SR is enough - Test multiple
- Don't ignore mobile - Growing user base
- Don't test only happy path - Test error states
- Don't skip dynamic content - Most common issues
- Don't rely on visual testing - Different experience
- 不要仅依赖一种屏幕阅读器 - 测试多种主流工具
- 不要忽略移动端 - 移动端用户群体正在增长
- 不要仅测试正常流程 - 需测试错误状态
- 不要跳过动态内容 - 这是最常见的问题点
- 不要仅依赖视觉测试 - 屏幕阅读器的体验与视觉完全不同