要让浏览器 UI 测试真正做到「100% 可复现」,业界目前给出的答案不是「再写一条更稳的用例」,而是直接把「非确定性」从根上拔掉。BrowserBook 的思路可以一句话概括:在浏览器内核与自动化脚本之间插入一层「确定性运行时」,录制阶段把随机源、时钟、网络、DOM 种子全部快照化;回放阶段用同一份快照驱动,任何比特级抖动直接报错,实现零方差执行。下面把整套机制拆成 4 个可落地参数,方便你在 Playwright/Cypress 之外快速验证。
1. 确定性时钟:把「时间」变成序列号
| 参数 | 录制值 | 回放值 | 备注 |
|---|---|---|---|
--deterministic-time-origin |
0 | 0 | 所有 performance.now () 起点对齐 |
--advance-time-step |
16ms | 16ms | 每帧固定步进,与屏幕刷新解耦 |
--request-animation-rate |
1× | 1× | 禁止浏览器抽帧,确保动画帧对齐 |
实现方式:在启动浏览器时注入 Date.now = () => timeOrigin + frameCount * 16,并把 requestAnimationFrame 重写成「计数器 + 1」。这样即使 CI 机器性能不同,同一用例的动画、轮询、超时都会落在同一毫秒刻度。
2. 网络快照:把「异步」变成「常量数组」
| 参数 | 录制值 | 回放值 | 备注 |
|---|---|---|---|
--record-network |
true | false | 录制阶段拦截所有 fetch/XHR |
--replay-network |
false | true | 回放阶段用录制 JSON 直接返回 |
--network-delay-jitter |
0 ms | 0 ms | 强制延迟为 0,消灭 RTT 方差 |
实现方式:基于 Playwright 的 route.fulfill(),把第一次运行的全部请求按顺序写入 network.dump.json,内容包括 URL、status、headers、body、时间戳。回放阶段不再发真实请求,而是按时间戳顺序直接 fulfill,彻底消灭 404、慢查询、A/B 分支带来的抖动。
3. 随机种子:把「Math.random」变成「伪随机表」
| 参数 | 录制值 | 回放值 | 备注 |
|---|---|---|---|
--seed-random |
42 | 42 | 全局 Math.random 替换为 seedrandom |
--seed-crypto |
42 | 42 | 把 crypto.getRandomValues 也劫持 |
--uuid-version |
v4→v1 | v1 | 用时间戳 + 计数器生成 UUID,避免随机 |
实现方式:在页面脚本最前端注入 window.Math.random = new seedrandom('42'),并代理 crypto.getRandomValues 返回「录制阶段预生成」的随机数表。只要种子相同,React key、弹窗位置、颜色随机化都会完全一致。
4. DOM 快照与重放:把「可见状态」变成「可执行脚本」
| 参数 | 录制值 | 回放值 | 备注 |
|---|---|---|---|
--snapshot-format |
json+base64 | json+base64 | 包含 outerHTML、样式、input value |
--snapshot-interval |
every action | every action | 每次 click/type 后自动快照 |
--replay-strategy |
strict | strict | DOM 哈希不一致立即抛错 |
实现方式:
- 每步操作后执行
document.doctype + document.documentElement.outerHTML,并把<input>、<textarea>、<select>的当前值写回 DOM,生成「自包含快照」。 - 快照文件按序号命名:
001-login.json、002-search.json…… - 回放阶段用
page.setContent()直接覆写整个文档,再执行下一步操作;若哈希与录制不一致,立即截图 diff 并抛错,实现「时间旅行」级别的现场还原。
5. 断线续传:让 CI 机器也能「接着跑」
即使所有状态都被快照化,CI 容器被强制重启仍会导致用例从头跑。BrowserBook 在 SSE 通道里加了两条指令:
| 指令 | 客户端行为 | 服务端行为 |
|---|---|---|
snapshot/restore?case=login&step=7 |
断线重连时先拉取第 7 步快照 | 返回对应 JSON 并回滚浏览器状态 |
snapshot/diff?case=login&step=7 |
校验本地快照是否一致 | 返回 204 或 412,412 时强制重跑 |
实现方式:
- 用例脚本在每一步操作后把「用例名 + 步骤号」通过 SSE 发送到服务端;
- 服务端把快照存到 Redis,TTL 24h;
- 客户端断线重连时带
Last-Event-ID,服务端按 ID 恢复上下文,实现「秒级续跑」。
6. 超时参数清单:不给「随机延迟」任何机会
| 阶段 | 推荐值 | 说明 |
|---|---|---|
| 页面加载 | 5 s | 网络已快照,超时即视为快照损坏 |
| 元素可见 | 2 s | 回放阶段禁用智能等待,超时直接抛错 |
| 动画结束 | 1 s | RAF 已固定帧率,1s 足够跑完 60 帧 |
| 网络空闲 | 0 ms | 回放阶段无真实网络,可设为 0 |
| 重试次数 | 0 | 确定性运行不需要重试,失败即缺陷 |
把以上 6 组参数写进 browserbook.config.js,你就能在本地开发机、CI 容器、甚至多模型并发跑的场景下,得到「像素级」一致的测试结果。下一步要验证的,不再是「这条用例稳不稳」,而是「快照有没有覆盖新功能」—— 这才是 UI 自动化真正的确定性未来。
资料来源
[1] Playwright 官方文档:Trace Viewer 与 UI Mode 时间旅行调试机制
[2] Mozilla WebReplay 项目:记录与回放 JavaScript 行为、DOM 结构及图形更新