JavaScript 测试环境设置完整 Document 内容 (Jest/JSDOM)
2025-03-15 16:18:10
如何设置 JavaScript 测试环境下的整个 Document 内容?
在用 Jest 写单元测试时,遇到个问题:我想把一些网页的 HTML 内容保存到文件里,作为测试数据。我可以用下面的代码把保存下来的 HTML 文件中的 <body>
内容加载进来:
const fs = require('node:fs');
html = fs.readFileSync('blah-blah-blah.body.html', 'utf8');
document.body.innerHTML = html;
挺好用的,但就是有点麻烦。因为直接用 curl url -o blah-blah-blah.html
保存整个文档很容易,但要从中单独提取出 <body>
元素就得再费点事。我尝试过直接设置 document.documentElement
:
html = fs.readFileSync('blah-blah-blah.document.html', 'utf8');
document.documentElement = html;
可惜 document.documentElement
是只读的,没戏。那该怎么办呢?
问题分析
根本原因在于,document.documentElement
属性(它代表整个文档的根元素,通常是 <html>
元素)在浏览器和多数模拟环境(例如 jsdom, Jest使用的环境)下是被设计为只读的。 这是为了防止对文档结构进行潜在的破坏性修改。 如果允许随意替换documentElement
, 可能会导致文档状态的不一致或其他难以预料的问题。
解决方案
有几个方法可以解决这个问题,下面一一介绍:
1. 使用 innerHTML
设置 document.documentElement
尽管不能直接赋值 document.documentElement
,但可以修改它的 innerHTML
。这招最直接:
-
原理:
innerHTML
属性允许我们设置或获取元素的 HTML 内容。通过设置document.documentElement.innerHTML
,我们可以替换整个文档的内容(除了<html>
标签本身)。 -
代码示例:
const fs = require('node:fs');
const html = fs.readFileSync('blah-blah-blah.html', 'utf8');
document.documentElement.innerHTML = html;
- 注意事项: 这样会移除并重新创建所有的子节点。原来的DOM上绑定的事件处理都会失效。
2. 使用 JSDOM
的 innerHTML
如果你使用 JSDOM
这个库,能用另一种方式来设置整个文档。 (Jest 内部用了 JSDOM)
-
原理 : JSDOM库, 在模拟的时候能完全替换内容。
-
代码示例:
const fs = require('node:fs');
const { JSDOM } = require('jsdom');
const html = fs.readFileSync('my-page.html', 'utf8');
const dom = new JSDOM(html);
// 获取 JSDOM 实例的 document 对象
const document = dom.window.document;
// document 就具备完整的 DOM API 可以操作了
console.log(document.querySelector('title').textContent);
- 进阶 : 使用这种方法来完全控制DOM,可以实现更加复杂的功能.
- 可以方便模拟不同的 userAgent.
- 通过
window
上的 API 可以模拟全局的localStorage
,sessionStorage
.
3. 清空并重建 body
内容
一个简单粗暴的备选方案是,先清空整个 body
,再把新的 HTML 内容塞进去。虽然不能替换 <html>
,但通常测试用例更关注 <body>
内部。
-
原理: 就像一开始的例子,直接操作
document.body.innerHTML
。但这回我们先把它清空。 -
代码示例:
const fs = require('node:fs');
const html = fs.readFileSync('blah-blah-blah.html', 'utf8');
// 提取 <body> 部分 (可以使用正则表达式或 HTML 解析器库)
const bodyContent = extractBodyContent(html); // 自己实现 extractBodyContent 函数
document.body.innerHTML = ''; // 清空
document.body.innerHTML = bodyContent; // 设置新的内容
你要自己实现从完整 HTML 提取body content的部分. 正则表达式最简单,但是复杂文档容易出bug.
使用类似`cheerio` 之类的 HTML解析库可以稳健处理.
-
Cheerio
方案如果你使用
cheerio
这个库, 可以这样加载:
const fs = require('node:fs');
const cheerio = require('cheerio');
const html = fs.readFileSync('blah-blah-blah.html', 'utf8');
const $ = cheerio.load(html);
document.body.innerHTML = $('body').html();
4. 使用 Object.defineProperty
(慎用)
理论上 可以通过修改属性的符把 documentElement
变成可写的, 但是绝对不推荐 , 这么干风险极高, 而且 Jest 和 jsdom 可能在未来版本禁止这种操作。
- 原理: JavaScript 允许通过
Object.defineProperty
来修改属性的特性(如可写性、可枚举性等)。 - 代码:
(只展示思路,不要在生产使用!)
Object.defineProperty(document, 'documentElement', {
writable: true,
});
- 风险 :
- 破坏了jsdom 模拟的环境的稳定性, 可能引入未知bug.
- 和测试框架行为冲突.
如何选择?
- 如果简单替换内容能满足需求,首选方法 1(
document.documentElement.innerHTML = html
)。 - 需要用到更强大的 jsdom 提供的功能, 使用方法2 (JSDOM实例).
- 如果仅仅关心
body
内的内容, 且想尽可能减少对原来<html>
的修改,使用方法3 (处理body
内容). - 绝对不要试图通过
Object.defineProperty
更改只读属性。