直接比对 innerhtml 会误报,是因为它包含非结构信息(如 data-reactroot、随机 key、毫秒时间戳、浏览器自动补全标签、注释节点等),导致每次字符串不一致;而结构测试应比对标准化的 dom 树(标签名、白名单属性、子节点顺序、文本内容),而非原始 html 字符串。

为什么直接比对 innerHTML 会误报
你写 expect(container.innerHTML).toBe(expectedHtml),看似简单,但只要页面里有 React 的 data-reactroot、Vue 的 data-v-xxxx、随机生成的 key 或毫秒级时间戳,每次运行结果都不同——不是结构变了,是元数据在变。快照文件体积爆炸,CI 上频繁失败。
- 浏览器自动补全的
<tbody>、<code>等标签,在原始 HTML 里不存在,但 DOM 中真实存在;比对原始字符串必然不等 - JS 渲染后插入的注释节点(如
<!-- react-mount-point-unstable -->)会被innerHTML包含,但不影响视觉和功能 -
textContent会合并相邻文本节点、忽略换行缩进,而innerHTML保留所有空格和格式,导致“看起来一样却断言失败” - 用
document.createElement('template')装载待测 HTML,再调用template.content.cloneNode(true),绕过浏览器实时解析差异 - 对克隆后的根节点调用
el.normalize(),合并相邻文本节点,消除换行/空格带来的干扰 - 只保留语义性属性:过滤掉
data-reactroot、data-testid、style(除非测试样式逻辑)、class(若 class 不影响结构,可选过滤) - 递归遍历节点,序列化为标准化对象:{ tagName, attributes: { id, role }, childNodes: [...] },再用 Jest 深比较
- 结构测试只读取
node.nodeType、node.tagName、node.getAttribute('id')、node.textContent,绝不调用getBoundingClientRect()或window.getComputedStyle() - 对表格、列表等复合结构,额外验证
tr下必须有td或th,禁止出现tr > div这类非法嵌套(可用container.querySelectorAll('tr > *:not(td):not(th)')扫描) - 用
screen.debug()或container.innerHTML快速确认实际挂载结构,避免把“组件未渲染完成”误判为“结构异常” - diff 工具优先比对路径最长的叶子节点,再向上收敛;这样能准确定位到
article里多了一个<span class="sr-only"></span> - 避免用
JSON.stringify直接序列化 DOM 对象——它会带大量不可枚举属性和循环引用,改用自定义遍历函数
用 cloneNode(true) + normalize() 构建纯净 DOM 树
真正可比的是节点结构本身:标签名、属性(仅白名单)、子节点顺序、文本内容。关键不是“字符串像不像”,而是“树形关系是否一致”。
如何定位结构性变更而非渲染差异
DOM 结构回归 ≠ 视觉回归。Safari 的 flex 塌陷、Chrome 的字体渲染偏移、CSS 动画帧延迟,这些都不该在结构测试里报警。
diff 输出要指向具体节点路径
报错信息写 Expected 27 nodes, got 28 毫无意义。开发需要知道“哪个 <div> 多了一层嵌套”,而不是总数差一。<ul>
<li>序列化时给每个节点打唯一路径标记,例如 <code>body > main > section:nth-child(2) > article > header
真正难的不是写出 diff 逻辑,而是定义清楚“什么才算结构不变”:是标签完全一致?还是允许 div 替换成 section 只要语义等价?这个边界得团队对齐,不能靠工具替你决定。











