首页
/ OpenCode 前端时间线布局连续性压测:Timeline Stability 测试套件全解析

OpenCode 前端时间线布局连续性压测:Timeline Stability 测试套件全解析

2026-09-06 11:55:19作者:范垣楠Rhoda

本文以 OpenCode Web 应用(packages/app)的 Timeline Layout Continuity 压测套件为对象,讲清它的运行方式、rAF 采样探针、声明式不变量契约与受控后端夹具的设计,并说明这套 pass/fail 判据能证明什么、不能证明什么。读完后你应能独立运行 bun run test:stability,理解每个视觉契约的底层实现,并能按同样思路为流式会话时间线编写布局稳定性测试。

一、这个套件解决什么问题

OpenCode 的会话时间线是一个持续接收事件流(消息更新、part 增量、工具状态变化)并做虚拟化的动态列表。内容增长、行替换、折叠展开、滚动与重排相互交织时,最容易被用户感知到的缺陷是:可见锚点跳动、相邻行重叠、内容闪烁白屏、焦点/展开状态丢失。

套件说明文档将其定位为"连续性验证":连续性探针在浏览器渲染机会之间采样由 DOM 派生的布局与可见性状态,测试用例则声明显式的契约。文档明确列出了它要验证的七类契约:

  • 当用户不在底部时,保持一个可见的语义锚点稳定;
  • 当活跃内容增长或新内容出现时,保持末端锚定(贴底);
  • 保持相邻可见行有序且无显著重叠;
  • 在更新与虚拟化过程中保持用户选择的展开(disclosure)状态;
  • 当一个可见表面替换另一个可见表面时,避免采样到空白间隙;
  • 在本地状态或焦点依赖行/控件身份的地方,保持逻辑行与控件身份稳定;
  • 在重测量过程中保持键盘、滚轮与嵌套滚动的归属一致。

套件驱动的是真实浏览器中的 reducer、projection、组件、虚拟化器、布局、焦点与交互代码;后端与事件生产者则是受控夹具。换言之,被测对象是完整的前端生产构建,被替换掉的只有网络与模型侧。

二、如何运行:test:stability 的实际构成

packages/app 下执行:

bun run test:stability

对照 package.json 可以还原这条脚本的完整构成:

bun test ./e2e/performance/unit/visual-stability.test.ts \
  && playwright test --config e2e/performance/timeline-stability/playwright.config.ts

即分两段执行:先跑纯分析器与采样器的单测(对应文档"Test Layers"中的 Oracle contract 层——"pure analyzer and browser sampler calibration tests"),再跑 Playwright 浏览器压测。

timeline-stability 专属的 Playwright 配置继承自 e2e 基础配置,并覆盖了以下关键项:

export default {
  ...config,                       // 继承 ../playwright.config 的 webServer/use 等配置
  testDir: ".",
  testMatch: "**/*.spec.ts",
  outputDir: "../../test-results/timeline-stability",
  reporter: [["html", { outputFolder: "../../playwright-report/timeline-stability", open: "never" ]], ["line"]],
  retries: 0,                      // 不重试:失败必须如实暴露
  workers: 1,                      // 单 Chromium worker 串行执行
  use: {
    ...config.use,
    trace: "retain-on-failure",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  },
}

三个参数与文档描述一一对应:

  • workers: 1 即"在单个 Chromium worker 中运行生产构建",保证时间线事件流在单浏览器内按序投递,避免并发干扰时序采样;
  • retries: 0 意味着任何偶发失败都会留痕而不是被重试掩盖;
  • retain-on-failure 三件套(trace / 截图 / 视频)正是诊断证据链的来源。

文档还强调:选定场景在应用就绪后施加确定性 4 倍 CPU 压力,"这是压力剖面(stress profile),而非对某台具体设备的仿真"。这个 4 倍压力在夹具里通过 CDP 精确实现(见第五节)。

三、连续性探针:rAF 驱动的 DOM 采样原理

探针实现位于 probe.ts,它把一段脚本注入页面,挂到 window.__visualStabilityProbe 上,并形成一个自持的采样循环。

3.1 采样循环与时间基线

  • 探针启动时记录 performance.now() 作为 startedAt,并把 performance.timeOrigin + startedAt 解析后作为 epoch 基线返回给宿主,使采样时间轴与 Playwright trace 对齐;
  • 核心是 sample() 函数:每次采样后通过 setTimeout(…, 0) + requestAnimationFrame(sample) 排队下一帧(见 probe.ts),从而绑定浏览器的渲染机会;
  • 启动 Promise 会等到至少一个样本落地才 resolve,确保后续标记(marker)不会发生在空窗口上。

每个样本记录两部分:

  1. 视口状态:探针定位包含 [data-timeline-row].scroll-view__viewport 元素,采样其 top/bottom/scrollTop/scrollHeight/clientHeight,并推导 distanceFromBottom(距底距离)——这是末端锚定判定的直接输入;
  2. 区域(region)状态:测试为每个关注的行/控件声明 CSS 选择器(可附加 closest 提升选择器),探针逐个采样:
    • present / visible / inViewport / cssHidden
    • 未裁剪布局边界 layoutTop/layoutBottom祖先裁剪后可见交集 top/bottom/width/height:探针沿父链向上,对所有 overflowY/overflowXhidden|clip|scroll|auto 的祖先以及视口本身做矩形交集(见 probe.ts)。这正对应文档"analyzer 同时记录未裁剪布局边界与祖先裁剪可见交集"一句;
    • 不透明度:既支持 opacitySelectors 聚合内容节点的不透明度(沿父链逐层相乘),也沿祖先链累计 opacity 并检测 display: none / visibility: hidden
    • node 身份号:用一个 WeakMap<Node, number> 给首次见到的 DOM 节点分配递增 ID,用于后续判定"是否被重新挂载";
    • label(aria-label)与截断后的 text 文本快照。

3.2 标记(marker)与事件窗口

测试在投递每个后端事件后调用 markVisualStability(page, label),把带时间戳的标记写入探针。analyzer.tsanalyzeVisualTraceByMarker 会把这些标记切分为事件窗口:每个窗口取"标记前最后一个样本 + 窗口内样本"逐段判定,问题消息会带上前缀 marker: issue。此外 motion 类问题默认还会做聚合分析aggregateMotion),即跨整个 trace 检查位置反转次数。

四、声明式契约:不变量类型与判定规则

测试通过 visualPlan(regions, invariants, options) 声明契约,不变量类型定义在 invariant.ts。文档七条契约与不变量类型的对应关系,以及分析器 analyzer.ts 中的具体判定逻辑如下:

不变量类型 语义 判定细节
required 指定区域必须至少渲染过一次 全程无 visible 样本则报 "never rendered"
continuous-any 一组区域中任一必须连续可见 在"首次可见→末次可见"区间内出现全空样本则报 "blanked between visible frames"
unique 指定区域不得同时出现多个实例 某样本 count > 1 即报重复
stable 指定区域保持同一 DOM 身份 可见样本中出现多个不同 node ID 则报 "remounted N times"(守护焦点/本地状态不丢)
fixed 指定区域在视口中位置固定(默认容差 1px) 相对首个可见样本的 top 位移超限即报
opacity 可见期间不透明度不得低于地板(legacy 默认 0.65) 任何可见样本低于 floor 即报
continuity present 与 visible 两个层面不得中途断裂 "present 帧之间消失"或"可见帧之间白屏"(限视口内样本)分别报出
motion 允许运动但限制方向反转次数 top/bottom/width/height 逐个计算相邻样本差的方向序列,反转次数超过 maxReversals / maxPositionReversals 即报
label-stability aria-label 不得回退 标签序列中出现"变回旧值"即报 "label reverted"
flow 按声明顺序的区域不得互相重叠或倒序 同时可见且在视口内的相邻两区域,before.bottom - after.top 超过 overlapTolerance(默认 0.5px)或顺序颠倒即报
preserve-bottom-anchor 初始贴底则全程保持贴底 首样本 distanceFromBottom <= 4 后,任何样本超过 4px 即报
acquire-bottom-anchor 结束时必须到达贴底 末样本 distanceFromBottom > 4 即报

其中 preserve-bottom-anchoracquire-bottom-anchor 的 4px 阈值可以直接在 analyzer.ts 中读到。

文档中"滚动条与原始 scrollTop 变化本身不会导致连续性检查失败;用户可见的语义锚点移动才会"这句,在源码层面得到印证:分析器从未直接比较 scrollTop 序列,所有判定都基于区域的可见几何、身份、标签与不透明度——这正是"语义锚点"与"滚动位移"的区分所在。

五、测试层级与确定性夹具

5.1 测试层级

README 将套件划分为六个层级,目录下的 spec 文件与之对应(timeline-stability 目录):

  • Projection(投影):被接收的行、分组、标签与最终可见状态——如 file-matrix.spec.tstransition-matrix.spec.ts
  • Local state(本地状态):展开状态、身份、重复投递、虚拟化恢复——如 lifecycle.spec.ts 中"用户折叠某行后,后续兄弟重渲染不得改变其 aria-expanded"的用例;
  • Interaction(交互):滚轮、键盘、嵌套滚动、可操作性与焦点——interaction.spec.tsscroll-interaction.spec.ts
  • Layout continuity(布局连续性):锚定、相邻关系、响应式重排、可见表面交接——lifecycle.spec.ts 中"Thinking 占位被流式 reasoning + text 替换且无白屏回合"的用例就在此列(用 continuous-any 覆盖 thinking/reasoning/text 三个区域);
  • Reducer hardening(reducer 加固):形状合法但故意乱序、重复、删除、替换的事件——adverse.spec.ts
  • Oracle contract(判据契约):纯分析器与浏览器采样器的校准测试——fixture.test.ts(bun 单测)与 oracle-browser.spec.ts

文档同时给出了一条夹具编写纪律:生产生命周期夹具应建模当前生产者实际会发出的状态;不可能或乱序的序列只属于 reducer-hardening 测试,不得被描述为"正常 provider 行为"。这避免了把加固测试的假想故障误标成线上行为。

5.2 夹具如何做到确定性

fixture.ts 是整个套件的确定性核心,setupTimeline(page, input) 的要点:

  • 受控后端mockOpenCodeServer + installSseTransport 在页面内拦截网络,构造确定性项目(proj_timeline_stability)、会话(ses_timeline_stability)与消息序列,事件按测试脚本逐条投递并可精确控制延迟(sendAll);
  • Schema 级校验:所有夹具消息、part、事件在进入页面之前先经 effect Schema 解码(errors: "all", onExcessProperty: "error"),validateTimelineMessages 还额外断言消息/ part ID 唯一、part 归属匹配、user/assistant 角色的 part 类型约束(见 fixture.ts)——保证投递给 reducer 的事件"形状永远合法",从而把"乱序/重复"等故障与"非法数据"清晰分开;
  • 确定性 4x CPU 压力cpuRate 参数经 CDP Emulation.setCPUThrottlingRate 施加(见 fixture.ts),spec 中普遍传 cpuRate: 4,与文档"selected scenarios use deterministic 4x CPU stress after application readiness"一致;
  • 环境矩阵维度viewport(默认 1400x900)、deviceScaleFactor(经 Emulation.setDeviceMetricsOverride)、localereducedMotionemulateMedia)、protocol: "v1" | "v2"seedHistory(预置 18 条历史消息让虚拟化时间线更真实)——分别支撑 environment-matrixcontext-matrixshell-matrix 等矩阵型 spec;
  • 稳定的等待手段waitForVisualSettle 要求指定选择器的矩形连续 stableFrames 帧签名不变相邻元素保持有序才放行;settle(frames) 等待若干 rAF。注意这与 e2e/AGENTS.md 的规范一致:不用墙钟延迟做同步,而是等待具体的 UI 状态。

一个完整的用例形态(lifecycle.spec.ts)是:以 4x CPU 压力与历史种子启动 → 发送 busy 状态 → 等 part 就位 → 滚到底 → startVisualProbe 声明区域 → sendAll 交错投递空/短/长三个并行 shell 的完成事件与后续文本 → stopVisualProbe 收集 trace → reportVisualStability 按契约(required/unique/stable/opacity/continuity/motion/label-stability/preserve-bottom-anchor/flow,且 perMarker: true)判定。

六、诚实的边界:这套判据不能证明什么

README 用一整节划定证据边界,值得原样吸收为设计准则:

  1. 不检查每个合成器呈现的像素requestAnimationFrame 之后取到的样本是 DOM/布局观测,不证明每个被采样状态都真的被显示,也不证明每个被显示的帧都被采样了;
  2. 因此它不覆盖:仅合成器或光栅化层面的瑕疵;颜色、对比度、canvas、WebGL、遮罩、不规则裁剪与任意遮挡;物理屏幕刷新率、操作系统原生缩放与任何具名低端设备;TCP 分段、代理缓冲与真实服务器/provider 的完整链路;
  3. Playwright 视频、trace、截图与观测 JSON 只是诊断证据,不是像素基线,不参与常规 pass/fail 判定。

这个边界声明的价值在于防止误读:绿色通过说明"在受控事件流与 4x CPU 压力下,DOM 布局契约全部成立",而不是"在任何设备上都视觉无瑕"。

七、诊断证据链

失败时,由于配置了 retain-on-failurePlaywright 配置 会保留:

  • video.webm——场景全过程录像;
  • trace.zip——Playwright 交互/网络/DOM 时间线;
  • 失败截图;
  • 采样 DOM/布局 trace JSON——即探针收集的 samples + markers 观测数据;
  • 事件标记(describeEvent 生成的 type:partID:tool:status 摘要)与汇总后的违例信息。

此外还有一个环境变量控制的可选开关:设置 OPENCODE_STABILITY_CAPTURE=1 才会启用 before/violation/after 截图采集。该开关在 capture.ts 中生效(if (process.env.OPENCODE_STABILITY_CAPTURE !== "1") return)。文档解释其为默认关闭的原因:合成器读回(readback)会扰动计时,因此只在需要取证时开启。

八、小结:可复用的方法论

Timeline Stability 套件给出的方法可以概括为四步,且每一步都有源码可对:

  1. 采样与渲染对齐:探针用 rAF 链绑定渲染机会,同时记录未裁剪几何与祖先裁剪交集,使"可见"有精确定义;
  2. 契约声明化:把"不闪烁、不重叠、锚点稳定、身份不变"等体验要求转写成可判定不变量,判据实现为纯函数(analyzeVisualObservations),可用 bun 单测独立校准;
  3. 故障注入与正常路径分层:合法事件走生产生命周期夹具,乱序/重复/删除事件只出现在 reducer-hardening 层,避免证据语义混淆;
  4. 判据边界显式化:明确声明不覆盖合成器/像素级问题与真实网络链路,诊断产物不参与 pass/fail,保证结论可解释。

对维护 OpenCode 会话时间线(或任何流式、虚拟化列表)的开发者而言,这份套件的价值不仅在于它能拦截回归,更在于它示范了如何用 DOM 级观测把"布局连续性"这类体验属性变成可声明、可判定、可复现的工程契约。

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