☰
无DOM测试与happy-dom:bloub如何验证导出缺陷的测试体系
2026/10/11 10:57:13 网站建设 项目流程
  • 前端
  • 图形学

【免费下载链接】bloub

SVG recreation of the x.ai bot avatar. One shape morphing through 14 states, measured off the reference video frame by frame.

项目地址:https://gitcode.com/gh_mirrors/bl/bloub
点击查看免费下载

bloub 是一个用 SVG 复现 x.ai 机器人头像动画的开源项目:一个黑色填充形状在 14 种状态间平滑变形,眼睛由两个独立的白色形状驱动。它能把头像导出为 SVG、PNG、GIF 和 MP4。正是这套导出功能,催生了一对一整套无 DOM 测试 + happy-dom的验证体系:13 个测试文件里只有一个文件声明@vitest-environment happy-dom,其余全部跑在纯 node 环境里。而这套体系真正抓住的,是只能靠逐帧翻看 MP4 才能发现的导出缺陷。

bloub 的 14 种状态并排冻结展示(docs/states.png),由引擎按时间采样渲染,而非浏览器截图:

1. 三层测试金字塔:node 打底,happy-dom 收口

先看清测试体系的分层,理解"为什么只有 1 个文件需要 DOM":

层代表文件运行环境验证什么
纯逻辑层engine.test.ts、cycles.test.ts、skins.test.tsnode引擎纯函数、时间线规则、形状/表情常量
纯数据层export.test.ts、anime.test.ts、i18n.test.tsnode导出画框、文件名、GIF 调色板与 LZW 编码
DOM 层capture.test.tshappy-dom离屏播放器渲染的 SVG 必须与引擎完全一致

分层的关键在 vitest.config.ts:

  • environment: 'node'是全局默认,DOM 是按文件申请的——在需要的文件头部写一行// @vitest-environment happy-dom即可;
  • 配置里特意只挂了一个 Vue 插件,它只为capture.test.ts这一个文件服务(要挂载BloubBot.vue组件才能被 Vite 转换);
  • 注释直接点明动机:全局 DOM 环境会拖慢整套测试,"把测试套件保持在几秒钟"(原文:C'est ce qui garde la suite a quelques secondes)。

也就是说:happy-dom 不是这个项目的底座,而是精确投放的一小块 DOM——只为验证"导出的渲染必须来自组件本体,而不是旁边重建的第二份画"。

2. 一个文件声明 happy-dom,其余 12 个文件跑在 node

唯一的 DOM 请求就在 capture.test.ts 的第一行(capture.test.ts#L1):

// @vitest-environment happy-dom

文件头部的注释(capture.test.ts#L11-L21)说得很直白:

这是全仓库唯一需要 DOM 的测试,所以环境声明放在文件头部:其余测试跑在node环境,这让capture.ts——连同整条导出链路——本来完全无法测试。

它能抓住的问题,除了逐帧检查 MP4 之外没有任何其他途径可见,而这正是这些缺陷当初被放行的原因:三个缺陷长期悄悄劣化着每一份导出的视频。

为什么capture.ts无法在 node 里测试?因为它是导出链路的 DOM 层(capture.ts#L1-L11):需要 canvas 光栅化、需要document.createElement、需要剪贴板。与之对应,export.ts里的画框计算、目录、文件名都是纯函数,留在 node 里由 export.test.ts 覆盖。一条导出链路被精确地切成了"可纯测的部分"和"必须挂 DOM 的部分"。

离屏播放器本身的实现也值得看一眼(capture.ts#L324-L377):在document.body里挂一个position:fixed; left:-99999px的宿主 div,用createApp把BloubBot以frozenAt: 0和首个状态的state挂载,然后逐帧调用组件的rendAt(t)推进时间。这一步需要真实的组件挂载与渲染,正是 happy-dom 的用武之地——但它比 jsdom 更轻,且完全够用,因为断言的内容全是不依赖布局与像素的字符串:身体路径的d属性、眼睛的transform矩阵、viewBox的值。

3. 纯函数引擎:无 DOM 测试的地基

这套体系成立的前提写在 docs/architecture.md 里:

engine.sample(t)是时间的纯函数。这正是frozenAt属性、冻结状态板和无 DOM 测试套件得以存在的原因。

因此src/bot/有一条铁律:不得出现依赖真实时间的内部状态,不得用Date.now(),不得引入任何 Vue 依赖。暂停、续播、跳到任意日期、跑测试,都会得到同一张图像。

测试正是利用这一点建立了"双轨对照":capture.test.ts里有一个辅助函数moteurAuMemeInstant(capture.test.ts#L36-L47),独立于组件构造一个BotEngine,按与组件完全相同的方式给每个区块的时间打上绝对偏移,然后逐时刻与离屏播放器输出的 SVG 逐字节比对:

expect(corpsDe(svg), `t=${t.toFixed(2)}`).toBe(attendu.bodyPath) expect(yeuxDe(svg), `t=${t.toFixed(2)}`).toEqual(attendu.eyes.map((e) => e.matrix))

身体路径、眼睛矩阵都是纯字符串,不需要任何渲染引擎,也不关心 DOM 长什么样——这正是"DOM 只负责挂载,断言只碰文本"的无 DOM 测试思路。

4. 四个被 happy-dom 抓到的导出缺陷

CHANGELOG(CHANGELOG.md,0.1.1 版本)记录了三个"不逐帧看 MP4 就发现不了"的图像缺陷,加上一个回归防线,恰好对应capture.test.ts的四个用例:

缺陷一:默认 GIF 开场是一颗没有眼睛的球。GIF 导出需要两遍扫描:第一遍统计颜色建调色板,第二遍编码。这要求重放序列必须逐帧一致,否则第一遍统计出的调色板对第二遍无效。但离屏播放器会记住"最后一次设置的区块",而引擎为淡入淡出保留"上一状态"——完整播放一遍再回到 t=0,第一个状态会与最后一个状态混合。默认循环的结尾是彗星状态,其eyeAlpha恰好为 0,于是 GIF 第一帧变成无眼球体。锁定用例:capture.test.ts#L86-L104,重放完整序列后回到 t=0,身体路径与眼睛矩阵必须与首帧完全相等。

缺陷二:每个区块接缝处都是坏帧。区块交界处,组件确实把状态设置到了绝对偏移再采样;但state的 watcher 队列会被导出流程的nextTick排空,绕过后一次本应惰性的redrawFrozen()。结果:setState之后立刻sample(0),混合比例为零,渲染出的是上一个状态的姿势——默认蒙太奇里有 13 处接缝,每处一帧坏图(0.1.1 修复前眼睛轨迹最多跳 53.4 个单位)。锁定用例:capture.test.ts#L116-L134,在每个区块的起点与中点时刻逐一对照引擎直出的姿势。

缺陷三:导出画面不是从蒙太奇的第一个状态开始。离屏播放器挂载时未传stateprop,模型落到默认值idle,引擎便构建在"静止"之上:一个以轨道状态开头的蒙太奇,导出第 0 帧却是静止球体朝三角形 morph 了 0.6 秒的过渡画面;而屏幕上的播放从不这样,它用当前区块启动状态。修复是在ouvreCycle里显式传入首状态(capture.ts#L341-L350),测试对orbit、egg、hexagon三种开头各断言一次 t=0 帧(capture.test.ts#L144-L158)。

缺陷四(回归防线):导出画框与组件画框脱节。DEMI_ECRAN曾以 158 硬编码,与组件里手写的VB = 158之间没有任何引用关系。想象回归场景:有人为环形更大的新状态加宽 viewBox,导出却仍用旧画框,每条视频多出空白边带,而所有测试都是绿的。用例直接断言导出 SVG 的viewBox等于组件实际发射的值(capture.test.ts#L62-L72);而DEMI_ECRAN与组件画框如今同源于 export.ts,画框包含所有形状等约束则留在 node 环境由 export.test.ts 把守。

5. 这套体系给前端测试的三点启示

  1. DOM 是资源,按文件申请,而不是全局默认。一行// @vitest-environment happy-dom把 DOM 成本压到一个文件,整套测试保持在秒级。
  2. 把"纯函数内核"当成测试资产。sample(t)不碰时钟、不碰框架,于是测试可以独立构造参照系,与 UI 路径逐字节对照。
  3. 肉眼不可见的缺陷,靠"复现 + 断言"固化。三个导出缺陷都是"看视频才能发现"的,而修复之后,锁定它们的测试把逐帧一致性变成了每次pnpm test都能重跑的断言。
想读什么去哪里
无 DOM 测试与冻结状态板的由来docs/architecture.md
导出链路(画框、GIF 两遍扫描、MP4)docs/export.md
唯一的 happy-dom 测试src/ui/capture.test.ts
离屏播放器实现src/ui/capture.ts
环境配置与 Vue 插件的用途vitest.config.ts
缺陷修复记录CHANGELOG.md

无 DOM 测试不是"没有浏览器"的妥协,而是一次主动的架构选择:纯函数引擎负责正确性,happy-dom 只负责让组件本体被挂载、被读走。正是这 12:1 的环境配比,让 bloub 在几秒内跑完全部测试,同时把导出链路里最隐蔽的三个缺陷永久锁在了 CI 里。

  • 前端
  • 图形学

【免费下载链接】bloub

SVG recreation of the x.ai bot avatar. One shape morphing through 14 states, measured off the reference video frame by frame.

项目地址:https://gitcode.com/gh_mirrors/bl/bloub
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询