screen-reader-test-author
Overview
Automated tools (axe-core, pa11y, Lighthouse) catch ~30-40% of
accessibility issues - the structural ones. The remaining 60-70%
require manual screen-reader testing by an actual human using
NVDA / JAWS / VoiceOver / TalkBack. The team needs a repeatable
script that any tester (not just the original author) can follow.
This skill takes a user flow and produces that script: per-step
keystroke + expected announcement. Two sibling artifact shapes live in
references/: a per-archetype WCAG 2.2 checklist generator for
spec-review-time acceptance criteria
(references/wcag-checklist.md) and
per-widget keystroke matrices for hand-run NVDA / VoiceOver passes
(references/widget-matrix.md). The
"Running the session" section below turns script + checklist into a
guided, signed session.
When to use
- A high-stakes feature ships and needs accessibility sign-off.
- Automated a11y tools are green but the team wants confidence
the actual screen-reader experience is good.
- Onboarding a new accessibility tester - they need a script to
follow until they internalize the conventions.
- Documenting an "a11y regression repro" when a bug ships.
Step 1 - Pick the screen reader
Most a11y audit conventions cover at least:
| Screen reader | Platform | Browser pairing |
|---|
| NVDA | Windows | Firefox or Chrome |
| JAWS | Windows | Chrome or Edge |
| VoiceOver | macOS / iOS / iPadOS | Safari (macOS); Safari (iOS) |
| TalkBack | Android | Chrome |
For the most-coverage-per-effort, test NVDA + Firefox and
VoiceOver + Safari - these are also the WAI-recommended pairs.
Start each script by naming the SR + browser + OS combination.
Step 2 - Define the flow
A flow is a sequence of user goals. Don't write step-by-step
keystrokes upfront - that's Step 3. The flow is at user-intent
level:
flow_name: "User edits their profile email"
preconditions:
- User is logged in.
- User is on the /profile page.
- Screen reader is on; browser virtual cursor is at the page heading.
steps:
- intent: "Navigate to the profile-edit form via main nav"
- intent: "Tab through the form to the email field"
- intent: "Edit the email value to a new valid email"
- intent: "Submit the form"
- intent: "Hear the success confirmation announcement"
Step 3 - Per intent, capture keystrokes + expected announcement
For each intent, the script row lists three things:
- Keystroke - the actual key combo the tester presses.
- Expected announcement - what the screen reader should say.
- Why - the WCAG SC or APG pattern the announcement satisfies.
NVDA browse mode navigates with quick-keys (H by heading, F by
form field); VoiceOver uses VO (Ctrl+Option) chords:
| Action | VoiceOver shortcut |
|---|
| Read next / previous | VO + Right / Left arrow |
| Next heading | VO + Cmd + H |
| Next form control | VO + Cmd + J |
| Activate (Enter) | VO + Space |
| Web rotor | VO + U (form / heading / link list overlay) |
A minimal NVDA + Firefox excerpt for the flow "User edits their
profile email":
| Keystroke | Expected announcement |
|---|
| H | "Edit profile, heading level 2" - moves to next heading. |
| F | "Email, edit, blank" - jumps to the first form field. |
| Tab, Enter | "Save changes, button", then a live region announces "Profile saved". |
Full per-reader scripts (NVDA + Firefox and VoiceOver + Safari) for
this flow, with the WCAG / APG mapping in the "Why" column:
references/screen-reader-scripts.md.
VoiceOver announcements are sometimes more verbose than NVDA's (role
words like "edit text" where NVDA says "edit"); that is a vendor
convention, not a bug to suppress.
Step 4 - Define what counts as PASS
For each step, the announcement should:
- Identify the element type - "button", "edit", "link",
"heading level 2", etc.
- Read the visible label verbatim - if the visible label is
"Save changes", the SR should say "Save changes" (not "Submit"
or the
id attribute).
- Convey state - for a checkbox, "checked" / "not checked";
for a button, "expanded" / "collapsed" / "pressed".
- Convey position in a set - for a tab, "1 of 4"; for a list
item, "1 of 10".
Failures to flag:
| Failure | Likely cause |
|---|
Announcement reads CSS class names or id attributes | Label is missing; SR fell back to other text. |
| Element type is wrong ("link" instead of "button") | <a> used as a button; or role="link" on a button. |
| State is missing | aria-expanded / aria-checked not set. |
| Position-in-set is missing | aria-setsize / aria-posinset not set on list items. |
| Live region announcement doesn't fire | Region added with content already present, OR aria-live="off". |
Step 5 - Wire to the test plan
The script becomes a manual test in the project's test plan.
Common formats:
Markdown checklist (lightweight)
## A11y Acceptance - User edits profile email
### NVDA + Firefox (Windows)
- [ ] Step 1: Pressing H twice lands on "Profile information" heading.
- [ ] Step 2: Pressing F lands on the email field; announces "Email, edit, blank".
- [ ] Step 3: After submit, a live region announces "Profile saved".
### VoiceOver + Safari (macOS)
- [ ] Step 1: VO+Cmd+H twice lands on "Profile information" heading.
- [ ] Step 2: VO+Cmd+J lands on email; announces "Email, edit text".
- [ ] Step 3: After submit, live region announces "Profile saved".
Sign-off: ___________________ Date: __________
Test-management tool integration
For teams using TestRail / Xray / Zephyr - encode the flow as a
manual test case with one step per row, expected result per row.
The skill emits the matching format; per
test-management-sync
(when shipped) the test case can be uploaded automatically.
Running the session
When a component has passed automated scans and static review and a human
tester needs a guided manual session to sign off acceptance, compose the
script (Steps 1-5) with the component's per-archetype checklist
(references/wcag-checklist.md) into one
session package. Required inputs: the component's rendered URL, its
archetype, and either an existing script or the intent-level flow (Step 2
format). Without a rendered URL the pre-conditions cannot be defined - stop
and ask.
Build the session package
Merge script + checklist into a single session document:
- Header: component name, archetype, URL, date, tester name (blank field),
SR + browser pair.
- NVDA section (Windows): single-letter quick-navigation keys. Per
WebAIM NVDA guide: H navigates headings, F form controls,
B buttons, K links; Shift+letter moves backwards; NVDA+Space toggles
Browse and Focus modes. Per the NVDA user guide, the default
NVDA modifier key is Insert (or numpadZero with numLock off).
- VoiceOver section (macOS): per WebAIM VoiceOver guide, the
VO keys are Control+Option; VO+Right/Left reads next/previous item;
VO+Cmd+H navigates headings; VO+Cmd+J form controls; VO+Space activates;
VO+U opens the Web Rotor.
- Checklist rows mapped to each script step, so the tester sees the WCAG SC
being verified alongside the keystroke.
Walk the tester through it
- Pre-conditions block: SR running, browser open to the URL, virtual
cursor at page top (NVDA browse mode; VoiceOver with Web Rotor closed).
- For each intent step: the keystroke, the expected announcement, the WCAG
SC being verified, and a
[ ] PASS / [ ] FAIL / [ ] BLOCKED
checkbox.
- After each overlay or composite step: a focus-return checkpoint per the
W3C APG dialog-modal pattern (focus must return to the
trigger on close).
- After each live-region step: a timing checkpoint (announce within
1-2 s of state change; matches the
aria-live="polite" contract per
WCAG SC 4.1.3 Status Messages).
Capture the session report
Write the results to a11y-sessions/<component>-<YYYY-MM-DD>-<sr>.md:
## Screen-Reader Session Report
**Component:** <name> **Archetype:** <archetype> **URL:** <url>
**Date:** <YYYY-MM-DD> **Tester:** _______________
**SR + Browser:** NVDA + Firefox | VoiceOver + Safari
| Step | Intent | Keystroke | Expected announcement | WCAG SC | Result |
|------|--------|-----------|-----------------------|---------|--------|
| 1 | Navigate to component heading | H | "<Component>, heading level 2" | 1.3.1 / 2.4.6 | [ ] PASS [ ] FAIL [ ] BLOCKED |
### Summary
| Verdict | Count |
|---------|-------|
| PASS / FAIL / BLOCKED | N |
**Overall:** PASS / FAIL / INCOMPLETE
**Failures to remediate:**
1. <Step N>: <what was announced> vs. <expected> - likely cause + WCAG SC.
**Sign-off:** _______________ Date: __________
The session never modifies component source. iOS VoiceOver and TalkBack
(Android) have different gesture models - author a separate mobile script
(Step 1) rather than reusing the desktop session.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|
| Testing on only one screen reader | NVDA bugs ≠ JAWS bugs ≠ VoiceOver bugs. | At least two reader/browser pairs (NVDA+Firefox, VoiceOver+Safari). |
| Asking the developer to test their own work | Familiarity bias; the dev knows how it should sound. | Independent tester or accessibility specialist; use the script blindly. |
| Recording expected announcements verbatim from one version | Screen-reader announcement strings change between versions. | Test the announcement contains key elements (label, role, state); avoid string-equality. |
| Skipping the "Why" column | Tester can't generalize from "this announcement was wrong" to "the underlying ARIA pattern is broken." | Always link to the WCAG SC or APG pattern. |
Limitations
- Manual. Cannot fully automate; the tester is the QA
instrument.
- Vendor variance. A test script that passes on NVDA might
fail on JAWS due to vendor-specific announcement choices; the
underlying code may be correct.
- Mobile tests. TalkBack and iOS VoiceOver have very different
gesture sets from desktop SRs; mobile flows need separate
scripts.
References