BrowserSkill 回归用例体系:用 manifest 与合成 fixture 固化真实浏览器几何回归缺陷
【免费下载链接】BrowserSkillLet AI agents use your real, logged-in browser without interrupting your work. CLI + extension for browser automation across any shell-capable AI agent.项目地址: https://gitcode.com/GitHub_Trending/br/BrowserSkill
回归用例是自动化测试体系中最难沉淀的一环:真实用户反馈的缺陷往往依赖特定的页面结构、嵌套 iframe、滚动状态与缩放组合,一旦缺少可复现的最小样例,修复便无从验证。BrowserSkill 在evals/browser/cases/regression/目录下建立了一套“一个用户反馈坏例 = 一个独立目录”的回归用例组织规范,配套 scaffold 脚手架命令、case manifest 声明式断言和合成 fixture,并以内置的真实 Chrome 几何测试与 CLI smoke 双重验证回归是否彻底修复。本文以该目录为骨架,深入剖析回归用例的目录规范、清单结构与运行方式,并以snapshot-coordinates(DOMSnapshot 布局单位回归)和oopif-scrollbars(OOPIF 占用滚动条回归)两个真实案例,展示 BrowserSkill 如何把 iframe 几何与坐标投影类缺陷固化为一套可重复执行的验证资产。
一、回归用例目录的职责与组织规范
regression/README.md 开篇即定义了该目录的核心职责:每一个用户反馈的坏例(badcase)都应拥有自己独立的目录,目录内包含三件套:
- manifest(
*.case.json):描述用例的声明式清单,包括提示词、覆盖操作、断言与 smoke 步骤; - 合成 fixture(
*.fixture.mjs):一段由服务端动态渲染的最小复现页面,专门构造触发缺陷的 DOM 结构; - README:描述症状(symptom)、最小复现(minimal reproduction)、期望行为(expected behavior)、源码参考(source reference)与修复方式(fix)。
这种“一坏例一目录”的隔离策略,保证了每个回归缺陷都能独立复现、独立调试、独立评审,而不会相互污染。同时,README 明确划出了敏感信息红线:
Never commit credentials, cookies, HAR files, production HTML, private screenshots, or proprietary assets.
即回归用例中严禁提交凭证、Cookie、HAR 抓包文件、生产环境 HTML、私有截图或专有资产。这一点对浏览器自动化项目尤为重要——fixture 必须全部由合成数据构造,绝不携带真实用户会话信息。从实际用例看,oopif-scrollbars.case.json 与 snapshot-coordinates.case.json 均通过合成 HTML 与回环主机名构造跨源拓扑,完全符合这一要求。
二、从零创建回归用例:scaffold 脚手架命令
目录规范要求每个用例必须同时具备 manifest、fixture 与 README,手工创建容易遗漏字段或写错目录结构。为此仓库提供了 scaffold 命令一键生成起点:
pnpm eval:browser scaffold <case-id> --title "..." --source "issue-or-pr"<case-id>:用例 ID,必须为小写 kebab-case(如oopif-scrollbars),由 scaffold-case.mjs 中的ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/强制校验;--title:用例标题;--source:来源标注,通常填 issue 或 PR 引用,用于追踪缺陷出处。
从 scaffold-case.mjs 的源码可以看到脚手架的产物结构:它在<cases>/<suite>/<id>下创建目录,生成一个默认 manifest(包含$schema、schemaVersion: 1、id、title、order、suite、tags、fixture.startPath、中英文prompts、coverage、assertions与smoke.steps),并生成对应的 fixture 模板,标记默认为CASE-<ID>(大写、连字符转下划线)。若目录已存在,脚手架会直接报错,避免覆盖既有用例。
三、case manifest 清单结构剖析
case manifest 是回归用例的“声明式核心”。其字段约束由 case-loader.mjs 中的validateCaseManifest完整实现,允许的顶层字段包括:$schema、schemaVersion、id、title、order、suite、tags、seed、source、fixture、prompts、coverage、assertions、smoke,其余未知字段会被直接判为错误。
关键字段的语义与约束如下:
| 字段 | 含义 | 校验要点 |
|---|---|---|
schemaVersion | 清单结构版本 | 必须等于1 |
id | 用例唯一标识 | 小写 kebab-case,且全局不允许重复 |
fixture.startPath | fixture 的起始 URL 路径 | 必须以/开头,与 fixture 中注册的 route 对应 |
prompts | 驱动 Agent 的提示词 | 必须同时提供en与zh-CN两种语言 |
coverage | 覆盖的操作清单 | 必须来自已知操作集合(如session.start、session.stop、page.navigate、inspect.observe),不允许重复 |
assertions | 三层断言 | 分site(页面/站点侧)、response(Agent 回复侧)、adapter(适配器/会话侧)三类 |
smoke | 最小冒烟步骤 | smoke.steps必须为数组,动作需在受支持的工作流动作集合内 |
其中assertions的三层设计值得展开:
- site 断言:验证页面侧状态,每个断言需要
type(如geometry.ready、geometry.scrollbars)、可选的where过滤器与minCount(正整数)。例如 oopif-scrollbars 用例要求data.root为真的geometry.ready事件至少出现 1 次,且data.vertical与data.horizontal同时为真的geometry.scrollbars事件至少出现 2 次; - response 断言:验证 Agent 最终回复文本,通过
includes检查是否包含标记字符串(如OOPIF-SCROLLBARS、GEOMETRY-195); - adapter 断言:验证适配器行为,通过
key(如sessionStopped)确认浏览器会话已被正确关闭。
在 case-loader.mjs 中,加载后的 manifest 还会被normalizeCase归一化:将fixture.startPath提升为startPath、三类断言与 smoke 步骤分别挂载为siteAssertions、responseAssertions、adapterAssertions、smokeSteps,并以Object.freeze冻结,保证后续执行阶段不会意外篡改清单。所有.case.json按 suite、order、id 排序后统一返回,任何清单的 JSON 解析失败、字段缺失或 ID 重复都会导致整个加载过程抛错,从源头拦截“带病”用例进入执行流程。
四、实战案例一:snapshot-coordinates(DOMSnapshot 布局单位回归 #195)
4.1 症状与根因
snapshot-coordinates/README.md 记录了该回归的来源为 PR #195,核心问题是:DOMSnapshot 的 bounds 与文档滚动偏移量保留的是 Blink 布局单位(layout units),而不是 CSS 像素。在设备缩放(device scale)或浏览器缩放(zoom)下,若直接将它们当作 CSS 像素处理,位置与尺寸都会在进入 iframe 投影(projection)或视口裁剪(clipping)之前就发生偏差——也就是说,坐标系换算的第一公里就走错了方向。
4.2 fixture 设计
snapshot-coordinates.fixture.mjs 构造了一个刻意复杂的页面拓扑:
- 一个滚动过的根页面(
body宽 2000px、高 2400px,加载后scrollTo(80, 240)); - 一个同进程(same-process)iframe(
#same),自身也发生滚动(scrollTo(40, 100)); - 一个OOPIF(
#cross),通过把 URL hostname 在127.0.0.1与localhost之间互换来强制跨进程; - 每个 iframe 内再嵌套一层
#nestediframe; - 各 owner 均带有
border: 6px、padding: 8px与transform: scale(1.25)(nested 为scale(0.8)),同时引入边框、内边距与 CSS 变换三个干扰因素。
fixture 脚本在双requestAnimationFrame后置位data.geometryReady并上报geometry.ready事件,确保断言只在其布局完全稳定后执行。注意其 URL 默认会为 OOPIF 设置scrollbars=none,这是为了把“布局单位换算”问题与既有的“OOPIF 滚动条宽度投影”问题隔离开——后者由专门的 oopif-scrollbars 用例覆盖。若想在同一 fixture 中恢复经典滚动条形态,可附加&classic-scrollbars查询参数。
4.3 数值回归测试
几何数值断言运行在真实 Chrome 之上,命令如下:
BSK_GEOMETRY_CHROME=/path/to/chrome pnpm --filter @browser-skill/extension exec vitest run \ src/tools/__tests__/snapshot-coordinates.browser.test.ts该测试(源码见 snapshot-coordinates.browser.test.ts)通过describe.skipIf(!process.env.BSK_GEOMETRY_CHROME)实现按需启用:普通单元测试运行时不设置BSK_GEOMETRY_CHROME,用例自动跳过;只有显式传入本地 Chrome 可执行文件路径时才真正执行,且不需要下载浏览器或引入新的包依赖。
测试用it.each覆盖5 种设备缩放/浏览器缩放组合:{deviceScale: 1, zoom: 1}、{deviceScale: 0.8, zoom: 1}、{deviceScale: 1, zoom: 1.25}、{deviceScale: 2, zoom: 1}、{deviceScale: 2, zoom: 0.8},并将snapshot-coordinates与oopif-scrollbars两个 fixture 组合成参数化矩阵。其验证逻辑关键点在于:
- 独立 oracle:测试内嵌一段独立 DOM 表达式,用
getBoundingClientRect()计算各[data-geometry-probe]探针的 border box,以及各 iframe owner 的坐标与 scale(scale = rect.width / frame.offsetWidth),再用独立的clip函数做视口裁剪——期望值完全由 DOM 原生几何推导,不依赖生产代码的换算逻辑,避免“用待测实现验证待测实现”; - 精度阈值:生产 capture 得到的本地矩形与顶层矩形,与独立 DOM border box 相比,允许的布局舍入误差最多 2 个 CSS 像素;
- 读取次数:同时校验每个目标只读取一次布局指标(layout metrics),防止性能退化;
- 拓扑自检:测试会显式验证请求的浏览器 zoom 与 OOPIF 拓扑是否真正生效,若环境不支持则会直接失败,而不是静默地测试了一个错误的配置。
测试自行启动并清理独立的 headless Chrome profile,保证每次运行环境一致。若想手动查看其使用方式,fixture 依赖的浏览器启动封装位于 snapshot-coordinates/chrome.mjs。
4.4 smoke 的边界
除了数值回归,该用例还提供 CLI 冒烟入口:
BSK_AUTO_UPDATE=off pnpm eval:browser smoke --case snapshot-coordinates --bsk ./target/debug/bsk但 README 特别强调:smoke 并不能证明坐标精度——CLI 的观察结果不会暴露原始矩形数据。smoke 只负责验证 fixture 就绪与可观察语义(标记GEOMETRY-195被正确报告、会话正常关闭),数值断言必须由上述几何测试承担。这种“分层验证”的定位划分,正是该回归体系的可取之处。
五、实战案例二:oopif-scrollbars(OOPIF 占用滚动条回归)
5.1 症状与根因
oopif-scrollbars/README.md 描述的缺陷更加微妙:iframe 的 content quad(内容四边形)包含其子视口滚动条所占的空间,而 CDP 的Page.getLayoutMetrics().cssLayoutViewport却排除这部分空间。若把后者直接映射到整个 quad 上,位置与尺寸就会被拉伸。
正确的处理原则是:缩放比例必须由完整的 target-local 视口(含滚动条空间)决定;而可见视口仍须在每个 OOPIF 边界处裁剪内容,并约束 target-local 动作点。也就是说,scale 用“总空间”算,clip 用“可见区域”切,二者缺一不可。
5.2 fixture 与查询参数
oopif-scrollbars.fixture.mjs 为此构造了跨两个嵌套 OOPIF 的回环主机名交替拓扑:根页面通过127.0.0.1/localhost互换 hostname 加载外层跨源 frame,外层 frame 再以同样的方式加载内层 frame,形成两层 OOPIF。页面结构包含:
- 滚动(不同页面不同
scrollTo偏移); - 边框(
border: 6px)、内边距(padding: 8px); - 缩放过的 iframe owner(外层
scale(1.2)、内层scale(0.8)); - 一个部分被裁剪的按钮(
#edge,position: fixed; right: -20px; bottom: -15px); - 一个完全被裁剪的按钮(
#outside,位于视口calc(100% + 2px)之外); - 自定义滚动条占用不同宽高(
::-webkit-scrollbar宽 17px、高 11px)。
fixture 通过scrollbars查询参数控制滚动条形态:
| 参数值 | 含义 |
|---|---|
both(默认) | 垂直 + 水平滚动条均占位 |
vertical | 仅垂直滚动条占位(水平隐藏) |
horizontal | 仅水平滚动条占位(垂直隐藏) |
none | 双轴均不显示滚动条 |
脚本在加载后上报geometry.scrollbars(通过innerWidth > document.documentElement.clientWidth等比较判定实际滚动条是否占位)与geometry.ready,并监听[data-geometry-probe]按钮的点击事件,把点击结果回传给browserEval——这正是真实点击验证的数据通道。
5.3 浏览器测试与 smoke
oopif-scrollbars 与 snapshot-coordinates共用同一个浏览器 runner(snapshot-coordinates.browser.test.ts)。在 5 种缩放/缩放组合之外,还额外以deviceScale: 1, zoom: 1遍历vertical、horizontal、none三种单轴/无滚动条形态。该测试覆盖的能力包括:
- 验证真实的 OOPIF 目标与滚动条占用尺寸;
- 用独立 DOM 矩形与裁剪逻辑对比 snapshot 与 live 几何;
- 派发真实 root-target 点击,并验证事件确实落到了预期的 frame/按钮(如
#edge部分裁剪仍可点击、#outside应被拒绝); - 拒绝完全被裁剪的控件,并校验同一目标的 snapshot 测量复用;
- 覆盖滚动条在垂直/水平/双向占位时的几何一致性。
runner 会创建并清理隔离的浏览器 profile,且与数值测试同样采用 opt-in 机制:未设置BSK_GEOMETRY_CHROME时在普通单元运行中自动跳过。
CLI 冒烟入口为:
BSK_AUTO_UPDATE=off pnpm eval:browser smoke --case oopif-scrollbars --bsk ./target/debug/bsk冒烟断言仅验证两个嵌套 frame 均有占用滚动条、OOPIF-SCROLLBARS标记被观察到、会话被关闭;数值几何与真实点击断言由上面的浏览器测试承担,smoke 单独运行不能替代数值回归。
六、运行几何回归测试的环境与前提
综合两个用例,运行这套几何回归需要满足以下条件,缺一不可:
- Node.js 22+:数值回归测试的运行环境要求;
- 本地 Chrome 可执行文件:通过
BSK_GEOMETRY_CHROME=/path/to/chrome指定。路径必须真实可用,因为测试会显式校验 zoom 与 OOPIF 拓扑,不支持的环境会直接失败而非悄悄跳过; - 包管理器 pnpm:命令以
pnpm --filter @browser-skill/extension exec vitest run ...形式在扩展包内执行测试; - CLI smoke 需要已构建的二进制:
--bsk ./target/debug/bsk指向 Rust 侧构建产物(debug 构建即可),且以BSK_AUTO_UPDATE=off关闭自动更新,保证冒烟过程可复现。
几何模块的底层实现可参考 apps/extension/src/tools/frame-geometry.ts(节点几何解析)与 apps/extension/src/browser-driver/frame-graph.ts(CDP frame 图管理)。数值测试正是以这些模块为被测对象,用独立 oracle 校验其输出。
七、回归用例体系的最佳实践小结
从regression/README.md与两个真实用例可以提炼出这套体系沉淀下来的几条关键准则:
- 一个坏例一个目录:manifest、fixture、README 三件套齐备,缺陷可独立复现与评审;
- 合成数据,拒绝真实资产:凭证、Cookie、HAR、生产 HTML、私有截图一律禁止入库,fixture 全部自建,跨源拓扑用回环主机名交替实现;
- 声明式清单 + 三层断言:
site(页面事件)、response(Agent 回复标记)、adapter(会话关闭)分别验证不同层次,smoke提供最小工作流冒烟; - 数值断言与 smoke 分层:smoke 只证明“能跑通、能观察到”,坐标精度必须由真实 Chrome 几何测试证明——这是避免“看起来绿了其实坏了”的关键;
- 问题隔离与组合覆盖:snapshot-coordinates 默认关掉 OOPIF 滚动条(
scrollbars=none)以隔离布局单位换算问题,滚动条投影问题交由&classic-scrollbars与独立的 oopif-scrollbars 用例覆盖;同时用参数化矩阵把 5 种缩放组合 × 2 个 fixture × 单轴/无滚动条形态一次性铺开; - opt-in 运行:真实浏览器测试仅在显式设置
BSK_GEOMETRY_CHROME时启用,普通 CI/单元运行不被拖慢,又不至于让回归静默漏测。
对于浏览器自动化类项目,几何与坐标换算是最容易在“真实浏览器”与“理论模型”之间产生偏差的领域。BrowserSkill 通过这套回归用例体系,把用户反馈的坏例固化为可复现、可断言的工程资产——既有声明式清单支撑自动化评估,又有真实 Chrome 数值测试兜底坐标精度,为后续 Agent 在复杂多 iframe 页面中的精准交互提供了可验证的回归防线。
【免费下载链接】BrowserSkillLet AI agents use your real, logged-in browser without interrupting your work. CLI + extension for browser automation across any shell-capable AI agent.项目地址: https://gitcode.com/GitHub_Trending/br/BrowserSkill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考