mcpField
V
visual-qa

visual-qa

github.com/rheinmir测试与质量1079 次安装2 周前更新
代码仓库

关于 visual-qa

visual-qa 是面向检查本地 SPA 的无头视觉测试工作流。它结合截图基线对比、页面内设计与无障碍检查,并要求逐页审阅后再复验。

主要功能

  1. 01截取路由截图并生成清单
  2. 02与像素基线进行对比
  3. 03检查页面内设计与无障碍规则
  4. 04逐路由审阅问题
  5. 05修复后重新截图验证

适用场景

  1. 01检测重新设计后的意外视觉变化
  2. 02验证应用不同主题和状态下的 UI 改动
  3. 03浏览器扩展无法捕获 localhost 时执行无头视觉检查

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rheinmir/setup --skill visual-qa

安装后在 Claude Code 中输入/visual-qa确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/
.gitignore
package-lock.json
package.json
route-shots.mjs

详细介绍

技能:visual-qa — 视觉自检工作流(capture → diff → judge → fix → re-verify)

存在的原因: Agent 修改 UI 后自夸是系统性偏差(失败 ui-pass-without-full-visual-review)。拍出的截图没人打开 = 等于没测试。这个工作流 用三层独立检查取代“Agent 自己看一眼就判定通过”,其中两层由机器完成。

2026/07/13 决策(来自 /last30days): 对于 AI 生成的 UI,行业只有一项成熟做法—— 基线快照 + 像素差异(argos-ci/argos:“detects unintended visual changes”; Playwright toHaveScreenshot)。而“LLM-judge 按 rubric 给截图打分”仍处于前沿探索阶段 (Agent-as-a-Judge,论文阶段)——所以我们自己实现这一层,不等别人。

内容

目的和背景
  • 目的: 用三层独立检查取代“Agent 自己看一眼就判定通过”,用于本地 SPA——基线像素差异(机器)、设计/a11y 不变量(机器)、按 rubric 完整阅读图片(Agent)——只有机器和人工检查都通过,并且有截图文件作为证据,才能通过。
  • 触发条件(何时使用):
    • 刚修改了主题 / UI / 品牌重塑,需要确认是否意外破坏了什么。
    • Frame 的 ui_role≠none 需要验收门禁(参见技能 br § 视觉自检循环)。
    • 浏览器扩展无法截取 localhost(很常见)→ 使用 headless CLI 替代。
    • 用户说“视觉 QA”、“visual qa”、“route-shots”、“逐个 route 截图”、“检查界面”、“基线差异”、“UI 是否有回归”、“headless 界面测试”、“/visual-qa”。
  • 非目标: 不与 computer-use(桌面 AX / Orca 浏览器)重叠;不是 senior 的 4 项评审(那是 /qc-uiux,使用此引擎);不放宽阈值来“消除错误”。
心智模型

app 运行中 + 真实 route 列表 + 登录 → route-shots(截图 + MANIFEST,所有主题/状态)→ 第 1 层基线差异(阈值 0)→ 第 2 层页面内不变量(contrast-aa · monochrome-surface · rogue-slab · shadow-clipped · overlap · row-misalign)→ 第 3 层按 rubric 阅读全部 N 张截图 → FINDINGS.md → 修复 → 重新截图 + 重新阅读。

输入和输出约定
字段必填?含义
输入app 正在运行(--base)是SPA localhost
输入来自真实 router 的 route 列表 + 登录账户是不要猜;登录失败时门禁必须失败
输入来自 app 本身的主题/状态列表是app 支持多个主题时,不要硬编码 light,dark
输入基线目录否首次运行时自动创建
输出<out>/*.png + MANIFEST.json是没有文件 = 从未截图
输出使用 --assert 时的退出码门禁必需route 出错 / 缺少截图 / 违反不变量 / 与基线不符时为 1
输出FINDINGS.md是每个 route 一行,包括“正常”的 route
规则与能力

血的教训——不要重蹈覆辙:

  • RULE-01 (MUST):凡是能计算的,就必须交给机器计算——禁止靠肉眼判断。文字对比度是数值(WCAG ratio = f(文字颜色,背景颜色))。Rubric 写着“文字达到 AA”,却没有设置不变量 ⇒ evaluator 盲目:模糊的文字会漏过整个登录页,Agent 看截图也无法想象出来。凡是可量化的标准 → 必须变成 assert,而不是 rubric 里的文字。
  • RULE-02 (MUST):任何路由都不能成为盲区。Driver 先登录 ⇒ /auth 重定向 ⇒ 登录页面永远不会被审计——偏偏有问题的就是这个页面。必须先截取未登录状态,再登录。检查一下:是否还有某些状态(modal、error、empty、loading)从未出现在截图中?
  • RULE-03 (MUST):主题是一种状态——light 和 dark 都要完整截取。App 通常通过 class(<html> 上的 .dark)根据 app 设置 启用 dark,而不是根据 prefers-color-scheme ⇒ headless 默认永远只会看到 light,dark 则完全成为盲区:用户在 dark 下打开 app,UI 已经损坏,gate 依然显示绿色。启用 --themes light,dark(默认)→ 首次运行就发现 dark 下有 172 处未达到 AAA,而 light 则毫无问题。设计上的结论:accent 必须随主题切换(浅色表面 → 深色 accent;深色表面 → 浅色 accent);两个主题共用同一个 accent 常量,从定义上就是错的。并且禁止在 accent 上硬编码文字颜色(color:#fff)——切换到另一个主题就会变成白字白底,只有 1.13:1。
  • RULE-04 (MUST):覆盖 app 提供的所有主题——主题列表必须从 app 获取,不能自行猜测。我把 light,dark 硬编码了,但 app 有三个主题(default / default-dark / paper)⇒ paper 无人检查 ⇒ 它和 dark 一样会损坏,发现问题的是用户,不是 gate。规则:运行前,从 app 本身读取主题列表(Settings→Preferences 下拉菜单 / source THEME_OPTIONS),然后全部覆盖。推而广之:所有状态维度(theme、locale、role、empty/full data)都必须从 app 获取,不能靠 Agent 的记忆。
  • RULE-05 (MUST):改变状态必须走 app 实际使用的变更路径——然后验证状态确实改变了。接连两层都出错:① 手动添加 .dark class(app 使用 data-theme + 注入 CSS ⇒ 这种机制根本不存在);② 设置 localStorage(app 会根据服务器上的账户设置覆盖它)。两次 gate 都在不存在的状态下显示绿色——比显示红色更糟。唯一可靠的路径是用户实际点击的路径(Settings → Preferences → 选择)。并且始终要有一个不变量:两个不同主题的背景却一模一样 ⇒ 截图是假的 ⇒ ASSERT FAIL(gate 连续截了两次同一个主题,还自我表扬)。
  • RULE-06 (MUST):主题是 app 的机制,先读 source。不要假设 .dark / prefers-color-scheme。搜索 documentElement.classList / data-theme / localStorage / 注入的 <style>,确认 app 如何切换主题;主题通常是在运行时注入 CSS ⇒ 对常规 token,它会覆盖我自己的静态主题文件;但它会被 !important 规则覆盖 ⇒ 结果就成了不伦不类的界面(app 的主题背景 + 我自己的主题 card = 页面中间出现一块奇怪的颜色)。正确的修复方式:把我自己的配色写进 app 注入的主题文件本身,不要在 app 根本不会读取的地方写 .dark{} 块。
  • RULE-07 (MUST):**登录失败 = gate 必须失败。**登录失败 ⇒ 所有路由都重定向到 /auth ⇒ driver 审计了 8 张一模一样的登录页面,然后打印“design-ok”。这种绿色比红色更糟:它确认了一个自己从未见过的 app。登录失败时,--assert 必须立即失败。
  • RULE-08 (MUST):用户报告问题 + 机器报告正常 ⇒ 机器错了,不是用户错了。(2026 年 7 月 14 日的教训——用户:“这是开发,不是法庭,你为什么要如此固执地为自己的论点辩护”。)Gate 显示绿色不是挡箭牌:它只能证明它被教会的内容,以及它实际截取到的状态。用户看到问题 = 存在一个 gate 从未进入过的 state/theme/bundle。禁止用猜测反驳用户(“可能是缓存导致的”“可能按错键了”)——每次猜测都是把验证工作推给用户,而我已经连续 3 次在这方面犯错。正确流程:(1) 相信用户的报告;(2) 复现用户的准确状态(route + theme + bundle + 登录/focus 状态);(3) 只有测量之后才能下结论。无法复现就直说“目前无法复现”,不要编造假设再为其辩护。
  • RULE-09 (MUST):截图前先消除随机因素,不要放宽阈值。闪烁的光标 + animation 会让每条route 每次截图都偏离 baseline 约 0.04% ⇒ gate 永远显示红色 ⇒ 结果被忽视 ⇒ 真正的 regression 漏过。Driver 在初始化时注入 CSS animation/transition:none; caret-color:transparent + 隐藏 CodeMirror 光标 ⇒ diff 降至 0%。放宽 --diff-threshold 来“让它不再报警”就是让机器失明,不是修复问题。
  • RULE-10 (MUST):过于宽泛的 SELECTOR = 自己坑自己。CSS 去噪规则中的 [class*="cursor"] 还会匹配 Tailwind 的 cursor-pointer/cursor-text ⇒ 输入框被隐藏 ⇒ 无法登录 ⇒ gate 把正在检查的 app 弄坏了。Gate 注入的所有 CSS/selector 都必须尽可能精确,添加之后还必须运行完整流程(登录 → 所有 route)。
  • RULE-11 (MUST):**Gate 必须能从干净的 repo 运行。**缺少 playwright-core ⇒ node route-shots.mjs 会在 import 行立即报错退出 ⇒ UI frame 的 acceptance_test 永远不会运行 = gate 已死,但无人知晓。依赖项声明在 skills/visual-qa/assets/package.json;安装:npm i --prefix skills/visual-qa/assets。
  • RULE-12 (MUST):**证据 > 口头声明。**只有存在图片文件时,pass/fixed 才有效。没有文件 = 没做。禁止根据推断声称 pass。
  • RULE-13 (MUST):**全部读完,不要挑着读。**读了 3/7 张图片就得出结论,是 ui-pass-without-full-visual-review failure 的根本原因——根因就在尚未打开的图片里。
  • RULE-14 (MUST):“机器 OK” ≠ “UI 好看”。不变量只知道它被教会的内容。Gate 显示绿色不是跳过阅读图片这一步的借口。
  • RULE-15 (MUST):**阈值是一把双刃剑。**阈值太宽松 → 真实问题漏过(0.5% diff 就漏掉了把整张 card 变平的 regression)。阈值太严格 → 误报警 → 嘈杂的 gate = 死掉的 gate。Baseline 使用阈值 0 + 人工审核;DOM 不变量必须设置能区分真实问题与噪声的阈值。
  • RULE-16 (MUST):CSS 要测量,不要猜。仅凭肉眼诊断连续错了 4 次(裁切 → 冲突 → selector 错误 → specificity 错误)。只有查询 getComputedStyle 才找到根因。任何 CSS 修复之前:先测量 computed style + rect,再修改。修改之后:先重新测量,再看图片。
  • RULE-17 (MUST):**不要为了“消除错误”而让机器失明。**把 blur 降到 detector 阈值以下 = 掩盖问题,而不是修复问题(光晕依然被裁切)。修复缺陷,不要改量尺。
  • RULE-18 (MUST):在 overflow:hidden 容器中(窄 rail、tab bar):禁止使用外扩阴影。阴影会被裁切或溢入缝隙 → 留下难看的痕迹。使用内阴影(不会溢出)或不加阴影。若另一个轴为 auto,则 overflow-x:visible 不可行(CSS 会将其强制改为 auto)。
  • RULE-19 (MUST):使用 playwright-core + 系统 Chrome(不要下载 130MB 的 chromium)。Baseline diff 需要 pixelmatch + pngjs(体积小,缺少它们时会自动跳过 baseline)。
  • RULE-20 (MUST):实时 SPA 禁止使用 networkidle——使用 domcontentloaded + 固定等待时间。
  • RULE-21 (MUST):不要与 computer-use 冲突(桌面 AX / Orca 浏览器)。此 skill 是面向 localhost SPA 的 headless CLI。
  • 能力:在 localhost 上运行 headless 浏览器;在页面内测量 computed style + rect;与 baseline 进行像素比较;记录图片/manifest/findings;修复目标项目的 UI。
失败边界
  • 登录失败 → --assert 立即失败,不要审核登录页面的副本。
  • 缺少 playwright-core → gate 在 import 行就会终止:blocked,安装 npm i --prefix skills/visual-qa/assets。
  • 基线差异 ≠ 0 → failed,直到查看并通过 --update-baseline 批准。
  • 用户报告错误,但机器报告正常 → 机器有误:复现相同状态;如果无法复现,就直说“无法复现”。
  • 机器检查正常,但尚未读完 N 张图片 → 尚未完成。

操作方式

主要工作流

W01–W07 分别对应下面的步骤 0–6。

StepTypeInputsActionOutputs/exitFailure/next
W01deterministicapp、路由、账户确认 app 正在运行,从 app 获取真实路由列表 + 主题列表路由 + 主题列表app 未运行 → blocked
W02effect路由列表使用 route-shots.mjs 截图(+ --assert)图片 + MANIFEST.json登录失败 → failed
W03deterministic图片 + 基线基线差异阈值为 0差异≠ 0 → 查看并批准(B02)或修复
W04deterministic页面内检查机器检查的不变量违规或正常硬性 FAIL → W06
W05judgmentMANIFEST 中的 N 条路由Read 全部 N 张图片,按评分标准打分每条路由一行有图片尚未阅读 → 尚未完成
W06effect发现项FINDINGS.md(根本原因)→ 修复已修复代码—
W07deterministic已修复代码重新截图 → 再次阅读机器检查正常且人工检查正常仍有问题 → W06

各步骤详情(W01–W07 的事实依据):

0. App 必须正在运行。 确认路由列表(从真实路由器获取,不要猜测)+ 登录账户。

1. 截图 + 证据。

node skills/visual-qa/assets/route-shots.mjs \
  --base http://localhost:5230 --out <dir>/qa-shots \
  --baseline <dir>/qa-baseline \
  --user demo --pass demo --assert

生成 <out>/*.png + MANIFEST.json(路由 → 文件 → 字节数)。没有文件 = 从未截图。

2. 基线差异(机器第 1 层)——捕获非预期变更。 首次自动创建基线。之后逐像素比较。默认阈值 = 0:任何差异 ≠ 0 都会 FAIL,且必须查看并通过 --update-baseline 手动批准。这是唯一能捕获以下类型错误的层: CSS 不变量检查和 Agent 肉眼都看不见的错误(例如:修复过度导致整个 UI 扁平化)。

3. 机器检查的不变量(机器第 2 层)——设计一致性 + 无障碍。 在页面内使用计算样式运行:

  • contrast-aa — 所有文本节点均符合 WCAG AAA(根据文字颜色 + 有效背景计算对比度;普通文字 7:1,大号文字 4.5:1)。硬性 FAIL。
  • monochrome-surface — 大面积窗格(sidebar/aside/nav)必须使用统一背景色。硬性 FAIL。
  • rogue-slab — 所有面积 ≥ 视口 2%、背景不透明且与 body 背景对比度 > 1.6:1 的元素。硬性 FAIL。 (monochrome-surface 只检查 sidebar/aside/nav,因此无法发现页面中任意 <div> 上的异常色块。)
  • shadow-clipped — 父元素 overflow:hidden 导致外扩光晕被裁切(带模糊阈值)。警告。
  • overlap — 两个控件(button/a/input/[role=button])不是父子元素,且矩形在两个方向上均重叠 ≥4px。捕获按钮相互覆盖的问题(例如 position:fixed 搭配 magic-offset,而 badge 宽度会变化)。硬性 FAIL。
  • row-misalign — 同一父元素下、处于近似同一行(top 偏差 ≤6px)的控件,高度差 >3px。捕获相邻按钮高度不一致的问题(例如 toggle 按钮因 padding/font 不同而比 badge 高)。硬性 FAIL。 (这两条规则于 15/07/26 添加,因为 /redesign-existing-projects(读取静态 CSS,无法测量矩形)和单元测试(检查内容,不检查坐标)都无法发现渲染时的几何错误。overlap 阈值故意不用面积百分比:真正的 bug 可能只裁切按钮边缘,占 18% 面积,任何百分比阈值都会漏掉——两个独立控件只要重叠几像素就算错误。)

根据项目的设计规范扩展不变量。

4. 阅读图片——严格覆盖率 + 评分标准(人工/Agent 层)。 MANIFEST 有 N 条路由 → 必须 Read 全部 N 张图片。FINDINGS 必须为每条路由提供一行 (包括“正常”)。按评分标准打分,不凭感觉——例如 neumorphism:① 单色表面 ② 阴影未被裁切 ③ CTA 使用扁平强调色 ④ 文字/图标符合 AA ⑤ 空状态符合品牌规范 ⑥ 焦点环。

5. FINDINGS.md → 修复。 表格格式:# · 路由 · 问题 · 严重程度 · **根本原因** · 修复方法。

6. 重新截图 → 再次阅读 → 才算通过。 机器检查正常且人工检查正常。缺少任意一项 = 尚未完成。

分支
ID类型守卫条件行为跳过 / 失败汇合点
B01conditional_requiredframe gate 只需 1 条 route运行 --route <r>—W03
B02user_optional相对基线的偏差是已审阅的有意变更--update-baseline未查看图片 → 不得批准W04
B03capability_optional有 pixelmatch + pngjs运行基线差异比对没有 → 自动跳过基线W04
B04recovery用户报告问题,但 gate 显示通过复现完全相同的状态(route + theme + bundle + 登录状态 / 焦点),测量后再反馈无法复现 → 如实说明W02
验证与停止条件

机器:--assert 的退出码(route 出错 / 缺少图片 / 不变量 / 基线)。人工:FINDINGS 中 MANIFEST 的每条 route 都有对应的一行。只有两者都没有问题且存在图片文件时才算通过;没有文件 = 尚未完成。

示例
  • 正向: 更改应用主题 /br 后,THEME_OPTIONS 生成 7 条 route × 3 种主题 → route-shots.mjs --assert 退出码为 0,基线差异为 0%,Agent 用 Read 查看所有图片,FINDINGS 有 7 行“干净”结果 → 通过并附上图片。
  • 边界 / 失败: light 正常,但尚未覆盖 dark → 启用 --themes light,dark 后发现 172 处未达到 AAA → --assert 退出码为 1;修复随主题切换的 accent 后重新截图,不要放宽 --diff-threshold。
参考 — Driver 标志

--base --out --user --pass · --route <r>(1 条 route,用于 frame gate)· --assert (以下情况退出码为 1:route 出错 / 缺少图片 / 违反不变量 / 基线偏差)· --baseline <dir> · --update-baseline(批准有意变更)· --diff-threshold <%>(默认值为 0)。

评论

支持 Markdown