首页
/ Langflow PR 级无障碍修复实战:IBM Equal Access Level 1 双引擎扫描与修复闭环

Langflow PR 级无障碍修复实战:IBM Equal Access Level 1 双引擎扫描与修复闭环

2026-09-04 16:36:36作者:凌朦慧Richard

Langflow 仓库通过 .agents/skills/ibm-a11y-pr-remediation/SKILL.md 定义了一套面向 PR/分支的前端无障碍(a11y)修复编排流程:自动收集当前 PR 触及的所有前端文件,将其映射到具体路由与组件状态,同时运行 axe 与 IBM Equal Access 两套扫描引擎,并默认直接修复全部 Level 1 范围内违规,直到复扫全部转绿(或仅剩有文档记录的基线债务)。读完本文,你可以掌握 Langflow 中"变更收集 → 表面映射 → 双引擎扫描 → 最小化修复 → 补测试 → 断言复扫 → 报告"的完整实战链路,以及 RUN_A11Y/RUN_A11Y_ASSERT 开关、IBM ACE 扫描脚本、基线文件等底层机制。

技能定位:PR 范围的修复编排器,而非引擎文档

该 Skill 的 frontmatter 明确声明了范围与模式(见 SKILL.md):

  • 范围仅限 IBM Equal Access Level 1,不扩展到 Level 2/3(除非用户明确要求);
  • 默认模式是 fix 而非 report-only:用户说"检查/扫描/清理这个 PR 的无障碍问题并希望应用修复"时触发;只要一份范围受限的审计报告则应改用姊妹技能 ibm-a11y-level1-audit,仅做批量路由扫描则用 ibm-a11y-route-scan
  • disable-model-invocation: true 表示该技能不由模型自动触发,只接受显式调用。

它是一个"编排器",刻意不重复引擎细节,而是链接到四个配套技能文档:

Mandate:七步强制流程

原文档给出的 Mandate 是整篇技能的核心契约:

  1. 对 PR(或当前分支与其 merge base)做 diff,找出 src/frontend/** 的变更;
  2. 将变更文件映射到 UI 表面 → 路由/组件/状态;
  3. 同时运行 axe(适用时走 Jest)与 IBM Equal Access(Playwright page.runA11yScan 和/或 scripts/a11y/a11y_scan.py);
  4. 修复所有范围内的 Level 1 违规——除非用户明确要求"仅报告"(此时移交给 ibm-a11y-level1-audit);
  5. 复扫直到 assert 模式通过;覆盖缺失时补充/更新 a11y 测试;
  6. 报告:改了什么、跑了什么命令、哪些债务被基线化或延期;
  7. 禁止行为:不得发明新的 tag 名、不得静默关闭扫描、不得擅自扩展到 IBM Level 2/3。

对应的进度检查清单:

IBM L1 PR A11y:
- [ ] 1. Collect changed frontend files
- [ ] 2. Map files → surfaces / routes / states
- [ ] 3. Scan (IBM + axe)
- [ ] 4. Fix all in-scope violations
- [ ] 5. Add/update tests if needed
- [ ] 6. Re-scan assert-green
- [ ] 7. Report back

第 1 步:收集变更的前端文件

优先使用 PR 的 merge base;没有 PR 时用当前分支对 main/master 的 merge base。原文档给出的完整命令集:

# PR number known
gh pr diff <n> --name-only | grep -E '^src/frontend/' || true

# Current branch vs upstream default
BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD origin/master)
git diff --name-only "$BASE"...HEAD -- 'src/frontend/**'

# Include uncommitted work when the user is mid-change
git diff --name-only HEAD -- 'src/frontend/**'
git diff --name-only --cached -- 'src/frontend/**'
git ls-files --others --exclude-standard 'src/frontend/**'

纳入范围与排除规则:

  • 纳入 src/frontend/src/**/*.{tsx,ts,jsx,js,css}(UI 代码)、src/frontend/tests/a11y/**(既有 a11y 覆盖),以及仅当 locale 文件变更影响可访问名称/标签时才纳入;
  • 跳过纯非 UI 的改动,除非它影响 a11y(例如改变焦点/ARIA 的测试 helper);
  • 若没有任何前端文件变更,明确说明后直接停止——这是该流程的第一道快速出口。

第 2 步:把文件映射到扫描表面

对每个变更文件,按变更类型确定扫描目标(原文档表格,完整保留):

变更类型 扫描目标
页面 / 路由 该路由 + 有意义的状态(空/有数据/模态/移动端)
共享组件(TableComponent、对话框、菜单) 所有使用它的消费页面——而不只是你改动的文件
基础原语 该原语的 Jest axe 测试 + 嵌入它的 Playwright 表面
仅 spec / 基线 重跑该 spec;失败才需要产品代码修复
a11y_routes.json 更新静态覆盖;跑静态扫描或路由扫描

映射时要列出交互控件与状态(默认、空、有数据、打开的模态/菜单、选中行、错误、移动端),并优先复用 src/frontend/tests/a11y/ 下既有 spec。

仓库中这套映射的"落地形态"是路由清单文件 a11y_routes.json:它声明了扫描前提(如 ENABLE_FILE_MANAGEMENT is true, so /assets routes are active),并为每条路由定义 pathsurface 描述与 ready 就绪选择器(例如 flows 路由等待 testId: mainpage_title,assets-files 路由还额外要求主内容出现"Files"文本与 upload-file-btn)。这保证了扫描只在页面真正渲染完成后进行,避免误报。

第 3 步:双引擎扫描(两者都必须通过)

原文档强调"自动化 a11y 不是一个工具能解决的",Langflow 采用双引擎分工:

  • axe-core:Jest 中的 axe()src/frontend/src/utils/a11y-test.ts),仅 jsdom 环境,适合组件级单测;
  • IBM Equal Access:对 ARIA 结构与键盘语义更严格。有状态表面(模态/菜单/选中/编辑态)用 Playwright page.runA11yScan(label);仅默认加载页面用 scripts/a11y/a11y_scan.py

引擎细节的源码佐证

Jest + axe 侧:共享的 axe 实例刻意禁用了 color-contrast 规则——jsdom 无法提供真实布局与 canvas,对比度只能由页面级 IBM 检查器覆盖(见 a11y-test.ts 中的注释)。这解释了为什么必须双引擎:单靠 Jest/axe 会漏掉对比度类问题。

Playwright + IBM ACE 侧runA11yScan 挂在共享 fixture 的 page 上(src/frontend/tests/fixtures.ts),其行为由两个环境变量控制:

const RUN_A11Y = process.env.RUN_A11Y === "true";        // 是否真正执行扫描
const RUN_A11Y_ASSERT = process.env.RUN_A11Y_ASSERT === "true"; // 是否对新违规断言失败
  • 未设置 RUN_A11Y 时,runA11yScan 直接返回 null,普通测试流程零开销;
  • 设置后每次扫描都会通过 IBM checker 的 getCompliance 取报告,并作为 Playwright 附件写入测试报告;
  • 再设置 RUN_A11Y_ASSERT 时,countNewA11yViolations 计算新增违规数并 expect(...).toBe(0)——即历史基线债务不阻塞,新违规立刻红。

Python 扫描器侧a11y_scan.py 是"路由感知的 IBM ACE 扫描器,附带 API 请求跟踪"(a11y_scan.py)。除文档中用到的 --url--routes--out--markdown--timeout-ms 外,脚本还提供了一组完整参数:

参数 默认值 说明
--routes 逗号分隔的路由列表
--route 可重复的单路由参数
--routes-file 路由清单 JSON(即 a11y_routes.json 的格式)
--route-group static 清单中要扫描的路由分组
--states-file 路由加载后执行的模态/状态动作 JSON
--levels violation violation,potentialviolation,recommendation,manual
--timeout-ms / --quiet-ms 30000 / 1000 页面加载与网络安静等待超时
--out / --markdown / --html a11y-scan-report.json JSON / Markdown / 自包含 HTML 报告
--ace-url unpkg 上的 accessibility-checker-engine 自定义 IBM ACE 引擎脚本地址
--browser-executable / --headed 空 / 关闭 指定 Chromium 可执行文件;有头模式

脚本同时会过滤同源的 /api//health/config 请求并排除静态资源,从而把"页面加载期间发起的 API 调用"记录进报告,辅助判断某路由依赖哪些后端数据。

可直接复制的扫描命令

IBM 引擎(Playwright 集成):

cd src/frontend
RUN_A11Y=true RUN_A11Y_ASSERT=true npx playwright test tests/a11y/<feature>.a11y.spec.ts --project=chromium --workers=5

# Python scanner 的 playwright 依赖不在默认 uv sync 里。
# 一次性准备:uv run --with playwright playwright install chromium
uv run --with playwright python scripts/a11y/a11y_scan.py \
  --url http://localhost:3000 \
  --routes /settings/<route> \
  --out /tmp/a11y.json --markdown /tmp/a11y.md --timeout-ms 45000

组件级(仅组件变更时):

cd src/frontend
npx jest path/to/<name>.a11y.test.tsx --runInBand

两条纪律:RUN_A11Y_ASSERT=true 下任何违规都会使测试失败;改过共享组件后,必须重扫所有使用它的页面。另外原文档明确"不得发明发现"——以扫描器输出为准,再辅以扫描器扫不到的 Level 1 人工抽查。

第 4 步:修复所有范围内的 Level 1 违规

默认动作是修复;只有用户说"report only"时才列出拟议修复而不改代码,并移交给 ibm-a11y-level1-audit 出正式报告。

修复规则(原文档完整保留):

  • 优先语义化 HTML 而非 ARIA;
  • 遵循 ibm-a11y-testing-guide 中的 Langflow 模式(AG Grid、Radix asChild、焦点还原、纯图标按钮加 aria-label);
  • 新 UI 字符串与 aria-label 一律走 i18n(t(...),全部 locale 文件),遵循 frontend-i18n
  • 修复保持最小化,不顺手重构无关 UI;
  • 不得静默关闭扫描。只有文档化的框架债务才允许放进 src/frontend/tests/a11y/baselines/ 的 IBM 基线;
  • 每个问题映射到 Level 1 的 WCAG/IBM 判据 ID;凡判据指南(ibm-level1-criteria.md)中标为 Level 2/3 的一律延期,除非用户扩大范围。

基线不是"免罪金牌"而是"受追踪的技术债"。仓库中真实例子 chromium__assets-files-actions-menu.jsondescription 字段写明了债务成因(Radix DropdownMenu 把内容 portal 到 <body>、脱离 landmark,导致 IBM 报 aria_content_in_landmark,且 <main> 是 overflow-hidden 无法简单改 portal 位置)、影响面(app 级 Radix 限制)、以及删除该文件即可让违规重新暴露的恢复路径——这正是"文档化债务"的样板写法。

人工 Level 1 抽查(扫描器盲区)

  • 2.1.1 / 2.1.2:Tab 与 Shift+Tab 全可达;Escape 关闭浮层;无键盘陷阱;
  • 2.4.3 / 2.4.7:焦点顺序匹配视觉顺序;焦点环可见;
  • 1.4.10:320px / 约 400% 缩放下无关键内容的横向滚动;
  • 1.4.1:状态/错误不能只靠颜色区分;
  • 3.3.1 / 3.3.2:错误以文本呈现并关联到对应字段;所有输入有标签。

第 5 步:测试与覆盖

原文档的"表面 → spec"对照表(与仓库实际文件一一对应):

表面 Spec
静态路由 static-routes.a11y.spec.ts(配合 a11y_routes.json
认证页 auth-pages.a11y.spec.ts
核心页面 core-pages.a11y.spec.ts
数据密集页 files.a11y.spec.tsapi-keys.a11y.spec.tsglobal-variables.a11y.spec.ts
其他数据密集路由 data-rich-routes.a11y.spec.ts

如果修复的状态没有扫描覆盖,就按 files.a11y.spec.ts / api-keys.a11y.spec.ts 的模式补一条(自定义键盘行为还要补键盘测试)。测试编写约定:

  • 每个 Playwright a11y 测试都打 @release 标签加一个领域标签(@workspace / @api / @database / @components / @starter-projects)——仓库中如 api-keys.a11y.spec.ts 即以 { tag: ["@release", "@api"] } 的方式落 tag;
  • test/expect 必须从 ../fixtures 导入(才能获得 runA11yScan 增强的 LangflowPage);
  • files.a11y.spec.ts 为例,spec 通过 page.route mock 掉 **/api/v2/files 注入固定数据、disableAnimations 关闭动画,使扫描在可复现的数据密集状态上进行。

第 6 步:复扫断言转绿并报告

用同样的命令、同样的 RUN_A11Y_ASSERT=true 重跑。每条发现必须落到三态之一:fixed(已修复)、baselined(已入基线并文档化)、open(仍开放,需说明原因)。

完成时的报告必须包含以下七项(原文档清单):

  1. 纳入考虑的前端变更文件;
  2. 扫描的表面 / 状态;
  3. 应用的修复(文件 + 修了什么);
  4. 执行过的命令——axe 与 IBM 两个引擎是否都跑过、是否都报告零新违规
  5. 新增/更新的 spec 或基线;
  6. 被跳过的状态及原因;
  7. 剩余风险或已接受的局限。

小结

这套 PR 级修复流程的设计要点可以归纳为四条:以 merge base diff 划定最小扫描范围;以"共享组件必须重扫全部消费方"堵住最常见的漏扫;以 RUN_A11Y/RUN_A11Y_ASSERT 两级开关把"扫描"与"门禁"解耦;以"基线只收文档化的框架债务 + 每条发现三态收尾"保证债务可见、可回溯。所有关键资产都集中在 src/frontend/tests/a11y/scripts/a11y/ 两处,配合 fixtures.ts 中的扫描挂载点,构成 Langflow 前端可访问性的完整工程闭环。

登录后查看全文
热门项目推荐
相关项目推荐