如何在Jest测试中通过JSDOM成功执行动态创建的JS脚本?
问题:Jest+jsdom测试DOM操作模块失败,脚本未执行
我有一个封装DOM操作便捷方法的模块,用Jest和jsdom做测试时遇到问题——测试里dom.window.document.addEventListener('DOMContentLoaded')内的断言不通过,../mocks/forTesting.js里的console.log也没输出。目标是让脚本执行,把id为"container"的div内容改成'test'。
测试代码
import { runScript } from '../export/script.js'; import { JSDOM } from 'jsdom' const dom = new JSDOM(`<!DOCTYPE html><html><body><div id="container">Hello</div></body></html>`, { resources: 'usable', runScripts: "dangerously" }) global.window = dom.window global.document = dom.window.document describe("Rendering Module", () => { test("Verify if mocked script was executed", async () => { const script = { name: 'testScript', value: `<script src="../mocks/forTesting.js" ></script>` } const scriptFreezed = Object.freeze(script); await runScript(scriptFreezed, dom.window.document); dom.window.document.addEventListener('DOMContentLoaded', () => { const text = dom.window.document.getElementById('container').innerHTML; expect(text).toEqual('test') }); dom.window.document.dispatchEvent(new dom.window.Event('DOMContentLoaded', {"bubbles": true})) }) })
报错信息
console.error Error: Uncaught [Error: expect(received).toEqual(expected) // deep equality Expected: "test" Received: "Hello"
../mocks/forTesting.js代码
window.document.addEventListener("DOMContentLoaded", function (event) { console.log('forTesting.js') window.document.getElementById('container').innerHTML = 'test'; });
runScript代码
const runScript = (script, dom) => { const node = dom.createRange().createContextualFragment(script.value); dom.head.appendChild(node); }
问题原因及修复方案
核心问题
- 脚本加载异步未处理:
runScript只是把script标签插入DOM,但脚本加载是异步操作,直接await它没用(因为它没返回Promise),导致你提前触发了DOMContentLoaded事件,此时脚本还没加载完成,监听逻辑根本没注册上。 - 事件触发顺序错误:先触发事件再等脚本加载,脚本里的监听自然不会执行。
- 相对路径可能失效:jsdom环境下相对路径加载脚本容易找不到文件,导致脚本根本没被执行。
修复步骤
1. 改造runScript,支持等待脚本加载完成
修改runScript,返回Promise监听脚本的加载状态:
const runScript = (script, dom) => { return new Promise((resolve, reject) => { const fragment = dom.createRange().createContextualFragment(script.value); const scriptElement = fragment.querySelector('script'); dom.head.appendChild(fragment); // 监听脚本加载完成/失败事件 scriptElement.onload = resolve; scriptElement.onerror = reject; }); }
2. 调整测试逻辑,确保脚本加载后再触发事件
用绝对路径加载脚本,避免路径问题,同时等待脚本加载完成后再处理事件:
import { runScript } from '../export/script.js'; import { JSDOM } from 'jsdom' import path from 'path'; import { fileURLToPath } from 'url'; // 生成脚本的绝对路径 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const scriptAbsolutePath = path.join(__dirname, '../mocks/forTesting.js'); const dom = new JSDOM(`<!DOCTYPE html><html><body><div id="container">Hello</div></body></html>`, { resources: 'usable', runScripts: "dangerously", url: 'http://localhost' // 设置base URL,避免资源加载跨域问题 }) const { window, document } = dom; describe("Rendering Module", () => { test("Verify if mocked script was executed", async () => { const script = { name: 'testScript', value: `<script src="${scriptAbsolutePath}" ></script>` }; const scriptFreezed = Object.freeze(script); // 等待脚本加载完成 await runScript(scriptFreezed, document); // 准备断言变量 let containerContent; document.addEventListener('DOMContentLoaded', () => { containerContent = document.getElementById('container').innerHTML; }); // 触发事件 document.dispatchEvent(new window.Event('DOMContentLoaded', { bubbles: true })); // 执行断言 expect(containerContent).toEqual('test'); }) })
3. 可选:简化forTesting.js逻辑(非必须)
因为jsdom初始化时页面已经解析完成,不需要再等DOMContentLoaded,可以直接执行逻辑:
console.log('forTesting.js') window.document.getElementById('container').innerHTML = 'test';
如果一定要保留DOMContentLoaded监听,上面的测试逻辑已经能保证事件触发时机正确。
内容的提问来源于stack exchange,提问作者Alice Arruda Atalla
相关产品推荐
相关产品推荐

