a11y-test

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Accessibility Testing Skill

可访问性测试技能

Browser Tooling Routing (read first)

浏览器工具路由(请先阅读)

Pick the right execution mode from the routing table before running anything (the table is the source of truth — don't trust remembered mode counts):
TaskToolWhy
Codified CI keyboard tests, visual regression, axe-core scans, WCAG compliance suites
npx playwright test
with
.spec.js
files
Real keyboard events, CI-runnable, version-controlled, reproducible. Primary path — all mandatory rules below still apply.
Baseline sweep across a list of URLs — machine-readable axe-core evidence per page, no
.spec.js
authoring, not CI-embedded
references/baseline-url-scan.mjs
(in-repo reference script; peer deps
playwright
+
@axe-core/playwright
)
Sequential per-page axe scan + summary JSON for baseline/regression evidence across many pages in one run.
--census
adds DOM-census heuristics (empty paragraphs, autocomplete-absence, duplicate ids);
--alt-snapshot
writes a diffable per-page alt-text map. Detector output, not a conformance verdict — axe-detectable subset (plus heuristics) only. See "Baseline URL-list scan" below.
Interactive agent-driven reconnaissance: snapshot ARIA structure, navigate a SPA to reach the page under test, verify a fix in place, capture annotated screenshots, probe a disclosure/menu/modal without writing a test file
agent-browser
CLI (snapshot+ref pattern, persistent CDP daemon, real keyboard events)
One shell call per action, no test-file overhead, returns
@e1
-style refs that map directly to actions. See "Interactive reconnaissance with agent-browser" below.
Generate a test script from a prose spec ("test that this modal traps focus and Escape closes it")
/webwright:run
or
/webwright:craft
(Claude Code plugin)
LLM generates complete Python Playwright script. Review before trusting. Also captures
aria_snapshot()
for deep ARIA tree inspection. See "Test script generation with Webwright" below.
Goal-driven journey audit of a live URL — "can a keyboard-only or screen-reader user complete this task?" — with evidence artifacts
keyboard-a11y-tester
(external clone, pinned release
0.5.0
; deterministic runner + agent-driven serve/step loop)
URL + goal in, evidence-linked WCAG findings out — no test file needed. Emulated screen-reader announcements, live-region capture, and focus-indicator measurement at the page/journey level that no other mode provides. See "Goal-driven journey audits with keyboard-a11y-tester" below.
Component/unit-level screen-reader assertions — accessible names, reading order, live-region announcements — in the project's own test suite (Vitest/Jest+jsdom, Storybook play functions, or a browser page), no URL or journey needed
@guidepup/virtual-screen-reader
(npm devDependency, exact-pinned
0.32.1
)
Per-component, per-PR spoken-output evidence in milliseconds — the implement→test layer keyboard-a11y-tester can't reach (it needs a deployed URL). Synthetic interactions: never keyboard-operability evidence. See "Component screen-reader assertions with virtual-screen-reader" below.
Visual inspection, DOM queries from a conversational session
agent-browser screenshot
/
agent-browser screenshot --annotate
/
agent-browser snapshot
Same daemon, no test runner needed.
Anything requiring real keyboard event delivery through an MCP wrapperDO NOT USE Playwright MCP. Its
browser_press_key
calls are silently dropped for most interactive widgets. Use
npx playwright test
or
agent-browser
instead.
Decision flowchart:
Do you have a prose description of what to test, but no test script yet?
  YES → /webwright:run (one-shot) or /webwright:craft (reusable parameterized tool)
  NO, you need to run an existing test → npx playwright test
  NO, you have a list of URLs and need machine-readable axe evidence across all of them, no test file → references/baseline-url-scan.mjs (or pa11y-ci --sitemap for sitemap-wide sweeps)
  NO, you need to audit a live URL against a user goal (journey, announcements, focus indicators) → keyboard-a11y-tester
  NO, you need to assert component announcements, names, or reading order in unit tests (no URL yet) → virtual-screen-reader
  NO, you need to explore interactively → agent-browser
CDP keyboard event delivery for
agent-browser
has been verified end-to-end
on both a vanilla JS disclosure widget (WAI-ARIA APG disclosure-faq example:
focus → press Enter → aria-expanded: false → true
) and a React state-driven modal (react.dev DocSearch:
Meta+K
→ React global keydown listener → state-mounted searchbox). The MCP keyboard delivery bug does not apply to
agent-browser
because it calls CDP
Input.dispatchKeyEvent
directly rather than through an MCP wrapper.
在运行任何测试前,请从路由表中选择合适的执行模式(表格是权威依据——不要依赖记忆中的模式):
任务工具原因
编码化的CI键盘测试、视觉回归测试、axe-core扫描、WCAG合规套件搭配
.spec.js
文件使用
npx playwright test
真实键盘事件、可在CI中运行、版本可控、可复现。主要路径——以下所有强制规则均适用。
对URL列表进行基线扫描——每页生成机器可读的axe-core证据,无需编写
.spec.js
,不嵌入CI
references/baseline-url-scan.mjs
(仓库内参考脚本;依赖
playwright
+
@axe-core/playwright
按顺序对每页进行axe扫描,并生成汇总JSON,用于一次运行中对多个页面进行基线/回归证据收集。
--census
参数添加DOM统计启发式规则(空段落、缺少自动补全、重复ID);
--alt-snapshot
参数生成可对比的每页替代文本映射。仅输出检测结果,而非合规判定——仅覆盖axe可检测的子集(含启发式规则)。请参阅下方“基线URL列表扫描”部分。
交互式代理驱动的侦察:快照ARIA结构、导航SPA以到达待测页面、验证修复效果、捕获带注释的截图、无需编写测试文件即可探查披露/菜单/模态框
agent-browser
CLI(快照+引用模式、持久化CDP守护进程、真实键盘事件)
每个操作只需一次shell调用,无需测试文件开销,返回直接映射到操作的
@e1
样式引用。请参阅下方“使用agent-browser进行交互式侦察”部分。
根据 prose 规范生成测试脚本(例如“测试此模态框是否捕获焦点并可通过Escape关闭”)
/webwright:run
/webwright:craft
(Claude Code插件)
LLM生成完整的Python Playwright脚本。使用前需进行审核。还会捕获
aria_snapshot()
用于深度ARIA树检查。请参阅下方“使用Webwright生成测试脚本”部分。
对实时URL进行目标驱动的旅程审核——“仅使用键盘或屏幕阅读器的用户能否完成此任务?”——并生成证据工件
keyboard-a11y-tester
(外部克隆,固定版本
0.5.0
;确定性运行器+代理驱动的服务/步骤循环)
输入URL和目标,输出关联证据的WCAG发现结果——无需测试文件。提供其他模式无法实现的页面/旅程级别的模拟屏幕阅读器公告、实时区域捕获和焦点指示器测量。请参阅下方“使用keyboard-a11y-tester进行目标驱动的旅程审核”部分。
在项目自身的测试套件(Vitest/Jest+jsdom、Storybook play函数或浏览器页面)中进行组件/单元级别的屏幕阅读器断言——可访问名称、阅读顺序、实时区域公告——无需URL或旅程
@guidepup/virtual-screen-reader
(npm开发依赖,精确固定版本
0.32.1
每个PR中可在毫秒级内获取组件级别的语音输出证据——这是keyboard-a11y-tester无法覆盖的实现→测试层(它需要已部署的URL)。仅支持合成交互:不提供键盘可操作性证据。请参阅下方“使用virtual-screen-reader进行组件屏幕阅读器断言”部分。
会话中的视觉检查、DOM查询
agent-browser screenshot
/
agent-browser screenshot --annotate
/
agent-browser snapshot
相同的守护进程,无需测试运行器。
需要通过MCP包装器传递真实键盘事件的任何场景请勿使用Playwright MCP。其
browser_press_key
调用对于大多数交互式小部件会被静默丢弃。请改用
npx playwright test
agent-browser
决策流程图:
你是否有测试内容的 prose 描述,但还没有测试脚本?
  是 → /webwright:run(一次性)或/webwright:craft(可复用的参数化工具)
  否,你需要运行现有测试 → npx playwright test
  否,你有URL列表并需要所有页面的机器可读axe证据,无需测试文件 → references/baseline-url-scan.mjs(或使用pa11y-ci --sitemap进行站点地图范围的扫描)
  否,你需要根据用户目标(旅程、公告、焦点指示器)审核实时URL → keyboard-a11y-tester
  否,你需要在单元测试中断言组件公告、名称或阅读顺序(还没有URL) → virtual-screen-reader
  否,你需要进行交互式探索 → agent-browser
已端到端验证
agent-browser
的CDP键盘事件传递
,包括纯JS披露小部件(WAI-ARIA APG披露FAQ示例:
focus → press Enter → aria-expanded: false → true
)和React状态驱动的模态框(react.dev DocSearch:
Meta+K
→ React全局按键监听器 → 状态挂载的搜索框)。MCP键盘传递不适用于
agent-browser
,因为它直接调用CDP
Input.dispatchKeyEvent
,而非通过MCP包装器。

Verification evidence contract

验证证据契约

Evidence type must match the failing condition. A screenshot is never evidence for an interaction-class fix (keyboard operability, focus behavior, or a status-message announcement) — it shows what a sighted mouse user sees, not what a keyboard or screen-reader user experiences. When a fix's evidence doesn't match its defect class, the fix ships labeled partial, naming which defect classes still lack matching evidence.
Defect classEvidence REQUIRED before "verified"Mode
Keyboard operability (reachable, operable with Tab/Enter/Space/Escape/arrows)Real-keyboard Playwright transcript — actual
page.keyboard.press()
calls, never ARIA-attribute inspection alone
npx playwright test
Focus order & focus-visible sufficiencyJourney-level focus trace evidence
keyboard-a11y-tester
Accessible name/role/state; status-message announcementsAssertion output against actual computed screen-reader output
virtual-screen-reader
Machine-detectable semantics, contrast, alt-presence (a rule fires or stops firing)Re-scan of the touched page(s) after the fix
baseline-url-scan.mjs
(axe-core violations;
--census
/
--alt-snapshot
for the heuristic classes)
Visual-only classes (layout, spacing, color/swatch correctness)Screenshot comparisonscreenshots (
agent-browser screenshot
/ Playwright screenshot)
This table is what
a11y-critic
Phase 0 checks a remediation's attached evidence against, and what
bug-reporting
's "Verification evidence" field cites.
证据类型必须与失败条件匹配。截图永远不能作为交互类修复(键盘可操作性、焦点行为或状态消息公告)的证据——它展示的是视力正常的鼠标用户看到的内容,而非键盘或屏幕阅读器用户的体验。当修复的证据与其缺陷类型不匹配时,修复将被标记为部分完成,并指出哪些缺陷类型仍缺乏匹配的证据。
缺陷类型标记为“已验证”前所需的证据模式
键盘可操作性(可访问、可通过Tab/Enter/Space/Escape/箭头键操作)真实键盘Playwright记录——实际的
page.keyboard.press()
调用,绝不能仅依赖ARIA属性检查
npx playwright test
焦点顺序和焦点可见性充分性旅程级别的焦点跟踪证据
keyboard-a11y-tester
可访问名称/角色/状态;状态消息公告针对实际计算的屏幕阅读器输出的断言结果
virtual-screen-reader
机器可检测的语义、对比度、替代文本存在性(规则触发或停止触发)修复后对受影响页面的重新扫描
baseline-url-scan.mjs
(axe-core违规;
--census
/
--alt-snapshot
用于启发式类别)
仅视觉类(布局、间距、颜色/样本正确性)截图对比截图(
agent-browser screenshot
/ Playwright截图)
此表格是
a11y-critic
第0阶段检查修复附带证据的依据,也是
bug-reporting
的“验证证据”字段引用的内容。

Interactive reconnaissance with agent-browser

使用agent-browser进行交互式侦察

For ad-hoc a11y probing inside a conversational session — before writing a
.spec.js
file, when verifying a single fix, or when exploring the ARIA structure of an unfamiliar component — use
agent-browser
. The snapshot+ref pattern eliminates locator hunting:
bash
agent-browser open https://example.com/component-under-test
agent-browser snapshot -i                    # Returns interactive elements with refs: [ref=e1], [ref=e2]...
agent-browser focus @e1                      # Focus by ref
agent-browser press Enter                    # Real CDP keyboard event
agent-browser get attr @e1 aria-expanded     # Verify state mutation
agent-browser screenshot --annotate          # Numbered overlays mapping to refs (useful for multimodal review)
agent-browser close
Key flags:
--profile Default
(reuse the user's Chrome login state for authenticated sites),
--session <name>
(isolated browser per parallel agent),
--json
(parseable output for programmatic checks),
--allowed-domains
(safety).
Keyboard-driving discipline (applies to all interactive modes, this one included): never send a pre-counted sequence of Tabs. Snapshot/observe, then act on what is actually focused — "Tab until the focused control is named X" is right; "Tab 6 times" is wrong. Confirm success by state change (attribute flip, URL change, announcement), not assumption.
When to escalate to
npx playwright test
: when the verification needs to live in CI, run across PR builds, or exercise the 12 APG widget pattern templates below. Reconnaissance with
agent-browser
is for interactive probing; codified regression still belongs in
.spec.js
files.
在会话中进行临时可访问性探查——编写
.spec.js
文件之前、验证单个修复时,或探索不熟悉组件的ARIA结构时——使用
agent-browser
。快照+引用模式无需查找定位器:
bash
agent-browser open https://example.com/component-under-test
agent-browser snapshot -i                    # 返回带引用的交互式元素:[ref=e1], [ref=e2]...
agent-browser focus @e1                      # 通过引用聚焦
agent-browser press Enter                    # 真实CDP键盘事件
agent-browser get attr @e1 aria-expanded     # 验证状态变更
agent-browser screenshot --annotate          # 带编号的覆盖层映射到引用(对多模态评审有用)
agent-browser close
关键标志:
--profile Default
(重用用户的Chrome登录状态以访问认证站点)、
--session <name>
(每个并行代理使用独立浏览器)、
--json
(可解析输出用于程序化检查)、
--allowed-domains
(安全控制)。
键盘操作准则(适用于所有交互模式,包括此模式): 绝不要发送预先计数的Tab序列。先快照/观察,然后对实际聚焦的元素执行操作——“按Tab直到聚焦控件名为X”是正确的;“按6次Tab”是错误的。通过状态变更(属性切换、URL变更、公告)确认成功,而非假设。
何时升级到
npx playwright test
当验证需要在CI中运行、跨PR构建执行,或需要使用以下12个APG小部件模式模板时。使用
agent-browser
进行侦察是为了交互式探查;编码化的回归测试仍应放在
.spec.js
文件中。

Test script generation with Webwright

使用Webwright生成测试脚本

When to use: You have a prose a11y requirement (from the planner or a ticket) and need a runnable test script, without hand-writing it.
What it produces: A Python Playwright script with navigation, keyboard interactions, ARIA state assertions, and screenshots. Webwright generates
sync_playwright
scripts by default — if you need async for an existing test harness, specify in the prompt.
Language mismatch warning: Webwright generates Python. Existing CI is Node.js/.spec.js. Generated scripts are starting points — for CI, port logic to .spec.js using the APG templates below, or run Python directly if a Python test runner is available.
Example
/webwright:run
(actual prompt that produced a passing dialog focus trap test in benchmark):
/webwright:run Navigate to https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/.
Open the modal by clicking the trigger button.
Verify focus moves into the modal.
Tab through all focusable elements and verify focus wraps (focus trap).
Press Escape and verify the modal closes and focus returns to the trigger.
Quality gate: The operator must review generated scripts before trusting results. Check that:
  • Keyboard events use
    page.keyboard.press()
    or
    locator.press()
    , NOT synthetic
    dispatchEvent
  • Assertions verify state changes (before/after), not just attribute presence
  • No
    time.sleep()
    > 5 seconds or hardcoded waits that mask timing issues
ARIA snapshot capability: Webwright's Playwright environment captures
page.locator("body").aria_snapshot()
— the full accessibility tree with roles, states, and relationships as structured YAML. Richer than
agent-browser snapshot -i
for structural analysis (captures all 4 tab→panel relationships via aria-controls/aria-labelledby cross-references, vs. agent-browser which shows only interactive element refs).
Limitations:
  • No built-in axe-core — the LLM must write injection code (it does this correctly; see benchmark task 3c)
  • May miss a11y-specific patterns unless the prompt is specific about what to check
  • Python scripts don't run in JS CI without a Python runner
  • Requires Claude Code plugin install — not available in Codex CLI
  • Do not run simultaneously with agent-browser — both launch Chrome instances that may conflict on ports
Benchmark results (2026-05-26): 25/25 across 5 WAI-ARIA APG tasks (dialog focus trap, tabs ARIA state, axe-core injection, menu keyboard navigation, ARIA tree inspection). All scripts used real
page.keyboard.press()
calls. Full results in
evals/suites/webwright-benchmark/
.
何时使用: 你有可访问性要求的 prose 描述(来自规划者或工单),需要可运行的测试脚本,而无需手动编写。
输出内容: 带有导航、键盘交互、ARIA状态断言和截图的Python Playwright脚本。Webwright默认生成
sync_playwright
脚本——如果现有测试 harness 需要异步版本,请在提示中指定。
语言不匹配警告: Webwright生成Python脚本。现有CI使用Node.js/.spec.js。生成的脚本只是起点——对于CI,需使用下方的APG模板将逻辑移植到.spec.js,或如果有Python测试运行器则直接运行Python脚本。
示例
/webwright:run
(在基准测试中生成通过的对话框焦点捕获测试的实际提示):
/webwright:run Navigate to https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/.
Open the modal by clicking the trigger button.
Verify focus moves into the modal.
Tab through all focusable elements and verify focus wraps (focus trap).
Press Escape and verify the modal closes and focus returns to the trigger.
质量门: 操作员必须在信任结果前审核生成的脚本。检查:
  • 键盘事件使用
    page.keyboard.press()
    locator.press()
    ,而非合成的
    dispatchEvent
  • 断言验证状态变更(前后对比),而非仅验证属性存在
  • 没有
    time.sleep()
    > 5秒或硬编码等待,以免掩盖时序问题
ARIA快照功能: Webwright的Playwright环境捕获
page.locator("body").aria_snapshot()
——包含角色、状态和关系的完整可访问性树,以结构化YAML格式呈现。对于结构分析,比
agent-browser snapshot -i
更丰富(通过aria-controls/aria-labelledby交叉引用捕获所有4个标签→面板关系,而agent-browser仅显示交互式元素引用)。
局限性:
  • 无内置axe-core——LLM必须编写注入代码(它能正确完成此操作;请参阅基准测试任务3c)
  • 除非提示明确说明要检查的内容,否则可能会遗漏特定的可访问性模式
  • Python脚本在JS CI中无法运行,除非有Python测试运行器
  • 需要安装Claude Code插件——在Codex CLI中不可用
  • 不要与agent-browser同时运行——两者都会启动Chrome实例,可能会在端口上冲突
基准测试结果(2026-05-26): 在5个WAI-ARIA APG任务(对话框焦点捕获、标签ARIA状态、axe-core注入、菜单键盘导航、ARIA树检查)中,25/25通过。所有脚本均使用真实的
page.keyboard.press()
调用。完整结果请查看
evals/suites/webwright-benchmark/

Installation

安装

Prerequisites: Python 3.10+, Playwright Python (
pip install playwright && playwright install chromium
)
Two-step install:
  1. /plugin marketplace add microsoft/Webwright
  2. /plugin install webwright@webwright
If marketplace fails:
git clone https://github.com/microsoft/Webwright && /plugin install ./Webwright
Platform note: Claude Code plugin only. Not available in Codex CLI. From Codex, the usable browser automation options are
agent-browser
and
keyboard-a11y-tester
(both plain CLIs). Generated
.py
scripts can be executed from Codex via
python3 script.py
.
先决条件: Python 3.10+、Playwright Python(
pip install playwright && playwright install chromium
两步安装:
  1. /plugin marketplace add microsoft/Webwright
  2. /plugin install webwright@webwright
如果市场安装失败:
git clone https://github.com/microsoft/Webwright && /plugin install ./Webwright
平台说明: 仅适用于Claude Code插件。在Codex CLI中不可用。在Codex中,可用的浏览器自动化选项是
agent-browser
keyboard-a11y-tester
(均为纯CLI工具)。生成的
.py
脚本可通过
python3 script.py
在Codex中执行。

Goal-driven journey audits with keyboard-a11y-tester

使用keyboard-a11y-tester进行目标驱动的旅程审核

When to use: you have a live URL and a task in plain words ("can a keyboard-only or screen-reader user complete X?") and need evidence-linked WCAG findings without writing a test file — discovery audits, before/after patch evidence, whole-journey reviews. When NOT to use: widget CI regression (→
.spec.js
+ the APG templates), quick probing or authenticated Chrome-profile flows (→
agent-browser
), rule scans (→ axe-core, §4).
What it is: ezufelt/keyboard-a11y-tester — an external tool, adopted at release
0.5.0
(commit
7e852a7
, MIT; originally adopted at
97eb13e
, bumped 2026-07-11 after upstream merged our PR #7 and began tagging releases — re-verify on every upgrade). Two layers: a deterministic Playwright/CDP runner (real keyboard events only, never
.click()
; machine-decidable WCAG checks; dual-signal focus-indicator measurement) and an emulated screen-reader persona (
@guidepup/virtual-screen-reader
: announcement capture, live-region monitoring, reading-order census). Runs both W3C personas (keyboard "Ade", screen-reader "Lakshmi") in one pass. As of 0.5.0 it also detects broken ARIA ID references and keyboard-focusable controls missing from the accessibility tree, includes our 3.3.2 UA-default-name check, and supports authenticated runs via
--storage-state <playwright-storageState.json>
(agent-browser remains the route when you want to reuse the user's real Chrome profile instead of exporting state). Cross-validated against this repo's 33 critic fixtures on 2026-07-10 — agreement record:
evals/results/keyboard-a11y-tester/README.md
.
何时使用: 你有实时URL和用普通语言描述的任务(“仅使用键盘或屏幕阅读器的用户能否完成X?”),需要关联证据的WCAG发现结果,而无需编写测试文件——发现审核、补丁前后证据、全旅程评审。何时不使用: 小部件CI回归(→
.spec.js
+ APG模板)、快速探查或认证Chrome配置文件流程(→
agent-browser
)、规则扫描(→ axe-core,第4节)。
工具介绍: ezufelt/keyboard-a11y-tester——外部工具,采用版本
0.5.0
(提交
7e852a7
,MIT协议;最初采用版本
97eb13e
,2026-07-11上游合并我们的PR #7并开始标记版本后升级——每次升级时重新验证)。包含两层:确定性Playwright/CDP运行器(仅使用真实键盘事件,绝不使用
.click()
;机器可判定的WCAG检查;双信号焦点指示器测量)和模拟屏幕阅读器角色(
@guidepup/virtual-screen-reader
:公告捕获、实时区域监控、阅读顺序统计)。一次运行即可执行两个W3C角色(键盘用户“Ade”、屏幕阅读器用户“Lakshmi”)。截至0.5.0版本,它还能检测损坏的ARIA ID引用和可键盘聚焦但未出现在可访问性树中的控件,包含我们的3.3.2 UA默认名称检查,并支持通过
--storage-state <playwright-storageState.json>
进行认证运行(当你想重用用户的真实Chrome配置文件而非导出状态时,仍需使用agent-browser)。2026-07-10已针对此仓库的33个批评 fixture 进行交叉验证——一致性记录:
evals/results/keyboard-a11y-tester/README.md

Install (clone path — verified; Node ≥ 20)

安装(克隆路径——已验证;Node ≥ 20)

bash
git clone https://github.com/ezufelt/keyboard-a11y-tester && cd keyboard-a11y-tester
git checkout 0.5.0                    # adopted pin (tagged release)
npm install && node scripts/setup-check.mjs   # npx playwright install chromium only if browser_available=false
The upstream Claude Code plugin flow (
/plugin marketplace add ezufelt/keyboard-a11y-tester
) exists but is unverified here; the clone path works from both Claude Code and Codex.
bash
git clone https://github.com/ezufelt/keyboard-a11y-tester && cd keyboard-a11y-tester
git checkout 0.5.0                    # 采用固定版本(标记的发布版)
npm install && node scripts/setup-check.mjs   # 仅当browser_available=false时运行npx playwright install chromium
上游Claude Code插件流程(
/plugin marketplace add ezufelt/keyboard-a11y-tester
)存在,但此处未验证;克隆路径在Claude Code和Codex中均可使用。

Run

运行

bash
undefined
bash
undefined

Batch blind Tab-crawl (unattended; never presses Enter/Space) — per viewport:

批量盲Tab爬取(无人值守;绝不按Enter/Space)——按视口:

node scripts/runner.mjs --url https://site --viewport desktop --max-steps 40 --out <dir>
node scripts/runner.mjs --url https://site --viewport desktop --max-steps 40 --out <dir>

Driven session (the value center — the agent decides every keystroke):

驱动会话(核心价值——代理决定每次按键):

node scripts/runner.mjs serve --url https://site --goal "find and submit the contact form"
--viewport desktop --port 9333 # default port is 9333 (9400 in upstream examples is just an example)
node scripts/runner.mjs serve --url https://site --goal "find and submit the contact form" \ --viewport desktop --port 9333 # 默认端口为9333(上游示例中的9400仅为示例)

prints: READY <session-dir>

输出:READY <session-dir>

node scripts/runner.mjs observe <session-dir> node scripts/runner.mjs step <session-dir> --press Tab # one key → AX name/role/state, focus style, sr_announcement node scripts/runner.mjs step <session-dir> --type "hello@example.com" node scripts/runner.mjs finish <session-dir> && node scripts/runner.mjs stop <session-dir>

**Core discipline: observe → decide → act** (see the keyboard-driving rule in the agent-browser section — it originated here). Read `sr_announcement.live_announcements` after any action that visibly changes the page: an entry proves the update reaches a screen reader; its absence after a visible change is 4.1.3 failure evidence.
node scripts/runner.mjs observe <session-dir> node scripts/runner.mjs step <session-dir> --press Tab # 一次按键 → AX名称/角色/状态、焦点样式、sr_announcement node scripts/runner.mjs step <session-dir> --type "hello@example.com" node scripts/runner.mjs finish <session-dir> && node scripts/runner.mjs stop <session-dir>

**核心准则:观察→决策→行动**(请参阅agent-browser部分的键盘操作规则——起源于此)。在任何可见改变页面的操作后,读取`sr_announcement.live_announcements`:条目证明更新已到达屏幕阅读器;可见改变后无条目即为4.1.3失败证据。

Artifacts (temp dir, or
--out
)

工件(临时目录或
--out
指定目录)

  • trace.json
    — per step: keystroke, selector, CDP AX name/role/state, computed focus style,
    focus_moved
    , screenshot ref,
    sr_announcement
  • deterministic-findings.json
    {wcag, persona, conformance_level, confidence, severity, url, locations, persona_impact, evidence[]}
  • screen-reader-census.json
    — whole-page reading order (spoken phrase, role, selector) + declared live regions
  • screenshots/step_NNNN.png
    — focused-region crops
These are measured test evidence for a11y-critic reviews (formal Phase 0 tier wiring lands with assessment Phase 3).
  • trace.json
    ——每一步:按键、选择器、CDP AX名称/角色/状态、计算的焦点样式、
    focus_moved
    、截图引用、
    sr_announcement
  • deterministic-findings.json
    ——
    {wcag, persona, conformance_level, confidence, severity, url, locations, persona_impact, evidence[]}
  • screen-reader-census.json
    ——全页面阅读顺序(语音短语、角色、选择器)+ 声明的实时区域
  • screenshots/step_NNNN.png
    ——聚焦区域裁剪图
这些是a11y-critic评审的可测量测试证据(正式第0层 wiring 随评估第3阶段落地)。

Calibration rules (measured on our 33 fixtures, 2026-07-10)

校准规则(2026-07-10基于我们的33个fixture测量)

  1. Batch-mode 4.1.3 "silent live region" findings are never failure evidence. A blind crawl never operates anything, so correctly-wired regions look silent (confidence 0.35–0.4 vs 0.7+ elsewhere). They are prompts to run a driven session and judge from
    live_announcements
    .
  2. UA-intrinsic names mask missing labels. An unlabeled
    <input type=file>
    reports AX name "Choose File", so the unnamed-control check stays quiet. Label association still needs axe/static/judgment review.
  3. Component-scale pages ≠ full pages. Skip-link (2.4.1) and landmark findings assume a whole page; on component targets treat them as granularity artifacts.
  4. AA vs AAA honesty. 2.4.13 focus-appearance findings are AAA-informative by design — never report them as 2.4.7 failures. The AAA pixel measurement is also rendering-environment-sensitive (macOS locally can emit AAA-informative findings that Linux CI does not, observed at both
    97eb13e
    and
    0.5.0
    ) — one more reason never to gate on it.
  5. Emulated SR ≠ real AT. Findings are spec-compliant-announcement evidence; the §6 manual NVDA/VoiceOver protocol still applies before shipping.
  6. conformance_level
    is the check's gate, not the SC's WCAG level
    (code-read at
    0.5.0
    , not fixture-measured — upstream #27, filed 2026-08-04)
    . Only the 2.4.13 check emits
    AAA
    ; every other finding falls through
    level || 'AA'
    to
    "AA"
    , mislabeling the nine Level A SCs the checks cover (1.1.1, 1.3.1, 1.4.1, 2.1.2, 2.4.1, 2.4.3, 3.2.1, 3.3.2, 4.1.2) — 2.4.7 and 4.1.3 are coincidentally correct. Read the field only as pass-fail (
    AA
    ) vs informative (
    AAA
    ) and derive the SC's true level from the SC number. Delete this rule when the pin advances past a fix for #27.
  1. 批量模式下4.1.3“静默实时区域”发现结果永远不能作为失败证据。盲爬永远不会操作任何内容,因此正确配置的区域看起来是静默的(置信度0.35–0.4,而其他地方为0.7+)。它们提示需要运行驱动会话,并根据
    live_announcements
    进行判断。
  2. UA固有名称掩盖缺失的标签。未标记的
    <input type=file>
    会报告AX名称“Choose File”,因此未命名控件检查不会触发。标签关联仍需axe/静态/判断评审。
  3. 组件级页面≠完整页面。跳过链接(2.4.1)和地标发现结果假设是完整页面;对于组件目标,将其视为粒度工件。
  4. AA与AAA诚实性。2.4.13焦点外观发现结果设计为AAA信息性——永远不要将其报告为2.4.7失败。AAA像素测量也对渲染环境敏感(本地macOS可能会发出Linux CI不会发出的AAA信息性发现结果,在
    97eb13e
    0.5.0
    版本均已观察到)——这是永远不要依赖它的另一个原因。
  5. 模拟SR≠真实AT。发现结果是符合规范的公告证据;在发布前仍需遵循第6节的手动NVDA/VoiceOver协议。
  6. conformance_level
    是检查的门槛,而非SC的WCAG级别
    (在
    0.5.0
    版本中读取代码,未通过fixture测量——上游#27,2026-08-04提交)。仅2.4.13检查会发出
    AAA
    ;其他所有发现结果均通过
    level || 'AA'
    默认为
    "AA"
    ,错误标记了检查覆盖的9个A级SC(1.1.1、1.3.1、1.4.1、2.1.2、2.4.1、2.4.3、3.2.1、3.3.2、4.1.2)——2.4.7和4.1.3是巧合正确。仅将此字段视为通过/失败(
    AA
    )与信息性(
    AAA
    ),并从SC编号推导SC的真实级别。当固定版本升级到修复#27的版本后,删除此规则。

Mapping findings → A11y Evidence Finding Contract

发现结果→可访问性证据发现契约映射

  • severity:
    serious
    → MAJOR (CRITICAL if it blocks the goal);
    moderate
    → MINOR or MAJOR by user impact;
    minor
    /AAA-informative → ENHANCEMENT
  • fingerprint: derive from selector + wcag + check kind (the tool's
    id
    embeds the viewport and is run-scoped — don't use it)
  • persona → perspective_alarms:
    keyboard
    keyboard_motor
    ;
    screen-reader
    screen_reader_semantic
  • evidence: cite step ids + measured values (e.g.
    trace.json step_0003: outline 3px solid; AAA contrast 2.34
    )
  • 严重程度:
    serious
    → MAJOR(如果阻止目标则为CRITICAL);
    moderate
    → 根据用户影响为MINOR或MAJOR;
    minor
    /AAA信息性 → ENHANCEMENT
  • 指纹:从选择器+wcag+检查类型派生(工具的
    id
    嵌入了视口且是运行范围的——不要使用它)
  • 角色→视角警报:
    keyboard
    keyboard_motor
    screen-reader
    screen_reader_semantic
  • 证据:引用步骤ID+测量值(例如
    trace.json step_0003: outline 3px solid; AAA contrast 2.34

Cautions

注意事项

  • Client production sites: on pages with a CAPTCHA the runner suppresses
    navigator.webdriver
    (page-scoped) so the CAPTCHA can initialize — automation-signal spoofing that can trip a WAF or security review. Get explicit client sign-off before pointing it at client production infrastructure; prefer staging.
  • Don't run concurrently with agent-browser or Webwright sessions (Chrome instance/port contention). Chromium-only — cross-browser coverage stays with
    npx playwright test
    .
  • Run desktop and mobile viewports separately; navigation often collapses behind a disclosure on mobile.
  • 客户生产站点: 在带有CAPTCHA的页面上,运行器会抑制
    navigator.webdriver
    (页面范围),以便CAPTCHA可以初始化——这种自动化信号伪造可能会触发WAF或安全审查。在指向客户生产基础设施之前,请获得明确的客户签署同意;优先使用 staging 环境。
  • 不要与agent-browser或Webwright会话同时运行(Chrome实例/端口冲突)。仅支持Chromium——跨浏览器覆盖仍需使用
    npx playwright test
  • 分别运行桌面和移动视口;导航在移动设备上通常会折叠在披露控件后面。

Component screen-reader assertions with virtual-screen-reader

使用virtual-screen-reader进行组件屏幕阅读器断言

When to use: you're implementing or fixing a component and need to assert what a screen reader computes and announces — accessible names, reading order, live-region announcements — in the project's own test suite, per PR, with no URL, journey, or deployed page. This is the
implement → test
layer: the cheapest point to catch the toast/async-status defect class. When NOT to use: keyboard operability (its interactions are synthetic
user-event
events — real-key evidence stays with
.spec.js
, agent-browser, or keyboard-a11y-tester), anything visual (jsdom has no layout), page/journey audits (→ keyboard-a11y-tester), rule scans (→ axe-core, §4), open-shadow-DOM components (invisible to it — upstream #182),
aria-busy
states (unsupported — upstream #36).
What it is: guidepup/virtual-screen-reader — a screen-reader simulator as a library (MIT; adopted at npm
0.32.1
, exact-pinned). Walks the accessibility tree of any DOM, emits spoken phrases (
"button, Save document"
), captures live-region announcements with politeness prefixes (
"polite: Draft saved"
), and exposes SR quick-key emulation via
virtual.perform(virtual.commands.*)
moveToNextHeading
,
moveToNextLandmark
, per-level heading jumps,
jumpToErrorMessageElement
(aria-errormessage), aria-flowto reading-order commands. Spec-anchored (ACCNAME 1.2, CORE-AAM, HTML-AAM, WAI-ARIA 1.2) and WPT-tested upstream. It is already this stack's transitive SR engine inside keyboard-a11y-tester; this lane uses it directly. Validated in-repo 2026-07-11 in three environments — plain Node+jsdom, Vitest 4 jsdom environment, real Chromium via its ESM build — plus the Storybook lane 2026-07-13 (10.4.6 play functions via
@storybook/addon-vitest
browser mode, 12/12):
docs/virtual-screen-reader-adoption-assessment.md
. Jest+jsdom is expected to match Vitest but was not run — treat it as unvalidated until someone runs it.
何时使用: 你正在实现或修复组件,需要断言屏幕阅读器计算和公告的内容——可访问名称、阅读顺序、实时区域公告——在项目自身的测试套件中,每个PR都要进行,无需URL、旅程或已部署页面。这是
实现→测试
层:捕获toast/异步状态缺陷类别的最低成本点。何时不使用: 键盘可操作性(其交互是合成的
user-event
事件——真实键盘证据仍需使用
.spec.js
、agent-browser或keyboard-a11y-tester)、任何视觉内容(jsdom无布局)、页面/旅程审核(→ keyboard-a11y-tester)、规则扫描(→ axe-core,第4节)、开放阴影DOM组件(对其不可见——上游#182)、
aria-busy
状态(不支持——上游#36)。
工具介绍: guidepup/virtual-screen-reader——作为库的屏幕阅读器模拟器(MIT协议;采用npm版本
0.32.1
,精确固定)。遍历任何DOM的可访问性树,发出语音短语(
"button, Save document"
),捕获带礼貌前缀的实时区域公告(
"polite: Draft saved"
),并通过
virtual.perform(virtual.commands.*)
暴露SR快捷键模拟——
moveToNextHeading
moveToNextLandmark
、按级别标题跳转、
jumpToErrorMessageElement
(aria-errormessage)、aria-flowto阅读顺序命令。基于规范(ACCNAME 1.2、CORE-AAM、HTML-AAM、WAI-ARIA 1.2),并在上游进行WPT测试。它已经是此堆栈中keyboard-a11y-tester内部的传递SR引擎;此路径直接使用它。2026-07-11已在三个环境中进行仓库内验证——纯Node+jsdom、Vitest 4 jsdom环境、通过其ESM构建的真实Chromium——加上2026-07-13的Storybook路径(10.4.6 play函数通过
@storybook/addon-vitest
浏览器模式,12/12通过):
docs/virtual-screen-reader-adoption-assessment.md
。Jest+jsdom预计与Vitest匹配,但未运行——在有人运行之前,视为未验证。

Install (Node ≥ 20)

安装(Node ≥ 20)

bash
npm install --save-dev @guidepup/virtual-screen-reader@0.32.1   # exact pin; re-verify calibration rules on any bump
bash
npm install --save-dev @guidepup/virtual-screen-reader@0.32.1   # 精确固定版本;任何版本升级时重新验证校准规则

Core patterns (Vitest, jsdom environment — the verified harness)

核心模式(Vitest,jsdom环境——已验证的harness)

js
import { afterEach, expect, test } from "vitest";
import { virtual } from "@guidepup/virtual-screen-reader";

afterEach(async () => {
  await virtual.stop().catch(() => {});   // stateful singleton — mandatory, or phrase logs leak across tests
  document.body.innerHTML = "";
});

// 1. Announcement assertion — persistent-container pattern (the only reliable shape; see rule 3)
test("saving announces to screen reader users", async () => {
  document.body.innerHTML = `
    <main><button>Save</button></main>
    <div role="status" id="app-status"></div>  <!-- persistent, empty at mount -->
  `;
  await virtual.start({ container: document.body });
  document.getElementById("app-status").textContent = "Item saved";
  await Promise.resolve();   // microtask flush suffices (measured) — no arbitrary sleep needed
  expect(await virtual.spokenPhraseLog()).toContain("polite: Item saved");
});

// 2. Reading-order / name assertions — bounded walk (never while-true; see rule 1)
test("reads the expected sequence", async () => {
  document.body.innerHTML = `<h2>Cart</h2><p>$29.00</p><button>Buy now</button>`;
  await virtual.start({ container: document.body });
  const phrases = [];
  for (let i = 0; i < 40; i++) {           // max-step guard: aria-modal traps the cursor by design
    await virtual.next();
    const p = await virtual.lastSpokenPhrase();
    phrases.push(p);
    if (p === "end of document") break;
  }
  expect(phrases.indexOf("$29.00")).toBeLessThan(phrases.indexOf("button, Buy now"));
});
js
import { afterEach, expect, test } from "vitest";
import { virtual } from "@guidepup/virtual-screen-reader";

afterEach(async () => {
  await virtual.stop().catch(() => {});   // 有状态的单例——必须执行,否则短语日志会在测试间泄漏
  document.body.innerHTML = "";
});

// 1. 公告断言——持久容器模式(唯一可靠的形状;请参阅规则3)
test("saving announces to screen reader users", async () => {
  document.body.innerHTML = `
    <main><button>Save</button></main>
    <div role="status" id="app-status"></div>  <!-- 持久化,挂载时为空 -->
  `;
  await virtual.start({ container: document.body });
  document.getElementById("app-status").textContent = "Item saved";
  await Promise.resolve();   // 微任务刷新足够(已测量)——无需任意睡眠
  expect(await virtual.spokenPhraseLog()).toContain("polite: Item saved");
});

// 2. 阅读顺序/名称断言——有限遍历(绝不要while-true;请参阅规则1)
test("reads the expected sequence", async () => {
  document.body.innerHTML = `<h2>Cart</h2><p>$29.00</p><button>Buy now</button>`;
  await virtual.start({ container: document.body });
  const phrases = [];
  for (let i = 0; i < 40; i++) {           // 最大步骤保护:aria-modal按设计捕获光标
    await virtual.next();
    const p = await virtual.lastSpokenPhrase();
    phrases.push(p);
    if (p === "end of document") break;
  }
  expect(phrases.indexOf("$29.00")).toBeLessThan(phrases.indexOf("button, Buy now"));
});

Storybook stories as SR tests (verified 2026-07-13: Storybook 10.4.6 +
@storybook/addon-vitest
browser mode, Chromium)

将Storybook故事作为SR测试(2026-07-13已验证:Storybook 10.4.6 +
@storybook/addon-vitest
浏览器模式,Chromium)

Component libraries that live in Storybook can make announcement assertions part of the stories themselves — CI-shaped via
npx vitest run --project=storybook
, and visible in the dev-mode Interactions panel:
js
import { expect, userEvent, waitFor } from "storybook/test";
import { virtual } from "@guidepup/virtual-screen-reader";

export const AnnouncesOnSave = {
  play: async ({ canvasElement, canvas }) => {
    await virtual.start({ container: canvasElement });   // scope to the story canvas
    try {
      await userEvent.click(await canvas.findByRole("button", { name: "Save order" }));
      await waitFor(async () =>
        expect(await virtual.spokenPhraseLog()).toContain("polite: Item saved"));
    } finally {
      await virtual.stop();   // mandatory — measured: the phrase log SURVIVES a missing stop and bleeds into the next story (start() itself recovers; the leak is the hazard)
    }
  },
};
Calibration rules 1–5 below apply unchanged in this lane (rule 3 re-verified in it). Upstream's own Storybook example omits the
finally
— don't copy that shape. Storybook 8
@storybook/test-runner
setups are expected to work but were not run here. Validation record:
evals/results/virtual-screen-reader/harness/storybook/
.
位于Storybook中的组件库可将公告断言作为故事本身的一部分——通过
npx vitest run --project=storybook
进行CI化,并在开发模式的Interactions面板中可见:
js
import { expect, userEvent, waitFor } from "storybook/test";
import { virtual } from "@guidepup/virtual-screen-reader";

export const AnnouncesOnSave = {
  play: async ({ canvasElement, canvas }) => {
    await virtual.start({ container: canvasElement });   // 作用域为故事画布
    try {
      await userEvent.click(await canvas.findByRole("button", { name: "Save order" }));
      await waitFor(async () =>
        expect(await virtual.spokenPhraseLog()).toContain("polite: Item saved"));
    } finally {
      await virtual.stop();   // 必须执行——已测量:短语日志会在缺少stop时存活并泄漏到下一个故事(start()本身会恢复;泄漏是风险)
    }
  },
};
下方的校准规则1–5在此路径中不变(规则3已重新验证)。上游自己的Storybook示例省略了
finally
——不要复制该形状。Storybook 8
@storybook/test-runner
设置预计可用,但此处未运行。验证记录:
evals/results/virtual-screen-reader/harness/storybook/

Calibration rules (measured 2026-07-11 at 0.32.1; environments noted)

校准规则(2026-07-11在0.32.1版本测量;注明环境)

  1. Never walk-to-end-of-document when
    aria-modal="true"
    is present
    (jsdom). The cursor traps inside the modal by design (upstream #54) — the walk never terminates. Scope
    container
    to the component and bound every walk with a max-step guard.
  2. A silent or short walk is not a clean pass — check for shadow roots first (jsdom). Open shadow DOM is invisible (upstream #182). If
    element.shadowRoot
    exists anywhere under the container, VSR evidence is partial: route to keyboard-a11y-tester or browser testing instead.
  3. Mount-with-content live regions read as silent — including correctly-fixed ones (jsdom AND Chromium — engine behavior, not a jsdom artifact). VSR announces mutations inside existing live regions (text changes, child insertions, mount-empty-then-fill) but not insertion of a pre-populated
    role="alert"
    element. Removal splits on
    aria-atomic
    (measured, fixture sweep): clearing a non-atomic region is silent; clearing an
    aria-atomic="true"
    region announces an empty
    "polite: "
    phrase — an empty polite entry is a region-clear marker, not noise. Interpretive context (domain knowledge, not probed here): real screen readers are also inconsistent on pre-populated alert insertion, which is why robust toast guidance uses a persistent container. Consequences: assert via the persistent-container pattern; a silent log after mount-with-content of an alert is inconclusive, not proof the fix failed; buggy-component silence is defect evidence only alongside the structural fact (no role/aria-live present).
  4. Fake timers wedge the singleton — hard incompatibility (Vitest; Jest unmeasured, mechanism harness-independent). Under fake timers,
    start()
    resolves but log reads hang forever, and the wedged state cascades hangs into every later test in the file, teardown included. Never enable fake timers in a file that runs VSR. Components needing fake timers (auto-dismiss toasts) get announcement assertions in a separate real-timer file — natural with the persistent-container pattern (assert the announcement on show; the timed dismiss is a separate concern).
  5. Cite the VSR version in every piece of evidence (
    vsr@0.32.1 <test file>
    ). Both consumption routes are frozen (this exact pin; keyboard-a11y-tester's committed lockfile), so skew arises only on deliberate upgrade — the citation makes it detectable. Re-verify rules 1–4 on any version bump.
  1. 当存在
    aria-modal="true"
    时,绝不要遍历到文档末尾
    (jsdom)。光标按设计捕获在模态框内(上游#54)——遍历永远不会终止。将
    container
    作用域限定为组件,并为每次遍历添加最大步骤保护。
  2. 静默或短遍历并不代表完全通过——首先检查阴影根(jsdom)。开放阴影DOM不可见(上游#182)。如果容器下存在
    element.shadowRoot
    ,则VSR证据是部分的:改用keyboard-a11y-tester或浏览器测试。
  3. 挂载时带内容的实时区域会被读取为静默——包括正确修复的区域(jsdom和Chromium——引擎行为,而非jsdom工件)。VSR会公告现有实时区域内的变更(文本更改、子元素插入、先挂载为空再填充),但不会公告预填充的
    role="alert"
    元素的插入。移除操作会根据
    aria-atomic
    拆分(已测量,fixture扫描):清除非原子区域是静默的;清除
    aria-atomic="true"
    区域会公告空的
    "polite: "
    短语——空的礼貌条目是区域清除标记,而非噪声。解释性上下文(领域知识,未在此探查):真实屏幕阅读器在预填充警报插入方面也不一致,这就是为什么可靠的toast指南使用持久容器。后果:通过持久容器模式进行断言;警报挂载时带内容后日志静默是不确定的,并非证明修复失败;只有当结构事实(无role/aria-live存在)时,组件静默才是缺陷证据。
  4. 假计时器会卡住单例——严重不兼容(Vitest;Jest未测量,机制与harness无关)。在假计时器下,
    start()
    会解析,但日志读取会永远挂起,卡住的状态会级联到文件中的每个后续测试,包括拆卸。永远不要在运行VSR的文件中启用假计时器。需要假计时器的组件(自动关闭的toast)在单独的真实计时器文件中进行公告断言——这与持久容器模式自然契合(在显示时断言公告;定时关闭是单独的问题)。
  5. 在每个证据中引用VSR版本
    vsr@0.32.1 <test file>
    )。两种使用路径均已冻结(此精确固定版本;keyboard-a11y-tester的已提交锁文件),因此偏差仅在故意升级时出现——引用可使其可检测。任何版本升级时重新验证规则1–4。

Evidence

证据

The artifacts are spoken-phrase logs plus the asserting test file — a11y-critic Phase 0 hard evidence (gate passed 2026-07-11:
evals/results/virtual-screen-reader/
; contract mapping in
docs/a11y-evidence-finding-contract.md
). Cite tool version + test file + the exact phrase or its absence, and pair silence with the structural fact (no role/aria-live present). Platform note: plain npm library — works from Claude Code and Codex.
工件是语音短语日志加上断言测试文件——a11y-critic第0阶段的硬证据(2026-07-11通过:
evals/results/virtual-screen-reader/
;契约映射在
docs/a11y-evidence-finding-contract.md
中)。引用工具版本+测试文件+确切短语或其缺失,并将静默与结构事实(无role/aria-live存在)配对。平台说明:纯npm库——在Claude Code和Codex中均可使用。

Baseline URL-list scan with references/baseline-url-scan.mjs

使用references/baseline-url-scan.mjs进行基线URL列表扫描

When to use: you have a list of URLs — a spot-check set, a sampled route list from a discovery or audit-scope engagement, a client's page inventory — and need machine-readable axe-core evidence across all of them in one run, without authoring a
.spec.js
file per page or wiring CI. Promoted 2026-08-14 from a one-off evidence harness written for the 2026-08-13 EPA public-sites engagement (the zivtech/a11y-audits repo (private),
2026-08-13-epa-public-sites/evidence/harness/audit-pages.mjs
, run against 40 views) into a reusable, generalized reference script: references/baseline-url-scan.mjs. When NOT to use: a single page or component you're actively developing against (→
.spec.js
+ the APG templates, or
agent-browser
for quick probing); keyboard operability or screen-reader announcement evidence — this mode never presses a key or captures an announcement (→ keyboard-a11y-tester or virtual-screen-reader); a sitemap-wide sweep where a maintained, hosted CI tool fits better than an in-repo script (→ pa11y-ci, below).
What it is: a plain Node script depending only on
playwright
and
@axe-core/playwright
— peer dependencies installed in your own project, never in this repo (this bundle stays prompt-only; see this repo's
CLAUDE.md
). It launches one Chromium browser, visits each URL sequentially with a polite delay between requests, scans with axe-core scoped to this bundle's WCAG 2.2 AA default tag set at each configured viewport — default
1280x800,320x800
, matching the EPA harness's desktop+narrow lineage, override with
--viewports WxH[,WxH...]
— and writes a per-URL JSON result keyed by viewport (rule id, impact, node count, up to 3 sample selectors per viewport) plus an aggregated
summary.json
(violation counts by impact, violations by rule across all pages and viewports). It deliberately drops the EPA harness's other engagement-specific extras — full structure inventory, ARIA snapshot, keyboard tab-trace, text-spacing reflow probe, per-node XPath — to stay a focused baseline tool; reach for
agent-browser
or
keyboard-a11y-tester
when you need those.
Two opt-in flags add non-axe signals, implemented in the sibling references/census.mjs:
  • --census
    — three DOM-census heuristics, reported under a
    census
    key on every viewport record, always separate from axe's
    violations
    /
    incomplete
    and always labeled detector heuristics, never a conformance verdict: empty paragraphs (no text, no element children), autocomplete-absence (inputs whose type/name/label suggest a WCAG 1.3.5 known purpose but carry no
    autocomplete
    attribute), and duplicate ids (an id used on more than one element).
  • --alt-snapshot
    — writes
    alt-snapshot.json
    : one entry per URL, each a sorted list of every
    img
    /
    svg[role=img]
    's selector,
    src_or_title
    , and
    alt
    . Captured once per page (first viewport only — alt text doesn't vary by viewport).
何时使用: 你有URL列表——抽查集、发现或审核范围参与中的采样路由列表、客户的页面清单——需要一次运行中所有页面的机器可读axe-core证据,无需为每个页面编写
.spec.js
文件或配置CI。2026-08-14从为2026-08-13 EPA公共站点参与编写的一次性证据harness(zivtech/a11y-audits仓库(私有),
2026-08-13-epa-public-sites/evidence/harness/audit-pages.mjs
,针对40个视图运行)升级为可复用的通用参考脚本:references/baseline-url-scan.mjs何时不使用: 你正在积极开发的单个页面或组件(→
.spec.js
+ APG模板,或使用
agent-browser
进行快速探查);键盘可操作性或屏幕阅读器公告证据——此模式绝不按键或捕获公告(→ keyboard-a11y-tester或virtual-screen-reader);站点地图范围的扫描,其中维护的托管CI工具比仓库内脚本更合适(→ pa11y-ci,下方)。
工具介绍: 纯Node脚本,仅依赖
playwright
@axe-core/playwright
——在你自己的项目中安装的对等依赖,绝不安装在此仓库中(此包仅保留提示;请参阅此仓库的
CLAUDE.md
)。它启动一个Chromium浏览器,按顺序访问每个URL,请求之间有礼貌的延迟,在每个配置的视口上使用axe-core扫描,范围为此包的WCAG 2.2 AA默认标签集——默认
1280x800,320x800
,匹配EPA harness的桌面+窄屏 lineage,可通过
--viewports WxH[,WxH...]
覆盖——并写入按视口键控的每个URL的JSON结果(规则ID、影响、节点计数、每个视口最多3个样本选择器)加上汇总的
summary.json
(按影响划分的违规计数、所有页面和视口的规则违规情况)。它故意删除了EPA harness的其他参与特定附加功能——完整结构清单、ARIA快照、键盘Tab跟踪、文本间距重流探查、每个节点XPath——以保持作为聚焦基线工具;当你需要这些功能时,请使用
agent-browser
keyboard-a11y-tester
两个可选标志添加非axe信号,在兄弟脚本references/census.mjs中实现:
  • --census
    ——三个DOM统计启发式规则,在每个视口记录的
    census
    键下报告,始终与axe的
    violations
    /
    incomplete
    分开,并始终标记为检测器启发式规则,而非合规判定:空段落(无文本、无元素子节点)、缺少自动补全(输入的类型/名称/标签表明WCAG 1.3.5已知用途,但无
    autocomplete
    属性)、重复ID(一个ID用于多个元素)。
  • --alt-snapshot
    ——写入
    alt-snapshot.json
    :每个URL一个条目,每个条目是每个
    img
    /
    svg[role=img]
    的选择器、
    src_or_title
    alt
    的排序列表。每个页面捕获一次(仅第一个视口——替代文本不随视口变化)。

Install and run

安装和运行

bash
npm install -D playwright @axe-core/playwright   # peer deps — install in your own project, never in this repo
npx playwright install chromium

node .claude/skills/a11y-test/references/baseline-url-scan.mjs --urls-file urls.txt --out ./baseline-scan-output
bash
npm install -D playwright @axe-core/playwright   # 对等依赖——在你自己的项目中安装,绝不安装在此仓库中
npx playwright install chromium

node .claude/skills/a11y-test/references/baseline-url-scan.mjs --urls-file urls.txt --out ./baseline-scan-output

or pass URLs directly, with a custom viewport list:

或直接传递URL,并使用自定义视口列表:

node .claude/skills/a11y-test/references/baseline-url-scan.mjs --out ./out --viewports 1280x800,320x800 https://example.com https://example.org/page
node .claude/skills/a11y-test/references/baseline-url-scan.mjs --out ./out --viewports 1280x800,320x800 https://example.com https://example.org/page

with the DOM-census heuristics and an alt-text snapshot:

启用DOM统计启发式规则和替代文本快照:

node .claude/skills/a11y-test/references/baseline-url-scan.mjs --out ./out --census --alt-snapshot https://example.com

`urls.txt`: one absolute `http(s)://` URL per line; blank lines and `#`-prefixed lines are ignored. Raise `--delay` (default 500ms) for rate-limited or robots-restricted targets — confirm you're authorized to test the target and check its robots.txt/terms of service before scanning a third party's production site. Exact-pin `@axe-core/playwright` in your own project's `package.json`, since rule availability is per-axe-core-version — the resolved version is recorded as `axe_core_version` in `summary.json` for exactly this reason.

**Catching silent alt-text regressions:** run with `--alt-snapshot` before and after a change, then diff the two `alt-snapshot.json` files —

```bash
node references/baseline-url-scan.mjs --out ./before --alt-snapshot https://example.com/page
node .claude/skills/a11y-test/references/baseline-url-scan.mjs --out ./out --census --alt-snapshot https://example.com

`urls.txt`:每行一个绝对`http(s)://`URL;空行和`#`前缀的行将被忽略。对于速率限制或机器人限制的目标,提高`--delay`(默认500ms)——扫描第三方生产站点前,确认你有权测试目标并检查其robots.txt/服务条款。在你自己的项目的`package.json`中精确固定`@axe-core/playwright`,因为规则可用性随axe-core版本而异——解析的版本会记录在`summary.json`的`axe_core_version`中,正是出于此原因。

**捕获静默替代文本回归:** 在更改前后使用`--alt-snapshot`运行,然后对比两个`alt-snapshot.json`文件——

```bash
node references/baseline-url-scan.mjs --out ./before --alt-snapshot https://example.com/page

...make your change...

...进行更改...

node references/baseline-url-scan.mjs --out ./after --alt-snapshot https://example.com/page diff before/alt-snapshot.json after/alt-snapshot.json

Any diff line is either an intentional content update or a silent regression — a human confirms which.
node references/baseline-url-scan.mjs --out ./after --alt-snapshot https://example.com/page diff before/alt-snapshot.json after/alt-snapshot.json

任何差异行要么是有意的内容更新,要么是静默回归——由人工确认。

What this mode IS and IS NOT

此模式的适用场景与不适用场景

  • IS for: baseline and regression sweeps across many pages in one run; machine-readable evidence (rule id, impact, node count, sample selectors) that can feed a11y-critic Phase 0 or the Optional A11y Evidence Finding Contract (§4 below); trend baselines across repeated runs of the same URL list — rerun and diff
    summary.json
    (or
    alt-snapshot.json
    for alt-text specifically).
  • IS NOT: keyboard-operability or screen-reader evidence of any kind — it never presses a key or captures an announcement (route those to keyboard-a11y-tester or virtual-screen-reader). Axe-core is a detector, not a verdict authority here, same as every other automated lane in this bundle: it covers roughly 30-40% of WCAG 2.2 issue classes (the same axe-detectable-subset ceiling as the §4 in-spec-file scans below), and its rules are heuristics, not the standard itself. A clean scan is not a conformance claim — it means axe found nothing in its rule set on the URLs scanned, nothing more. Treat the output as candidate findings for human review. The
    --census
    checks are heuristics one level below even axe's rules — pattern-matches on markup shape and naming, not accessibility-tree computation — so their false-positive rate is higher by design; triage every
    census
    hit by hand before filing it.
  • 适用场景: 一次运行中对多个页面进行基线和回归扫描;机器可读证据(规则ID、影响、节点计数、样本选择器),可用于a11y-critic第0阶段或可选可访问性证据发现契约(下方第4节);对同一URL列表重复运行的趋势基线——重新运行并对比
    summary.json
    (或针对替代文本对比
    alt-snapshot.json
    )。
  • 不适用场景: 任何类型的键盘可操作性或屏幕阅读器证据——它绝不按键或捕获公告(将这些路由到keyboard-a11y-tester或virtual-screen-reader)。axe-core在此是检测器,而非判定权威,与此包中的其他自动化路径相同:它覆盖了大约30-40%的WCAG 2.2问题类别(与下方第4节规范文件扫描相同的axe可检测子集上限),其规则是启发式规则,而非标准本身。干净的扫描并不代表合规声明——它仅意味着axe在其规则集中未在扫描的URL上发现任何问题,仅此而已。将输出视为人工评审的候选发现结果。
    --census
    检查是比axe规则更低一级的启发式规则——基于标记形状和命名的模式匹配,而非可访问性树计算——因此其误报率设计上更高;在提交之前,必须手动分类每个
    census
    命中结果。

pa11y-ci for sitemap-wide sweeps (routed, not vendored)

使用pa11y-ci进行站点地图范围的扫描(路由,而非 vendored)

For a sweep driven by a sitemap rather than a hand-maintained URL list, route to
pa11y-ci
instead of adding sitemap discovery to this script:
bash
npx pa11y-ci --sitemap https://example.com/sitemap.xml --runner axe --runner htmlcs
Same adoption boundary as keyboard-a11y-tester and virtual-screen-reader: a routed external tool the operator installs in their own project, never vendored into this repo.
对于由站点地图驱动的扫描,而非手动维护的URL列表,请路由到
pa11y-ci
,而非在此脚本中添加站点地图发现功能:
bash
npx pa11y-ci --sitemap https://example.com/sitemap.xml --runner axe --runner htmlcs
与keyboard-a11y-tester和virtual-screen-reader采用相同的边界:操作员在自己的项目中安装的路由外部工具,绝不 vendored 到此仓库中。

1. Keyboard Accessibility Tests

1. 键盘可访问性测试

MANDATORY: All keyboard tests MUST use real Playwright keyboard interactions against a live or local site. Never check ARIA attributes alone and claim a keyboard test passed — you must actually press keys and verify the result.
强制要求:所有键盘测试必须针对实时或本地站点使用真实的Playwright键盘交互。绝不能仅检查ARIA属性就声称键盘测试通过——你必须实际按键并验证结果。

Required Testing Method

要求的测试方法

  • Use
    page.keyboard.press('Enter')
    ,
    page.keyboard.press('Tab')
    ,
    page.keyboard.press('Escape')
    ,
    page.keyboard.press('Space')
    for single keys
  • Use
    page.keyboard.press('Shift+ArrowRight')
    ,
    page.keyboard.press('Control+Enter')
    ,
    page.keyboard.press('Meta+Enter')
    for key combos
  • Use
    page.keyboard.down('Shift')
    /
    page.keyboard.up('Shift')
    with
    page.keyboard.press('ArrowRight')
    for held-key sequences (e.g., text selection)
  • Use
    element.focus()
    then verify with
    toBeFocused()
    or
    document.activeElement === element
  • NEVER use synthetic
    dispatchEvent(new KeyboardEvent(...))
    to test keyboard features — that bypasses the real browser keyboard path and proves nothing
  • NEVER claim a keyboard test passed by only reading DOM attributes (aria-expanded, aria-pressed, etc.) without actually pressing a key and observing the state change
  • 使用
    page.keyboard.press('Enter')
    page.keyboard.press('Tab')
    page.keyboard.press('Escape')
    page.keyboard.press('Space')
    测试单个按键
  • 使用
    page.keyboard.press('Shift+ArrowRight')
    page.keyboard.press('Control+Enter')
    page.keyboard.press('Meta+Enter')
    测试组合键
  • 使用
    page.keyboard.down('Shift')
    /
    page.keyboard.up('Shift')
    搭配
    page.keyboard.press('ArrowRight')
    测试按住按键的序列(例如文本选择)
  • 使用
    element.focus()
    然后通过
    toBeFocused()
    document.activeElement === element
    验证
  • 绝不使用合成的
    dispatchEvent(new KeyboardEvent(...))
    测试键盘功能——这会绕过真实的浏览器键盘路径,无法证明任何内容
  • 绝不仅通过读取DOM属性(aria-expanded、aria-pressed等)而不实际按键并观察状态变更就声称键盘测试通过

What to Test (with real key presses)

测试内容(使用真实按键)

  1. Tab order: Press Tab repeatedly and verify focus moves to each interactive element in logical order
  2. Enter/Space activation: Focus a button/link, press Enter or Space, verify the expected action occurred (panel opened, state toggled, navigation happened)
  3. Escape to dismiss: Open a modal/popup/sidebar, press Escape, verify it closed
  4. Arrow key navigation: For tablists, menus, and custom widgets — press Arrow keys and verify focus/selection moves
  5. Keyboard text selection: For content areas — use Shift+Arrow to select text, verify selection was created via
    window.getSelection()
  6. Modifier combos: Test Ctrl+Enter, Meta+Enter, and other app-specific shortcuts
  7. Focus management: After opening/closing panels, verify focus moves to the correct element (e.g., CKEditor gets focus when annotation form opens, focus returns to trigger after modal closes)
  1. Tab顺序:重复按Tab键,验证焦点按逻辑顺序移动到每个交互式元素
  2. Enter/Space激活:聚焦按钮/链接,按Enter或Space,验证预期操作发生(面板打开、状态切换、导航完成)
  3. Escape关闭:打开模态框/弹出窗口/侧边栏,按Escape,验证其关闭
  4. 箭头键导航:对于标签列表、菜单和自定义小部件——按箭头键,验证焦点/选择移动
  5. 键盘文本选择:对于内容区域——使用Shift+箭头选择文本,通过
    window.getSelection()
    验证选择已创建
  6. 修饰符组合:测试Ctrl+Enter、Meta+Enter和其他应用特定快捷键
  7. 焦点管理:打开/关闭面板后,验证焦点移动到正确元素(例如,注释表单打开时CKEditor获得焦点,模态框关闭后焦点返回到触发器)

State Verification Pattern

状态验证模式

Every keyboard test must follow this pattern:
1. Record initial state (aria-expanded, aria-pressed, visibility, activeElement)
2. Perform real keyboard action (page.keyboard.press)
3. Wait for UI to update (waitForTimeout or waitForFunction)
4. Verify state actually changed (attribute toggled, element visible/hidden, focus moved)
Example — testing a toggle button:
js
const btn = page.locator('button[aria-expanded]');
const initialExpanded = await btn.getAttribute('aria-expanded');
await btn.focus();
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
const afterExpanded = await btn.getAttribute('aria-expanded');
expect(initialExpanded).not.toBe(afterExpanded); // State MUST change
每个键盘测试必须遵循此模式:
1. 记录初始状态(aria-expanded、aria-pressed、可见性、activeElement)
2. 执行真实键盘操作(page.keyboard.press)
3. 等待UI更新(waitForTimeout或waitForFunction)
4. 验证状态实际变更(属性切换、元素显示/隐藏、焦点移动)
示例——测试切换按钮:
js
const btn = page.locator('button[aria-expanded]');
const initialExpanded = await btn.getAttribute('aria-expanded');
await btn.focus();
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
const afterExpanded = await btn.getAttribute('aria-expanded');
expect(initialExpanded).not.toBe(afterExpanded); // 状态必须变更

WAI-ARIA APG Keyboard Test Templates

WAI-ARIA APG键盘测试模板

Reusable Playwright templates for common widget patterns. Each uses real
page.keyboard.press()
calls — never synthetic events.
1. Tree View Interactions: ArrowDown/Up move
aria-activedescendant
; ArrowRight expands closed node or moves to first child; ArrowLeft collapses open node or moves to parent; Home/End jump to first/last visible treeitem; Enter activates.
js
const tree = page.locator('[role="tree"]');
await tree.focus();
const before = await tree.getAttribute('aria-activedescendant');
await page.keyboard.press('ArrowDown');
await page.waitForTimeout(200);
const after = await tree.getAttribute('aria-activedescendant');
expect(after).not.toBe(before);
expect(after).toBeTruthy(); // must reference a [role="treeitem"] id
2. Roving Tabindex (Tabs) Interactions: ArrowRight/Left move focus between
[role="tab"]
elements and update tabindex; active tab keeps
tabindex="0"
, others get
tabindex="-1"
; only one
aria-selected="true"
per
[role="tablist"]
.
js
const activeTab = page.locator('[role="tab"][tabindex="0"]');
await activeTab.focus();
await page.keyboard.press('ArrowRight');
await page.waitForTimeout(200);
const newActive = page.locator('[role="tab"][tabindex="0"]');
await expect(newActive).toHaveAttribute('aria-selected', 'true');
expect(await page.locator('[role="tab"][aria-selected="true"]').count()).toBe(1);
3. Dialog Focus Trap Interactions: Tab/Shift+Tab cycle within
[role="dialog"]
(last focusable→first, first→last); Escape closes; focus returns to trigger after close.
js
await triggerButton.click();
const dialog = page.locator('[role="dialog"]');
// Tab past last focusable item — should wrap to first
const focusables = dialog.locator('button, [href], input, [tabindex="0"]');
const count = await focusables.count();
for (let i = 0; i < count; i++) await page.keyboard.press('Tab');
await expect(focusables.first()).toBeFocused();
await page.keyboard.press('Escape');
await expect(triggerButton).toBeFocused();
4. Sidebar/Panel Focus Management Interactions: Close button receives focus on panel open; Escape closes panel and returns focus to trigger.
js
await triggerButton.click();
const panel = page.locator('[role="region"]'); // or your panel selector
await expect(panel.locator('button[aria-label*="Close"]')).toBeFocused();
await page.keyboard.press('Escape');
await page.waitForTimeout(150); // allow React unmount + setTimeout(0)
await expect(triggerButton).toBeFocused();
5. Disclosure Widget Interactions: Enter/Space toggle
aria-expanded
between "true"/"false";
aria-controls
references the panel id; panel visibility matches expanded state.
js
const btn = page.locator('button[aria-expanded]');
await btn.focus();
const initial = await btn.getAttribute('aria-expanded');
await page.keyboard.press('Enter');
await page.waitForTimeout(200);
const toggled = await btn.getAttribute('aria-expanded');
expect(toggled).not.toBe(initial);
const panelId = await btn.getAttribute('aria-controls');
const panel = page.locator(`#${panelId}`);
await expect(panel).toBeVisible(); // when expanded=true
6. Menu Button / Dropdown Interactions: Enter/Space opens menu, focus moves to first item; Arrow keys navigate with wrapping; Escape closes and returns focus to trigger; Home/End jump to first/last; type-ahead jumps to matching item.
js
const trigger = page.locator('button[aria-haspopup="menu"]');
await trigger.focus();
await page.keyboard.press('Enter');
await page.waitForTimeout(200);
const menu = page.locator('[role="menu"]');
await expect(menu).toBeVisible();
await expect(menu.locator('[role="menuitem"]').first()).toBeFocused();
await page.keyboard.press('End');
await expect(menu.locator('[role="menuitem"]').last()).toBeFocused();
await page.keyboard.press('Escape');
await expect(trigger).toBeFocused();
7. Combobox / Autocomplete Interactions: typing shows listbox with filtered options; ArrowDown focuses first option; Enter selects and closes; Escape closes without selection;
aria-expanded
and
aria-activedescendant
update.
js
const input = page.locator('[role="combobox"]');
await input.focus();
await input.type('ap');
await page.waitForTimeout(300);
await expect(input).toHaveAttribute('aria-expanded', 'true');
const listbox = page.locator('[role="listbox"]');
await page.keyboard.press('ArrowDown');
expect(await input.getAttribute('aria-activedescendant')).toBeTruthy();
await page.keyboard.press('Enter');
await expect(listbox).toBeHidden();
8. Listbox (single and multi-select) Interactions: Arrow keys move selection in single-select; Space toggles in multi-select; Shift+Arrow extends range; Home/End jump to first/last; type-ahead navigation.
js
const listbox = page.locator('[role="listbox"]');
await listbox.focus();
await expect(listbox.locator('[role="option"]').first()).toHaveAttribute('aria-selected', 'true');
await page.keyboard.press('ArrowDown');
await page.waitForTimeout(150);
await expect(listbox.locator('[role="option"]').nth(1)).toHaveAttribute('aria-selected', 'true');
await page.keyboard.press('End');
await expect(listbox.locator('[role="option"]').last()).toBeFocused();
9. Slider Interactions: ArrowLeft/Right adjust by step; PageUp/Down by larger increment; Home/End set to min/max;
aria-valuenow
,
aria-valuemin
,
aria-valuemax
update.
js
const slider = page.locator('[role="slider"]');
await slider.focus();
const before = Number(await slider.getAttribute('aria-valuenow'));
await page.keyboard.press('ArrowRight');
await page.waitForTimeout(150);
expect(Number(await slider.getAttribute('aria-valuenow'))).toBeGreaterThan(before);
await page.keyboard.press('Home');
expect(await slider.getAttribute('aria-valuenow')).toBe(await slider.getAttribute('aria-valuemin'));
await page.keyboard.press('End');
expect(await slider.getAttribute('aria-valuenow')).toBe(await slider.getAttribute('aria-valuemax'));
10. Date Picker Interactions: Arrow keys navigate days; PageUp/Down navigate months; Shift+PageUp/Down navigate years; Enter selects and closes; Escape closes without selection and returns focus to input.
js
const input = page.locator('[aria-label*="date" i]');
await input.focus();
await page.keyboard.press('Enter');
const grid = page.locator('[role="grid"]');
await expect(grid).toBeVisible();
await page.keyboard.press('ArrowRight');
await page.keyboard.press('PageDown');
await page.keyboard.press('Enter');
await expect(grid).toBeHidden();
expect(await input.inputValue()).not.toBe('');
11. Accordion Interactions: Enter/Space on header toggles panel;
aria-expanded
reflects state; Arrow keys move between headers; Home/End jump to first/last header.
js
const headers = page.locator('[role="button"][aria-expanded]');
await headers.first().focus();
const initial = await headers.first().getAttribute('aria-expanded');
await page.keyboard.press('Enter');
await page.waitForTimeout(200);
expect(await headers.first().getAttribute('aria-expanded')).not.toBe(initial);
await page.keyboard.press('ArrowDown');
await expect(headers.nth(1)).toBeFocused();
await page.keyboard.press('End');
await expect(headers.last()).toBeFocused();
12. Radio Group Interactions: Arrow keys move selection within group (roving tabindex); Tab moves to/from group as a whole; first or checked radio receives initial focus;
aria-checked
updates with selection.
js
const radios = page.locator('[role="radiogroup"] [role="radio"]');
await radios.first().focus();
await page.keyboard.press('ArrowDown');
await page.waitForTimeout(150);
await expect(radios.nth(1)).toHaveAttribute('aria-checked', 'true');
await expect(radios.first()).toHaveAttribute('aria-checked', 'false');
await page.keyboard.press('Tab');
await expect(radios.nth(1)).not.toBeFocused();
常见小部件模式的可复用Playwright模板。每个模板均使用真实的
page.keyboard.press()
调用——绝不使用合成事件。
1. 树视图 交互:ArrowDown/Up移动
aria-activedescendant
;ArrowRight展开关闭的节点或移动到第一个子节点;ArrowLeft折叠打开的节点或移动到父节点;Home/End跳转到第一个/最后一个可见的treeitem;Enter激活。
js
const tree = page.locator('[role="tree"]');
await tree.focus();
const before = await tree.getAttribute('aria-activedescendant');
await page.keyboard.press('ArrowDown');
await page.waitForTimeout(200);
const after = await tree.getAttribute('aria-activedescendant');
expect(after).not.toBe(before);
expect(after).toBeTruthy(); // 必须引用[role="treeitem"]的ID
2. 漫游Tabindex(标签) 交互:ArrowRight/Left在
[role="tab"]
元素之间移动焦点并更新tabindex;活动标签保持
tabindex="0"
,其他标签设置为
tabindex="-1"
;每个
[role="tablist"]
仅一个
aria-selected="true"
js
const activeTab = page.locator('[role="tab"][tabindex="0"]');
await activeTab.focus();
await page.keyboard.press('ArrowRight');
await page.waitForTimeout(200);
const newActive = page.locator('[role="tab"][tabindex="0"]');
await expect(newActive).toHaveAttribute('aria-selected', 'true');
expect(await page.locator('[role="tab"][aria-selected="true"]').count()).toBe(1);
3. 对话框焦点捕获 交互:Tab/Shift+Tab在
[role="dialog"]
内循环(最后一个可聚焦元素→第一个,第一个→最后一个);Escape关闭;关闭后焦点返回到触发器。
js
await triggerButton.click();
const dialog = page.locator('[role="dialog"]');
// Tab跳过最后一个可聚焦项——应环绕到第一个
const focusables = dialog.locator('button, [href], input, [tabindex="0"]');
const count = await focusables.count();
for (let i = 0; i < count; i++) await page.keyboard.press('Tab');
await expect(focusables.first()).toBeFocused();
await page.keyboard.press('Escape');
await expect(triggerButton).toBeFocused();
4. 侧边栏/面板焦点管理 交互:面板打开时关闭按钮获得焦点;Escape关闭面板并将焦点返回到触发器。
js
await triggerButton.click();
const panel = page.locator('[role="region"]'); // 或你的面板选择器
await expect(panel.locator('button[aria-label*="Close"]')).toBeFocused();
await page.keyboard.press('Escape');
await page.waitForTimeout(150); // 允许React卸载 + setTimeout(0)
await expect(triggerButton).toBeFocused();
5. 披露小部件 交互:Enter/Space切换
aria-expanded
在"true"/"false"之间;
aria-controls
引用面板ID;面板可见性与展开状态匹配。
js
const btn = page.locator('button[aria-expanded]');
await btn.focus();
const initial = await btn.getAttribute('aria-expanded');
await page.keyboard.press('Enter');
await page.waitForTimeout(200);
const toggled = await btn.getAttribute('aria-expanded');
expect(toggled).not.toBe(initial);
const panelId = await btn.getAttribute('aria-controls');
const panel = page.locator(`#${panelId}`);
await expect(panel).toBeVisible(); // 当expanded=true时
6. 菜单按钮/下拉菜单 交互:Enter/Space打开菜单,焦点移动到第一个项;箭头键导航并环绕;Escape关闭并将焦点返回到触发器;Home/End跳转到第一个/最后一个;输入跳转匹配项。
js
const trigger = page.locator('button[aria-haspopup="menu"]');
await trigger.focus();
await page.keyboard.press('Enter');
await page.waitForTimeout(200);
const menu = page.locator('[role="menu"]');
await expect(menu).toBeVisible();
await expect(menu.locator('[role="menuitem"]').first()).toBeFocused();
await page.keyboard.press('End');
await expect(menu.locator('[role="menuitem"]').last()).toBeFocused();
await page.keyboard.press('Escape');
await expect(trigger).toBeFocused();
7. 组合框/自动补全 交互:输入显示过滤选项的列表框;ArrowDown聚焦第一个选项;Enter选择并关闭;Escape关闭不选择;
aria-expanded
aria-activedescendant
更新。
js
const input = page.locator('[role="combobox"]');
await input.focus();
await input.type('ap');
await page.waitForTimeout(300);
await expect(input).toHaveAttribute('aria-expanded', 'true');
const listbox = page.locator('[role="listbox"]');
await page.keyboard.press('ArrowDown');
expect(await input.getAttribute('aria-activedescendant')).toBeTruthy();
await page.keyboard.press('Enter');
await expect(listbox).toBeHidden();
8. 列表框(单选和多选) 交互:单选时箭头键移动选择;多选时Space切换;Shift+箭头扩展范围;Home/End跳转到第一个/最后一个;输入导航。
js
const listbox = page.locator('[role="listbox"]');
await listbox.focus();
await expect(listbox.locator('[role="option"]').first()).toHaveAttribute('aria-selected', 'true');
await page.keyboard.press('ArrowDown');
await page.waitForTimeout(150);
await expect(listbox.locator('[role="option"]').nth(1)).toHaveAttribute('aria-selected', 'true');
await page.keyboard.press('End');
await expect(listbox.locator('[role="option"]').last()).toBeFocused();
9. 滑块 交互:ArrowLeft/Right按步长调整;PageUp/Down按较大增量调整;Home/End设置为最小/最大值;
aria-valuenow
aria-valuemin
aria-valuemax
更新。
js
const slider = page.locator('[role="slider"]');
await slider.focus();
const before = Number(await slider.getAttribute('aria-valuenow'));
await page.keyboard.press('ArrowRight');
await page.waitForTimeout(150);
expect(Number(await slider.getAttribute('aria-valuenow'))).toBeGreaterThan(before);
await page.keyboard.press('Home');
expect(await slider.getAttribute('aria-valuenow')).toBe(await slider.getAttribute('aria-valuemin'));
await page.keyboard.press('End');
expect(await slider.getAttribute('aria-valuenow')).toBe(await slider.getAttribute('aria-valuemax'));
10. 日期选择器 交互:箭头键导航日期;PageUp/Down导航月份;Shift+PageUp/Down导航年份;Enter选择并关闭;Escape关闭不选择并将焦点返回到输入框。
js
const input = page.locator('[aria-label*="date" i]');
await input.focus();
await page.keyboard.press('Enter');
const grid = page.locator('[role="grid"]');
await expect(grid).toBeVisible();
await page.keyboard.press('ArrowRight');
await page.keyboard.press('PageDown');
await page.keyboard.press('Enter');
await expect(grid).toBeHidden();
expect(await input.inputValue()).not.toBe('');
11. 手风琴 交互:Enter/Space在标题上切换面板;
aria-expanded
反映状态;箭头键在标题之间移动;Home/End跳转到第一个/最后一个标题。
js
const headers = page.locator('[role="button"][aria-expanded]');
await headers.first().focus();
const initial = await headers.first().getAttribute('aria-expanded');
await page.keyboard.press('Enter');
await page.waitForTimeout(200);
expect(await headers.first().getAttribute('aria-expanded')).not.toBe(initial);
await page.keyboard.press('ArrowDown');
await expect(headers.nth(1)).toBeFocused();
await page.keyboard.press('End');
await expect(headers.last()).toBeFocused();
12. 单选按钮组 交互:箭头键在组内移动选择(漫游tabindex);Tab整体移入/移出组;第一个或已选中的单选按钮获得初始焦点;
aria-checked
随选择更新。
js
const radios = page.locator('[role="radiogroup"] [role="radio"]');
await radios.first().focus();
await page.keyboard.press('ArrowDown');
await page.waitForTimeout(150);
await expect(radios.nth(1)).toHaveAttribute('aria-checked', 'true');
await expect(radios.first()).toHaveAttribute('aria-checked', 'false');
await page.keyboard.press('Tab');
await expect(radios.nth(1)).not.toBeFocused();

Live Site Requirement

实时站点要求

Keyboard tests MUST run against a real site (local dev environment like Lando/DDEV, or staging). Guard against accidental use of mocks:
js
if (!BASE_URL || !BASE_URL.match(/https?:\/\/.+/)) {
  throw new Error('Keyboard tests require a real site. Set BASE_URL.');
}
键盘测试必须针对真实站点运行(本地开发环境如Lando/DDEV,或staging环境)。防止意外使用模拟数据:
js
if (!BASE_URL || !BASE_URL.match(/https?:\\/\\/.+/)) {
  throw new Error('Keyboard tests require a real site. Set BASE_URL.');
}

SPA-Specific Testing Patterns

SPA特定测试模式

React and other SPA frameworks introduce gotchas that break naive Playwright tests:
  • No direct URL navigation: SPA routes (e.g.,
    /book/truth-lending/2460032
    ) return 404 from the server — the server has no route for them. Navigate WITHIN the app by clicking menu items and waiting for React to render. Use
    waitForSelector()
    to confirm content has loaded before interacting.
  • Duplicate DOM (mobile + desktop): Many React apps render the same component twice — once for desktop, once for mobile. Playwright strict mode throws when a selector matches both. Fix by scoping to a container (
    nav.left-sidebar [role="tree"]
    ) or appending
    .first()
    /
    .last()
    to your locator.
  • React state waits: After
    page.keyboard.press()
    , React state updates are async — the DOM may not reflect the new state for tens of milliseconds. Add
    waitForTimeout(200–500)
    or
    waitForFunction(() => ...)
    before asserting on ARIA attributes that change via React state.
  • React 16
    setTimeout(0)
    for focus-after-unmount
    : In React 16, focus calls issued inside async callbacks do not survive component unmount. Production code must wrap the focus call in
    setTimeout(() => el.focus(), 0)
    . Tests must account for this by allowing 100–200ms after a panel closes before checking
    document.activeElement
    .
  • DOMPurify stripping
    data-*
    attributes
    : A bare
    DOMPurify.sanitize()
    call strips
    data-*
    attributes by default. If tests find click handlers broken after sanitization, the fix is to route sanitization through a wrapper component that calls sanitize at render time (not as a pre-processing step that discards needed attributes).
  • Playwright MCP cannot deliver keyboard events: The Playwright MCP browser integration CANNOT forward keyboard events —
    browser_press_key
    calls are silently dropped for most interactive widgets. Always run keyboard a11y tests with
    npx playwright test
    using
    .spec.js
    files. Use the MCP browser only for visual inspection and DOM queries.
React和其他SPA框架会引入破坏朴素Playwright测试的陷阱:
  • 无直接URL导航:SPA路由(例如
    /book/truth-lending/2460032
    )从服务器返回404——服务器没有对应的路由。通过点击菜单项并等待React渲染在应用内导航。使用
    waitForSelector()
    确认内容已加载后再进行交互。
  • 重复DOM(移动+桌面):许多React应用会渲染相同组件两次——一次用于桌面,一次用于移动。Playwright严格模式在选择器匹配两者时会抛出错误。通过将选择器作用域限定到容器(
    nav.left-sidebar [role="tree"]
    )或在定位器后追加
    .first()
    /
    .last()
    来修复。
  • React状态等待
    page.keyboard.press()
    后,React状态更新是异步的——DOM可能需要数十毫秒才能反映新状态。在断言通过React状态变更的ARIA属性之前,添加
    waitForTimeout(200–500)
    waitForFunction(() => ...)
  • React 16
    setTimeout(0)
    用于卸载后聚焦
    :在React 16中,异步回调内发出的聚焦调用无法在组件卸载后存活。生产代码必须将聚焦调用包装在
    setTimeout(() => el.focus(), 0)
    中。测试必须考虑这一点,允许面板关闭后100–200ms再检查
    document.activeElement
  • DOMPurify剥离
    data-*
    属性
    :裸
    DOMPurify.sanitize()
    调用默认会剥离
    data-*
    属性。如果测试发现 sanitization 后点击处理程序损坏,修复方法是通过包装组件路由 sanitization,在渲染时调用sanitize(而非作为丢弃所需属性的预处理步骤)。
  • Playwright MCP无法传递键盘事件:Playwright MCP浏览器集成无法转发键盘事件——
    browser_press_key
    调用对于大多数交互式小部件会被静默丢弃。始终使用
    .spec.js
    文件通过
    npx playwright test
    运行键盘可访问性测试。仅将MCP浏览器用于视觉检查和DOM查询。

CSS Anti-patterns That Break Keyboard Access

破坏键盘可访问性的CSS反模式

visibility:hidden
+
:focus-within
catch-22 (CRITICAL)
Never use
visibility: hidden
on elements that are supposed to become visible when a parent receives keyboard focus via
:focus-within
. The pattern creates an impossible state for keyboard users:
  • visibility: hidden
    removes the element from the tab order entirely
  • Because the element can't receive focus,
    :focus-within
    is never triggered on the parent
  • Result: keyboard users can never reach the element at all
css
/* ❌ BROKEN — keyboard users can never trigger :focus-within on the parent */
.annotation-block-edit {
  opacity: 0;
  visibility: hidden; /* removes from tab order → :focus-within never fires */
}
.annotation-block:focus-within .annotation-block-edit {
  opacity: 1;
  visibility: visible;
}

/* ✅ CORRECT — opacity keeps element in tab order; :focus-within works */
.annotation-block-edit {
  opacity: 0; /* visually hidden but still focusable */
}
.annotation-block:hover .annotation-block-edit,
.annotation-block:focus-within .annotation-block-edit {
  opacity: 1;
}
This applies to any "reveal on hover/focus" pattern: edit buttons, delete buttons, action menus inside cards. Use
opacity
only (not
visibility
) when the element must remain keyboard-reachable.
visibility:hidden
+
:focus-within
死锁(严重)
绝不要对应该在父元素通过
:focus-within
获得键盘焦点时显示的元素使用
visibility: hidden
。此模式为键盘用户创建了不可能的状态:
  • visibility: hidden
    会将元素从Tab顺序中完全移除
  • 因为元素无法获得焦点,父元素上的
    :focus-within
    永远不会触发
  • 结果:键盘用户永远无法访问该元素
css
/* ❌ 损坏——键盘用户永远无法触发父元素的:focus-within */
.annotation-block-edit {
  opacity: 0;
  visibility: hidden; /* 从Tab顺序中移除 → :focus-within永远不会触发 */
}
.annotation-block:focus-within .annotation-block-edit {
  opacity: 1;
  visibility: visible;
}

/* ✅ 正确——opacity保持元素可聚焦;:focus-within有效 */
.annotation-block-edit {
  opacity: 0; /* 视觉隐藏但仍可聚焦 */
}
.annotation-block:hover .annotation-block-edit,
.annotation-block:focus-within .annotation-block-edit {
  opacity: 1;
}
这适用于任何“悬停/聚焦时显示”模式:编辑按钮、删除按钮、卡片内的操作菜单。当元素必须保持键盘可访问时,仅使用
opacity
(不使用
visibility
)。

ARIA Attribute Checks (supplement, not substitute)

ARIA属性检查(补充,而非替代)

After verifying keyboard operability, also check:
  • Buttons have
    aria-label
    or visible text
  • Toggle buttons have
    aria-pressed
    or
    aria-expanded
  • Tab widgets have
    role="tablist"
    ,
    role="tab"
    ,
    aria-selected
  • SVGs inside buttons have
    aria-hidden="true"
  • Close buttons have descriptive
    aria-label
  • Only one tab has
    aria-selected="true"
    per tablist
验证键盘可操作性后,还需检查:
  • 按钮有
    aria-label
    或可见文本
  • 切换按钮有
    aria-pressed
    aria-expanded
  • 标签小部件有
    role="tablist"
    role="tab"
    aria-selected
  • 按钮内的SVG有
    aria-hidden="true"
  • 关闭按钮有描述性的
    aria-label
  • 每个标签列表仅一个标签有
    aria-selected="true"

Section 5: Time-Based Media Tests

第5节:基于时间的媒体测试

Run these tests when
<video>
,
<audio>
, or media player components are present.
当存在
<video>
<audio>
或媒体播放器组件时,运行这些测试。

Caption Infrastructure

字幕基础设施

  • Verify
    <track kind="captions">
    exists on every
    <video>
    with speech
  • Verify
    <track>
    has valid
    src
    pointing to caption file
  • Verify caption toggle control exists and is keyboard-accessible
  • 验证每个带语音的
    <video>
    都有
    <track kind="captions">
  • 验证
    <track>
    有指向字幕文件的有效
    src
  • 验证字幕切换控件存在且可通过键盘访问

Transcript Availability

转录本可用性

  • Verify transcript exists adjacent to media OR a visible link to it
  • For audio-only content: verify full text transcript is available
  • 验证转录本存在于媒体旁边或有指向它的可见链接
  • 对于纯音频内容:验证提供完整文本转录本

Media Player Keyboard Access

媒体播放器键盘可访问性

  • Tab: focus enters player controls; all controls have visible focus indicators
  • Space: play/pause toggle
  • Arrow keys: seek forward/backward; Up/Down: volume control
  • C or CC button: caption toggle; Escape: exit fullscreen
  • Tab:焦点进入播放器控件;所有控件有可见焦点指示器
  • Space:播放/暂停切换
  • 箭头键:向前/向后快进;Up/Down:音量控制
  • C或CC按钮:字幕切换;Escape:退出全屏

Audio Auto-play

音频自动播放

  • Verify no audio auto-plays on page load
  • If auto-play exists: verify pause/stop control is the first focusable element
  • 验证页面加载时无音频自动播放
  • 如果存在自动播放:验证暂停/停止控件是第一个可聚焦元素

Section 6: Screen Reader Test Protocol

第6节:屏幕阅读器测试协议

Test Matrix

测试矩阵

Screen ReaderBrowserMode
NVDAChromeBrowse mode + Focus mode
VoiceOverSafari (macOS)Web rotor + standard navigation
(Optional) JAWSChrome/EdgeVirtual cursor + Forms mode
屏幕阅读器浏览器模式
NVDAChrome浏览模式 + 焦点模式
VoiceOverSafari (macOS)Web转子 + 标准导航
(可选)JAWSChrome/Edge虚拟光标 + 表单模式

Landmark Navigation Test

地标导航测试

  • Use landmark navigation (NVDA: D key, VoiceOver: Web rotor)
  • Verify:
    <main>
    ,
    <nav>
    ,
    <header>
    ,
    <footer>
    announced correctly
  • Verify: multiple
    <nav>
    elements have distinguishing
    aria-label
  • 使用地标导航(NVDA:D键,VoiceOver:Web转子)
  • 验证:
    <main>
    <nav>
    <header>
    <footer>
    正确公告
  • 验证:多个
    <nav>
    元素有区分性的
    aria-label

Heading Navigation Test

标题导航测试

  • Navigate by headings (NVDA: H key, VoiceOver: Web rotor); verify hierarchy is logical, no skipped levels;
    <h1>
    present
  • 通过标题导航(NVDA:H键,VoiceOver:Web转子);验证层次结构合理,无跳过级别;存在
    <h1>

Form Mode Test

表单模式测试

  • Tab into form (NVDA enters focus mode automatically)
  • Verify: each input announces its label and "required" if applicable
  • Verify: error messages announce when field is focused;
    aria-describedby
    reads after label
  • Tab进入表单(NVDA自动进入焦点模式)
  • 验证:每个输入框公告其标签和(如果适用)“必填”
  • 验证:字段聚焦时公告错误消息;
    aria-describedby
    在标签后读取

Live Region Test

实时区域测试

  • Trigger dynamic content changes (form submission, async updates, notifications)
  • Verify:
    aria-live="polite"
    announces after current speech
  • Verify:
    aria-live="assertive"
    interrupts; toast content announced without focus moving
  • 触发动态内容变更(表单提交、异步更新、通知)
  • 验证:
    aria-live="polite"
    在当前语音后公告
  • 验证:
    aria-live="assertive"
    中断;toast内容公告时焦点不移动

SPA Route Change Test

SPA路由变更测试

  • Navigate between routes; verify page title updates and is announced
  • Verify: focus moves to main content or heading; back button restores expected focus
  • 在路由之间导航;验证页面标题更新并公告
  • 验证:焦点移动到主要内容或标题;后退按钮恢复预期焦点

2. Visual Regression Tests (REQUIRED)

2. 视觉回归测试(强制要求)

Visual regression tests ensure accessibility fixes don't introduce unintended visual changes. Supports Playwright and optionally BackstopJS for side-by-side HTML reports.
视觉回归测试确保可访问性修复不会引入意外的视觉变更。支持Playwright,可选BackstopJS用于并排HTML报告。

Baseline Strategy

基线策略

  • Preferred: Use
    npx playwright test --update-snapshots
    on the current branch to establish baselines, then run tests after further changes to detect regressions.
  • CRITICAL — build must be complete first: Only run
    --update-snapshots
    after any build (React, webpack, etc.) has fully finished. Running it during a concurrent build captures mixed pre/post-build screenshots — some pages reflect old code, some new. The resulting baseline is internally inconsistent and will fail on the next clean run. Wait for the build to complete, then run
    --update-snapshots
    , then run the tests.
  • Cross-branch comparison: Only when explicitly requested. Requires branch switching, cache clearing, and potential config sync — avoid unless necessary.
  • Never assume branch-switching is safe without checking with the user first.
  • 首选: 在当前分支上使用
    npx playwright test --update-snapshots
    建立基线,然后在进一步变更后运行测试以检测回归。
  • 严重——必须先完成构建: 仅在任何构建(React、webpack等)完全完成后运行
    --update-snapshots
    。在并发构建期间运行会捕获混合的构建前/构建后截图——某些页面反映旧代码,某些反映新代码。生成的基线内部不一致,下次干净运行时会失败。等待构建完成,然后运行
    --update-snapshots
    ,再运行测试。
  • 跨分支对比: 仅在明确请求时进行。需要分支切换、缓存清除和潜在的配置同步——除非必要,否则避免。
  • 绝不在未先与用户确认的情况下假设分支切换是安全的。

Playwright Screenshot Configuration

Playwright截图配置

Use
toHaveScreenshot()
with the correct options:
  • maxDiffPixelRatio
    (0 to 1): Maximum ratio of different pixels to total pixels. Use
    0.01
    (1%) for element screenshots,
    0.03
    (3%) for full-page screenshots. This is the primary control for flakiness.
  • threshold
    (0 to 1): Per-pixel color distance tolerance (0 = exact, 1 = any color). Default
    0.2
    is fine for most cases. This is NOT the overall diff threshold.
  • maxDiffPixels
    : Absolute count of allowed different pixels. Alternative to
    maxDiffPixelRatio
    .
js
// Element screenshot — tight tolerance
await expect(element).toHaveScreenshot('name.png', {
  maxDiffPixelRatio: 0.01,
});

// Full-page screenshot — looser for dynamic content
await expect(page).toHaveScreenshot('name.png', {
  fullPage: true,
  maxDiffPixelRatio: 0.03,
  mask: [page.locator('.dynamic-region')],
});
使用
toHaveScreenshot()
并设置正确选项:
  • maxDiffPixelRatio
    (0到1):不同像素与总像素的最大比率。元素截图使用
    0.01
    (1%),全页截图使用
    0.03
    (3%)。这是控制不稳定的主要手段。
  • threshold
    (0到1):每个像素的颜色距离容差(0=完全匹配,1=任意颜色)。默认
    0.2
    适用于大多数情况。这不是整体差异阈值。
  • maxDiffPixels
    :允许的不同像素的绝对计数。是
    maxDiffPixelRatio
    的替代选项。
js
// 元素截图——严格容差
await expect(element).toHaveScreenshot('name.png', {
  maxDiffPixelRatio: 0.01,
});

// 全页截图——动态内容使用较宽松容差
await expect(page).toHaveScreenshot('name.png', {
  fullPage: true,
  maxDiffPixelRatio: 0.03,
  mask: [page.locator('.dynamic-region')],
});

BackstopJS (Optional)

BackstopJS(可选)

BackstopJS provides an HTML report with side-by-side visual diffs — useful for manual review. It can run alongside Playwright tests.
Setup:
bash
npm install --save-dev backstopjs
Configuration (
backstop.json
):
  • Use
    scenarioDefaults
    for shared settings (delay, misMatchThreshold, removeSelectors)
  • Use
    "selectors": ["document"]
    for full-page, or class/tag selectors for elements
  • Avoid attribute selectors with quoted values (e.g.
    [type='text']
    ) — they cause parse errors in Puppeteer engine
  • Use
    requireSameDimensions: false
    for pages with dynamic heights
  • Full-page scenarios need higher
    misMatchThreshold
    (15-20%) due to dynamic content
  • Element scenarios can use tighter thresholds (5-10%)
Popup/overlay handling: Create an
onReady.cjs
engine script (use
.cjs
extension if project has
"type": "module"
in package.json):
js
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
module.exports = async (page, scenario, vp) => {
  await wait(2000);
  await page.evaluate(() => {
    document.querySelectorAll('dialog, [role="dialog"], .modal, .popup').forEach(el => el.remove());
  });
  await wait(300);
};
Workflow:
bash
npx backstop reference --config=path/to/backstop.json  # Capture baseline
npx backstop test --config=path/to/backstop.json       # Compare against baseline
npx backstop approve --config=path/to/backstop.json    # Promote test -> reference
npx backstop openReport --config=path/to/backstop.json # View HTML report
BackstopJS提供带有并排视觉差异的HTML报告——适用于人工评审。它可与Playwright测试一起运行。
设置:
bash
npm install --save-dev backstopjs
配置
backstop.json
):
  • 使用
    scenarioDefaults
    设置共享配置(延迟、misMatchThreshold、removeSelectors)
  • 使用
    "selectors": ["document"]
    进行全页测试,或使用类/标签选择器进行元素测试
  • 避免带引号值的属性选择器(例如
    [type='text']
    )——它们会在Puppeteer引擎中导致解析错误
  • 使用
    requireSameDimensions: false
    处理动态高度的页面
  • 全页场景需要更高的
    misMatchThreshold
    (15-20%),因为动态内容
  • 元素场景可使用更严格的阈值(5-10%)
弹出窗口/覆盖层处理: 创建
onReady.cjs
引擎脚本(如果项目的package.json中有
"type": "module"
,请使用
.cjs
扩展名):
js
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
module.exports = async (page, scenario, vp) => {
  await wait(2000);
  await page.evaluate(() => {
    document.querySelectorAll('dialog, [role="dialog"], .modal, .popup').forEach(el => el.remove());
  });
  await wait(300);
};
工作流程:
bash
npx backstop reference --config=path/to/backstop.json  # 捕获基线
npx backstop test --config=path/to/backstop.json       # 与基线对比
npx backstop approve --config=path/to/backstop.json    # 将测试结果提升为基线
npx backstop openReport --config=path/to/backstop.json # 查看HTML报告

Handling Dynamic Content

处理动态内容

CMS pages often contain dynamic elements (timestamps, session blocks, popups). These cause false failures.
  • Prefer element-level screenshots over full-page — more stable and more useful for a11y regression detection.
  • Mask dynamic regions: Playwright uses
    mask: [page.locator()]
    , BackstopJS uses
    removeSelectors
    or
    hideSelectors
    .
  • Common elements to mask/remove:
    .contextual
    ,
    .toolbar-tray
    ,
    .messages
    ,
    [data-drupal-messages]
    ,
    dialog
    ,
    [role="dialog"]
    , time/date elements.
  • Dismiss popups before capture: Use Playwright's
    dismissPopups()
    helper or BackstopJS
    onReadyScript
    .
  • Use
    waitForLoadState('networkidle')
    and a short wait to let JS behaviors settle before capture.
CMS页面通常包含动态元素(时间戳、会话块、弹出窗口)。这些会导致误判失败。
  • 优先选择元素级截图而非全页截图——更稳定,对可访问性回归检测更有用。
  • 屏蔽动态区域:Playwright使用
    mask: [page.locator()]
    ,BackstopJS使用
    removeSelectors
    hideSelectors
  • 常见需要屏蔽/移除的元素
    .contextual
    .toolbar-tray
    .messages
    [data-drupal-messages]
    dialog
    [role="dialog"]
    、时间/日期元素。
  • 捕获前关闭弹出窗口:使用Playwright的
    dismissPopups()
    助手或BackstopJS
    onReadyScript
  • **使用
    waitForLoadState('networkidle')
    **和短暂等待,让JS行为稳定后再捕获。

Contrast Verification

对比度验证

  • Use browser DevTools (Chrome: CSS Overview, Firefox: Accessibility Inspector) to audit all text contrast
  • Run axe-core with
    color-contrast
    rule enabled (catches most but not all cases)
  • Manually check: text over images/gradients (axe-core misses these)
  • Manually check: focus indicator contrast against both focused and unfocused backgrounds
  • Check non-text contrast: UI component borders, icons, form control outlines (WCAG 1.4.11)
  • Test with forced-colors mode: verify all interactive elements remain distinguishable
  • 使用浏览器开发者工具(Chrome:CSS概览,Firefox:可访问性检查器)审核所有文本对比度
  • 启用
    color-contrast
    规则运行axe-core(捕获大多数但非所有情况)
  • 手动检查:图像/渐变上的文本(axe-core会遗漏这些)
  • 手动检查:焦点指示器与聚焦和非聚焦背景的对比度
  • 检查非文本对比度:UI组件边框、图标、表单控件轮廓(WCAG 1.4.11)
  • 测试强制颜色模式:验证所有交互式元素仍可区分

Zoom and Reflow Verification

缩放和重流验证

  • Set viewport to 1280px, zoom to 400% (equivalent to 320px)
  • Verify: no horizontal scrollbar, content reflows to single column
  • Verify: no text truncation, overlap, or content hidden behind other elements
  • Test text spacing override: 1.5x line height, 2x paragraph spacing, 0.12em letter spacing
  • Verify: all interactive elements remain visible and operable at 200% zoom
  • 将视口设置为1280px,缩放至400%(相当于320px)
  • 验证:无水平滚动条,内容重流为单列
  • 验证:无文本截断、重叠或内容被其他元素遮挡
  • 测试文本间距覆盖:1.5倍行高、2倍段落间距、0.12em字母间距
  • 验证:所有交互式元素在200%缩放时仍可见且可操作

Elements to Test

测试元素

  • Focus indicators (links, buttons, inputs in :focus state)
  • Breadcrumbs (structure and current page indicator)
  • Navigation menus (default, hover, active states)
  • Form inputs (borders, focus states)
  • Link underlines in content areas
  • External link icons
  • Skip links (when visible)
  • Progress bars and loading indicators
  • 焦点指示器(链接、按钮、:focus状态的输入框)
  • 面包屑(结构和当前页面指示器)
  • 导航菜单(默认、悬停、活动状态)
  • 表单输入框(边框、焦点状态)
  • 内容区域的链接下划线
  • 外部链接图标
  • 跳过链接(可见时)
  • 进度条和加载指示器

Viewport Sizes

视口尺寸

  • Desktop: 1280x800
  • Tablet: 768x1024
  • Mobile: 320x568
  • 桌面:1280x800
  • 平板:768x1024
  • 移动:320x568

Reporting

报告

  • Playwright:
    npx playwright show-report
    for HTML report with side-by-side diffs
  • BackstopJS:
    npx backstop openReport
    for visual comparison dashboard
  • Playwright:
    npx playwright show-report
    查看带并排差异的HTML报告
  • BackstopJS:
    npx backstop openReport
    查看视觉对比仪表板

3. WCAG Compliance Checks

3. WCAG合规性检查

  • 1.1.1 Non-text Content (alt text, aria-labels)
  • 1.4.1 Use of Color (link underlines)
  • 1.4.3 Contrast Minimum (4.5:1 normal text, 3:1 large text — note: text inside UI components like buttons uses TEXT thresholds, not the 3:1 UI component boundary threshold)
  • 1.4.10 Reflow (320px viewport)
  • 1.4.11 Non-text Contrast (form borders, focus indicators)
  • 2.4.4 Link Purpose (contextual aria-labels)
  • 2.4.6 Headings and Labels (no empty headings)
  • 2.4.7 Focus Visible (outline visibility)
  • 2.4.8 Location (breadcrumbs with aria-current)
  • 2.4.11 Focus Not Obscured (focused element not hidden by sticky headers/footers/banners) [WCAG 2.2]
  • 2.4.13 Focus Appearance (focus indicator ≥2px perimeter, 3:1 contrast change) [WCAG 2.2]
  • 2.5.7 Dragging Movements (drag ops have single-pointer alternative) [WCAG 2.2]
  • 2.5.8 Target Size (interactive targets ≥24x24 CSS pixels) [WCAG 2.2]
  • 3.3.7 Redundant Entry (don't re-ask for info already provided) [WCAG 2.2]
  • 3.3.8 Accessible Authentication (no cognitive function tests for login, paste/autofill supported) [WCAG 2.2]
  • 1.1.1 非文本内容(替代文本、aria-labels)
  • 1.4.1 颜色使用(链接下划线)
  • 1.4.3 最低对比度(普通文本4.5:1,大文本3:1——注意:UI组件内的文本使用文本阈值,而非3:1 UI组件边界阈值)
  • 1.4.10 重流(320px视口)
  • 1.4.11 非文本对比度(表单边框、焦点指示器)
  • 2.4.4 链接用途(上下文aria-labels)
  • 2.4.6 标题和标签(无空标题)
  • 2.4.7 焦点可见(轮廓可见性)
  • 2.4.8 位置(带aria-current的面包屑)
  • 2.4.11 焦点不被遮挡(聚焦元素不被粘性页眉/页脚/横幅遮挡)[WCAG 2.2]
  • 2.4.13 焦点外观(焦点指示器≥2px周长,3:1对比度变化)[WCAG 2.2]
  • 2.5.7 拖动操作(拖动操作有单指针替代方案)[WCAG 2.2]
  • 2.5.8 目标大小(交互式目标≥24x24 CSS像素)[WCAG 2.2]
  • 3.3.7 重复输入(不要重复询问已提供的信息)[WCAG 2.2]
  • 3.3.8 可访问身份验证(登录无需认知功能测试,支持粘贴/自动填充)[WCAG 2.2]

4. Automated Scanning (axe-core via Playwright)

4. 自动化扫描(通过Playwright使用axe-core)

Inject axe-core into live pages via Playwright for automated WCAG violation detection. This catches issues that manual review misses (computed contrast through CSS layers, missing ARIA on dynamically rendered content, landmark coverage).
通过Playwright将axe-core注入实时页面,自动检测WCAG违规。这会捕获人工评审遗漏的问题(通过CSS层计算的对比度、动态渲染内容上缺失的ARIA、地标覆盖)。

axe-core Injection Pattern

axe-core注入模式

js
// In a Playwright test file (.spec.js)
const { test, expect } = require('@playwright/test');
const fs = require('fs');

test('axe-core accessibility scan', async ({ page }) => {
  await page.goto(BASE_URL);
  await page.waitForLoadState('networkidle');

  // Inject axe-core
  const axeSource = fs.readFileSync(
    require.resolve('axe-core/axe.min.js'), 'utf-8'
  );
  await page.evaluate(axeSource);

  // Run audit
  const results = await page.evaluate(() =>
    axe.run(document, {
      runOnly: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice']
    })
  );

  // Report violations
  const violations = results.violations;
  if (violations.length > 0) {
    const report = violations.map(v => ({
      id: v.id,
      impact: v.impact,
      description: v.description,
      helpUrl: v.helpUrl,
      nodes: v.nodes.length
    }));
    console.log('axe violations:', JSON.stringify(report, null, 2));
  }
  expect(violations.length).toBe(0);
});
js
// 在Playwright测试文件(.spec.js)中
const { test, expect } = require('@playwright/test');
const fs = require('fs');

test('axe-core accessibility scan', async ({ page }) => {
  await page.goto(BASE_URL);
  await page.waitForLoadState('networkidle');

  // 注入axe-core
  const axeSource = fs.readFileSync(
    require.resolve('axe-core/axe.min.js'), 'utf-8'
  );
  await page.evaluate(axeSource);

  // 运行审核
  const results = await page.evaluate(() =>
    axe.run(document, {
      runOnly: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice']
    })
  );

  // 报告违规
  const violations = results.violations;
  if (violations.length > 0) {
    const report = violations.map(v => ({
      id: v.id,
      impact: v.impact,
      description: v.description,
      helpUrl: v.helpUrl,
      nodes: v.nodes.length
    }));
    console.log('axe violations:', JSON.stringify(report, null, 2));
  }
  expect(violations.length).toBe(0);
});

Multi-Page Scanning

多页面扫描

For sites with multiple routes, scan each page variant:
  • Default state (no interactions)
  • Loading state (if applicable — trigger a load and scan before it completes)
  • Error state (submit an invalid form, then scan)
  • Expanded state (open all disclosures/tabs, then scan)
对于有多路由的站点,扫描每个页面变体:
  • 默认状态(无交互)
  • 加载状态(如适用——触发加载并在完成前扫描)
  • 错误状态(提交无效表单,然后扫描)
  • 展开状态(打开所有披露/标签,然后扫描)

Dynamic Test Prioritization

动态测试优先级

After the axe-core scan, use findings to prioritize manual testing effort:
  • axe found ARIA violations → prioritize screen reader testing (Section 1 keyboard + ARIA checks)
  • axe found color-contrast violations → prioritize visual inspection (Section 2 focus indicators, link underlines)
  • axe found heading/structure violations → prioritize keyboard navigation order testing
  • axe found no form violations → deprioritize form testing with a note that automated checks passed
  • Always test regardless: focus indicators at zoom, reduced-motion, skip links
axe-core扫描后,使用发现结果优先处理人工测试工作:
  • axe发现ARIA违规 → 优先进行屏幕阅读器测试(第1节键盘+ARIA检查)
  • axe发现颜色对比度违规 → 优先进行视觉检查(第2节焦点指示器、链接下划线)
  • axe发现标题/结构违规 → 优先进行键盘导航顺序测试
  • axe未发现表单违规 → 降低表单测试优先级,并注明自动化检查通过
  • 始终测试:缩放时的焦点指示器、减少动画、跳过链接

Scale and Sampling (>15 pages)

规模和采样(>15页)

For large sites, classify pages into template groups and scan one representative per group:
  1. Run
    discover
    phase: list all routes, group by template (list page, detail page, form page, etc.)
  2. Select 1-2 pages per template group
  3. Scan representatives, report which templates were covered
  4. Document sampling strategy in the test report
For audit-scope runs (conformance audits, pre-VPAT work), extend template sampling with WCAG-EM sampling discipline (Steps 3–4 of WCAG-EM 2.0; verified reference:
docs/wcag-em-2-reference.md
):
  1. Random sample: add a randomly selected sample of 10% of the structured (template-based) sample, on top, drawn from routes not already selected; record the selection method in the test report
  2. Representativeness check: if the random sample surfaces content types or violation patterns the structured sample missed, the template classification was not representative — expand the structured sample, re-classify, and repeat until the random sample stops surfacing new finding types
  3. Complete processes: multi-view journeys (checkout, multi-step applications, auth) are tested end-to-end, never as isolated pages — include every view in the process (the default sequence plus completion-critical branches) and route them to keyboard-a11y-tester driven sessions (see "Goal-driven journey audits" above); per-page scans do not count as process evidence
  4. State coverage: each sampled page is evaluated in its states (default, loading, error, expanded — the Multi-Page Scanning list above); name the state coverage in the sampling documentation
对于大型站点,将页面分类为模板组,并扫描每个组的一个代表:
  1. 运行
    discover
    阶段:列出所有路由,按模板分组(列表页、详情页、表单页等)
  2. 每个模板组选择1-2个页面
  3. 扫描代表页面,报告覆盖的模板
  4. 在测试报告中记录采样策略
对于审核范围运行(合规审核、VPAT前工作),使用WCAG-EM采样规则扩展模板采样(WCAG-EM 2.0的步骤3–4;已验证参考:
docs/wcag-em-2-reference.md
):
  1. 随机采样: 在结构化(基于模板)采样的基础上,添加10%的随机选择样本,从未选中的路由中抽取;在测试报告中记录选择方法
  2. 代表性检查: 如果随机样本发现结构化样本遗漏的内容类型或违规模式,则模板分类不具代表性——扩展结构化样本,重新分类,重复直到随机样本不再发现新的发现类型
  3. 完整流程: 多视图旅程(结账、多步骤应用、认证)需端到端测试,绝不能作为孤立页面——包含流程中的每个视图(默认序列加上完成关键分支),并将其路由到keyboard-a11y-tester驱动的会话(请参阅上方“目标驱动的旅程审核”);每页扫描不能作为流程证据
  4. 状态覆盖: 每个采样页面在其状态下进行评估(默认、加载、错误、展开——上方多页面扫描列表);在采样文档中注明状态覆盖

Output Format

输出格式

Report axe-core results alongside keyboard and visual regression results:
undefined
将axe-core结果与键盘和视觉回归结果一起报告:
undefined

axe-core Scan Results

axe-core扫描结果

Pages scanned: [count] Total violations: [count] Critical: [n] | Serious: [n] | Moderate: [n] | Minor: [n]
扫描页面数:[count] 总违规数:[count] 严重:[n] | 重要:[n] | 中等:[n] | 轻微:[n]

Violations by Rule

按规则划分的违规

Rule IDImpactDescriptionPagesElements
color-contrastseriousElements must meet color contrast312

This output feeds directly into the a11y-critic's Phase 0 (Consume Test Evidence) — measured violations become hard evidence in the design review.
规则ID影响描述页面数元素数
color-contrast重要元素必须满足颜色对比度312

此输出直接输入到a11y-critic的第0阶段(消费测试证据)——测量的违规成为设计评审中的硬证据。

Optional A11y Evidence Finding Contract

可选可访问性证据发现契约

When a test produces a failing keyboard, axe-core, visual, static-analysis, or manual finding, include an
A11y Evidence Finding
block for each issue that should be handed to
a11y-critic
or
perspective-audit
. Do not emit placeholder contracts for passing checks or clean fixtures.
Use these fields when evidence exists:
undefined
当测试产生失败的键盘、axe-core、视觉、静态分析或人工发现结果时,为每个应提交给
a11y-critic
perspective-audit
的问题添加
A11y Evidence Finding
块。不要为通过的检查或干净的fixture发出占位符契约。
有证据时使用以下字段:
undefined

A11y Evidence Finding

A11y Evidence Finding

finding_id: stable lowercase id, e.g. a11y_form_error_describedby fingerprint: stable 8-64 char hex hash derived from component/target/rule, not the crawl URL alone source: test command, test file, axe rule id, agent-browser ref, or observed artifact wcag_or_apg: WCAG 2.2 criterion or WAI-ARIA APG pattern citation section_508_fpc_context: Section 508 context only when applicable; Revised Section 508 maps web conformance to WCAG 2.0 Level A/AA severity: CRITICAL | MAJOR | MINOR | ENHANCEMENT perspective_alarms: screen_reader_semantic=LOW|MEDIUM|HIGH; keyboard_motor=LOW|MEDIUM|HIGH; etc. evidence: file:line, DOM excerpt, axe node, screenshot, keyboard trace, or measured value reproduction_steps: commands or user steps needed to reproduce expected_behavior: what the user or assistive technology should experience actual_behavior: what the test observed trend: new | persistent | worsening | improving | resolved

Guidelines:
- Treat WCAG 2.2 AA as the current planning and testing target. Treat Section 508 as regulatory context only when the project scope explicitly requires it.
- Use stable fingerprints to support trend language across reruns. Prefer component name + selector/accessibility target + rule/pattern + criterion over route-only fingerprints.
- Mark perspective alarms only when the evidence suggests a perspective-specific access risk. Any MEDIUM or HIGH alarm can feed `perspective-audit`.
- Do not copy scanner/runtime code or generated dashboard state from external projects into this skill. This contract is a reporting discipline, not a crawler product boundary.
finding_id: 稳定的小写ID,例如a11y_form_error_describedby fingerprint: 从组件/目标/规则派生的稳定8-64字符十六进制哈希,而非仅爬取URL source: 测试命令、测试文件、axe规则ID、agent-browser引用或观察到的工件 wcag_or_apg: WCAG 2.2准则或WAI-ARIA APG模式引用 section_508_fpc_context: 仅在适用时提供Section 508上下文;修订后的Section 508将Web合规性映射到WCAG 2.0 A/AA级别 severity: CRITICAL | MAJOR | MINOR | ENHANCEMENT perspective_alarms: screen_reader_semantic=LOW|MEDIUM|HIGH; keyboard_motor=LOW|MEDIUM|HIGH; etc. evidence: file:line, DOM摘录, axe节点, 截图, 键盘跟踪, 或测量值 reproduction_steps: 重现所需的命令或用户步骤 expected_behavior: 用户或辅助技术应体验的内容 actual_behavior: 测试观察到的内容 trend: new | persistent | worsening | improving | resolved

准则:
- 将WCAG 2.2 AA作为当前规划和测试目标。仅当项目范围明确要求时,将Section 508作为监管上下文。
- 使用稳定指纹支持重新运行时的趋势语言。优先使用组件名称+选择器/可访问性目标+规则/模式+准则,而非仅路由指纹。
- 仅当证据表明存在特定视角的访问风险时,标记视角警报。任何MEDIUM或HIGH警报均可输入到`perspective-audit`。
- 不要将外部项目的扫描器/运行时代码或生成的仪表板状态复制到此技能中。此契约是报告规则,而非爬虫产品边界。

5. Static Analysis (eslint-plugin-jsx-a11y) — React/Vue/JSX only

5. 静态分析(eslint-plugin-jsx-a11y)——仅适用于React/Vue/JSX

Use when the project uses React, Next.js, Vue, or other JSX/TSX framework. Catches missing alt text, invalid ARIA, and inaccessible element nesting at build time — no running server needed.
当项目使用React、Next.js、Vue或其他JSX/TSX框架时使用。在构建时捕获缺失的替代文本、无效ARIA和不可访问的元素嵌套——无需运行服务器。

Setup

设置

bash
undefined
bash
undefined

Install as dev dependency

安装为开发依赖

pnpm add -D eslint-plugin-jsx-a11y # or npm/yarn
pnpm add -D eslint-plugin-jsx-a11y # 或npm/yarn

Create temporary standalone config (avoids ESLint 9 flat config issues)

创建临时独立配置(避免ESLint 9扁平配置问题)

cat > eslint.a11y.mjs << 'EOF' import jsxA11y from "eslint-plugin-jsx-a11y"; import tseslint from "typescript-eslint"; export default [{ files: ["src//*.tsx", "src//*.jsx"], plugins: { "jsx-a11y": jsxA11y }, languageOptions: { parser: tseslint.parser, parserOptions: { ecmaFeatures: { jsx: true } }, }, rules: { ...jsxA11y.flatConfigs.recommended.rules }, }]; EOF
cat > eslint.a11y.mjs << 'EOF' import jsxA11y from "eslint-plugin-jsx-a11y"; import tseslint from "typescript-eslint"; export default [{ files: ["src//*.tsx", "src//*.jsx"], plugins: { "jsx-a11y": jsxA11y }, languageOptions: { parser: tseslint.parser, parserOptions: { ecmaFeatures: { jsx: true } }, }, rules: { ...jsxA11y.flatConfigs.recommended.rules }, }]; EOF

Run

运行

npx eslint --config eslint.a11y.mjs src/
npx eslint --config eslint.a11y.mjs src/

Clean up temp config (keep the plugin installed)

清理临时配置(保留插件安装)

rm eslint.a11y.mjs
undefined
rm eslint.a11y.mjs
undefined

Known False Positives

已知误报

Custom component
role
props, ARIA passed via spread, dynamic content loaded post-render, Next.js
<Link>
components (render valid anchors at runtime).
自定义组件
role
属性、通过spread传递的ARIA、渲染后加载的动态内容、Next.js
<Link>
组件(运行时渲染有效的锚点)。

ICT Testing Baseline coverage crosswalk (declared Section 508 audits only)

ICT测试基线覆盖交叉表(仅适用于声明的Section 508审核)

When an audit-scope engagement declares Revised Section 508, its baseline-coverage statement is sourced from references/ict-baseline-crosswalk.yaml: a hand-built map of all 62 active web baseline tests (pinned at
atbcb/ICTTestingBaseline
main
@
6c537a3b
, 2026-08-12) to the execution modes above and the evidence artifact each produces — 22 covered / 26 partial / 13 not-covered / 1 always-passes. The gate is the planner federal profile's conformance floor declaration in the engagement's audit-scope plan: no floor declaration → no baseline citations in any output, and a baseline citation in a component-scope review is a finding against the output. One exemption: an engagement-independent capability statement quoting the crosswalk verbatim ("designed to cover N of 62; gaps: ...") may answer a pre-award/procurement capability question with no floor declaration — findings, reviews, and reports stay gated.
Rules:
  • The not-covered and partial rows are the deliverable. They name what gets assigned to manual/AT methods in the evaluation plan's sampling and coverage boundary. Never imply stack coverage the crosswalk doesn't grant; a test classified judgment-only in the manifest is never
    covered
    .
  • Every baseline test ID cited must exist in
    docs/ict-baseline-test-id-manifest.yaml
    for the web baseline.
    IDs are valid per-baseline (
    11.A-PageTitled
    is web-only;
    11.A-DocumentTitled
    is documents-only), and baseline IDs are the exact-ID class models fabricate — hand-verify every one; never generate them.
  • Phrasing: "designed to cover N of 62; gaps: ..." — never "baseline-aligned" or "baseline-conformant" (alignment recognition is an external review of a test process), and never any Trusted Tester certification claim (a DHS credential held by humans).
  • 24.A-Parsing
    always passes by upstream design
    (WCAG 2.0 Errata 13) — execute nothing for it; report real markup consequences under the SCs they actually break.
  • The Electronic Documents baseline is out of measurement scope entirely (web-only stack): document samples go to the report's coverage boundary with a manual/AT method, never to these execution modes.
  • Maintenance: the crosswalk is rebuilt by hand against
    docs/ict-testing-baseline-reference.md
    on that reference's recheck triggers — a regenerated crosswalk without value-checking is the fabrication failure mode by construction.
当审核范围参与声明修订后的Section 508时,其基线覆盖声明源自references/ict-baseline-crosswalk.yaml:手动构建的所有62个活动Web基线测试(固定在
atbcb/ICTTestingBaseline
main
@
6c537a3b
,2026-08-12)与上述执行模式的映射,以及每个模式产生的证据工件——22个覆盖/26个部分覆盖/13个未覆盖/1个始终通过。门槛是参与审核范围计划中规划者联邦配置文件的合规下限声明:无下限声明→任何输出中均无基线引用,组件范围评审中的基线引用是针对输出的发现结果。一个例外:独立于参与的能力声明逐字引用交叉表(“设计覆盖62个中的N个;差距:...”)可在无下限声明的情况下回答预授予/采购能力问题——发现结果、评审和报告仍受门槛限制。
规则:
  • 未覆盖和部分覆盖行是交付成果。它们指定了评估计划的采样和覆盖边界中分配给人工/AT方法的内容。绝不暗示交叉表未授予的堆栈覆盖;清单中分类为仅判断的测试永远不是
    覆盖
    的。
  • 引用的每个基线测试ID必须存在于Web基线的
    docs/ict-baseline-test-id-manifest.yaml
    。ID按基线有效(
    11.A-PageTitled
    仅适用于Web;
    11.A-DocumentTitled
    仅适用于文档),基线ID是类模型生成的精确ID——手动验证每个ID;绝不生成它们。
  • 措辞: “设计覆盖62个中的N个;差距:...”——绝不使用“基线对齐”或“基线合规”(对齐认可是对测试过程的外部评审),绝不使用任何Trusted Tester认证声明(人类持有的DHS凭证)。
  • 24.A-Parsing
    按上游设计始终通过
    (WCAG 2.0勘误13)——无需执行任何操作;根据它们实际违反的SCs报告真实标记后果。
  • 电子文档基线完全超出测量范围(仅Web堆栈):文档样本进入报告的覆盖边界,采用人工/AT方法,绝不进入这些执行模式。
  • 维护: 交叉表在参考的重新检查触发时,根据
    docs/ict-testing-baseline-reference.md
    手动重建——未经值检查的重新生成交叉表是构造上的伪造失败模式。

Test Execution Order

测试执行顺序

  1. Static analysis (§5) — fast, no server needed
  2. Keyboard accessibility tests (§1)
  3. Visual regression tests (§2)
  4. axe-core automated scans (§4)
  5. WCAG compliance checks (§3)
  6. Time-based media tests (§5-media) — if applicable
  7. Screen reader tests (§6) — if applicable
  8. Report consolidated results with pass/fail counts per section
Lifecycle integration: These test results feed into a11y-critic reviews. The full a11y lifecycle is: plan → [generate test scripts] → critique plan → revise → implement → test (this skill) → critique implementation → fix → re-test
Webwright script generation fits between "plan" and "critique plan" — use it to generate test scripts from the planner's output before running them. Generated scripts are inputs to the test phase, not a replacement for it.
  1. 静态分析(第5节)——快速,无需服务器
  2. 键盘可访问性测试(第1节)
  3. 视觉回归测试(第2节)
  4. axe-core自动化扫描(第4节)
  5. WCAG合规性检查(第3节)
  6. 基于时间的媒体测试(第5节-媒体)——如适用
  7. 屏幕阅读器测试(第6节)——如适用
  8. 报告合并结果,按部分统计通过/失败计数
生命周期集成: 这些测试结果输入到a11y-critic评审中。完整的可访问性生命周期是: 规划 → [生成测试脚本] → 评审规划 → 修订 → 实现 → 测试(此技能) → 评审实现 → 修复 → 重新测试
Webwright脚本生成适合在“规划”和“评审规划”之间——使用它从规划者的输出生成测试脚本,然后再运行它们。生成的脚本是测试阶段的输入,而非替代测试阶段。",