JSDOM 脚本元素在 Mocha 中被覆盖
JSDOM script element being overwritten in Mocha
我有两个几乎相同的 JS 文件,我无法更改,我想为其添加测试。
文件 1:
const url = "https://file-1.js";
(function () {
"use strict";
window.onload = () => {
const script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
};
})();
文件 2:
const url = "https://file-2.js";
(function () {
"use strict";
window.onload = () => {
const script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
};
})();
然后测试1:
const chai = require("chai");
const { expect } = chai;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const { window } = new JSDOM(`<!DOCTYPE html><head></head><p>Fake document</p>`, {
resources: "usable",
});
global.document = window.document;
global.window = window;
const myFile = require("../src/myFile");
describe("Test 1", function () {
it("Loads a file from an external source", function (done) {
console.log(window.document.head.children); // See what's going on
expect(window.document.head.children[0].src).to.equal("https://file-1.js");
});
});
测试 2:
const chai = require("chai");
const { expect } = chai;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const myFile2 = require("../src/myFile2");
describe("Test 2", function () {
it("Loads a file from an external source", function (done) {
console.log(window.document.head.children); // See what's going on
expect(window.document.head.children[0].src).to.equal("https://file-2.js");
});
});
测试 2 通过但测试 1 失败。 console.logs 的值为:
HTMLCollection { '0': HTMLScriptElement {} }
并且 console.log(window.document.head.children[0].src)
产生:
https://file-2.js
我希望在 window.document.head
中有两个 children,但根据上述只有 1 个。看来 Mocha 首先在所有测试中加载所有必需的文件,而第二个文件中的 appendChild 正在覆盖第一个文件中的值。
有办法解决这个问题吗?我尝试了 done()
或在调用 require 的地方四处移动,但结果相同。
I created a repo for you to look at.几点说明:
如果要加载外部脚本,我们需要为 JSDOM 设置 runScripts: "dangerously"
标志(参见 this issue)。
我们需要自己手动重新触发 load
事件 - 基本上,在您的脚本执行时,document.readyState
的状态是 "complete"
,即 load
事件已经触发。
这里发生的事情是,一旦 JSDOM 完成编译我们在初始化时传递给它的 HTML 脚本,window
就准备好了。我们可以导入我们需要的东西,然后 手动触发 load
事件 - 只要我们不将任何脚本传递给初始 JSDOM 调用,我们就可以确保我们不会触发任何东西两次。
当加载事件触发时,生成的 <script>
标签实际上被添加到 DOM,但由于它们包含虚拟 URL,实际上没有任何内容加载,进程抛出:Error: Could not load script: "https://file-1.js/"
。为了测试,我将那些 URL 更改为 jQuery 库和 Hammer.js,您将需要添加逻辑以确保 URL 是安全的。
因为两个脚本都设置了 window.onload = function() {...}
,如果我们 运行 他们两个然后触发 load
事件(我们通常会这样做),只有最后一个一个将被触发,因为每个 window.onload
集都会覆盖前者。
我们可以解决这个问题,但这只是因为我们知道脚本包含的内容。查看解决方法的测试文件:只需 require
,启动 onload,然后使用 delete window.onload
。我使用 dispatchEvent
只是为了显示它的形式,但是由于覆盖问题对于 window.addEventListener
不是问题(只是为了天真地设置 window.onload
属性),它调用 window.onload()
然后删除它可能会更好。虽毛茸茸,但并非难驾驭
过去几天我实际上一直在做与此类似的事情,并且最近提出了两个包来帮助处理类似的情况:enable-window-document (which exposes window
and document
globals) and enable-browser-mode(旨在完全模拟浏览器 运行 时间,将 global
对象设置为 window
并公开一个 window.include
函数以在全局上下文中评估导入的脚本,即 include('jquery.min.js')
,没有错误)。
对于这种情况(以及测试脚本的低复杂度),enable-window-document 就足够了。当 运行 在完全浏览器兼容模式下,我们实际上会失败,因为两个脚本中的 const url = ...
声明 - 当启用完全浏览器兼容性时,这些是在全局上下文中评估的,这导致尝试重新- 设置声明为 const
的 window.url
变量。简单地设置 window
和 document
全局变量 将 适用于此用例,但如果您开始加载复杂的脚本,您可能 运行 会遇到问题。
如果您的脚本可以在浏览器中 运行(即没有冲突的全局 const
变量),我建议使用 enable-browser-mode
,然后替换任何 require
使用 include()
调用浏览器 JS(test1.js
和 test2.js
)。这将确保 window 引用全局对象,并且您的普通浏览器 JS 将按预期执行。
加载所有脚本并解决 onload
冲突后,我们 运行 测试:
// inside index.js
...
console.log(document.head.outerHTML);
console.log("jQuery:", window.$);
$ node .
RUNNING TESTS...
<head><script src="https://code.jquery.com/jquery-3.5.1.min.js"></script><script src="https://hammerjs.github.io/dist/hammer.min.js"></script></head>
jQuery: undefined
奇怪的是我们无法在 运行 时访问 window.jQuery
。然而,在控制台中:
$ node
Welcome to Node.js v14.4.0.
Type ".help" for more information.
> require('.')
RUNNING TESTS...
<head><script src="https://code.jquery.com/jquery-3.5.1.min.js"></script><script src="https://hammerjs.github.io/dist/hammer.min.js"></script></head>
jQuery: undefined
{}
> window.jQuery
<ref *1> [Function: S] {
fn: S {
jquery: '3.5.1',
constructor: [Circular *1],
length: 0,
toArray: [Function: toArray]
...
因此,我建议您四处游玩,看看什么可以上班,什么不能上班。
脚注:Jest 是热门的 Facebook 垃圾,我不会关心调试它(声称 window
global 在 myFile.js
中不存在等等)。我们在这里做的事情很老套,似乎超出了套件的范围,或者以某种方式与其本机 JSDOM 接口冲突,尽管我可能会遗漏一些东西。如果你想花时间调试它,做我的客人:我离开了项目结构,这样你就可以 运行 jest
看看它在抱怨什么,但你需要取消注释掉 describe
语句等
无论如何,希望这对您有所帮助。
在查看了 Christian 的回答中的 repo 后,我意识到我需要在导入每个文件后触发 window.onload
事件。
此外,我不想运行 ('dangerously') 脚本,只是确保将它们作为脚本元素附加到一个文件中。就这些了。
以下作品:
const chai = require("chai");
const { expect } = chai;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const { window } = new JSDOM(`<!DOCTYPE html><head></head><p>Fake document</p>`, {
resources: "usable",
});
global.document = window.document;
global.window = window;
const downloaderPopup = require("../src/MyFile");
window.dispatchEvent(new window.Event("load"));
const downloaderMain = require("../src/MyFile2");
window.dispatchEvent(new window.Event("load"));
describe("Both tests", function () {
describe("Test 1", function () {
it("Dynamocally loads file 1", function () {
expect(window.document.head.children[1].src).to.equal("https://file-1.js");
});
});
describe("Test 2", function () {
it("Dynamically loads file 2", function () {
expect(window.document.head.children[0].src).to.equal("https://file-2.js");
});
});
});
我有两个几乎相同的 JS 文件,我无法更改,我想为其添加测试。
文件 1:
const url = "https://file-1.js";
(function () {
"use strict";
window.onload = () => {
const script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
};
})();
文件 2:
const url = "https://file-2.js";
(function () {
"use strict";
window.onload = () => {
const script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
};
})();
然后测试1:
const chai = require("chai");
const { expect } = chai;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const { window } = new JSDOM(`<!DOCTYPE html><head></head><p>Fake document</p>`, {
resources: "usable",
});
global.document = window.document;
global.window = window;
const myFile = require("../src/myFile");
describe("Test 1", function () {
it("Loads a file from an external source", function (done) {
console.log(window.document.head.children); // See what's going on
expect(window.document.head.children[0].src).to.equal("https://file-1.js");
});
});
测试 2:
const chai = require("chai");
const { expect } = chai;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const myFile2 = require("../src/myFile2");
describe("Test 2", function () {
it("Loads a file from an external source", function (done) {
console.log(window.document.head.children); // See what's going on
expect(window.document.head.children[0].src).to.equal("https://file-2.js");
});
});
测试 2 通过但测试 1 失败。 console.logs 的值为:
HTMLCollection { '0': HTMLScriptElement {} }
并且 console.log(window.document.head.children[0].src)
产生:
https://file-2.js
我希望在 window.document.head
中有两个 children,但根据上述只有 1 个。看来 Mocha 首先在所有测试中加载所有必需的文件,而第二个文件中的 appendChild 正在覆盖第一个文件中的值。
有办法解决这个问题吗?我尝试了 done()
或在调用 require 的地方四处移动,但结果相同。
I created a repo for you to look at.几点说明:
如果要加载外部脚本,我们需要为 JSDOM 设置
runScripts: "dangerously"
标志(参见 this issue)。我们需要自己手动重新触发
load
事件 - 基本上,在您的脚本执行时,document.readyState
的状态是"complete"
,即load
事件已经触发。这里发生的事情是,一旦 JSDOM 完成编译我们在初始化时传递给它的 HTML 脚本,
window
就准备好了。我们可以导入我们需要的东西,然后 手动触发load
事件 - 只要我们不将任何脚本传递给初始 JSDOM 调用,我们就可以确保我们不会触发任何东西两次。当加载事件触发时,生成的
<script>
标签实际上被添加到 DOM,但由于它们包含虚拟 URL,实际上没有任何内容加载,进程抛出:Error: Could not load script: "https://file-1.js/"
。为了测试,我将那些 URL 更改为 jQuery 库和 Hammer.js,您将需要添加逻辑以确保 URL 是安全的。因为两个脚本都设置了
window.onload = function() {...}
,如果我们 运行 他们两个然后触发load
事件(我们通常会这样做),只有最后一个一个将被触发,因为每个window.onload
集都会覆盖前者。我们可以解决这个问题,但这只是因为我们知道脚本包含的内容。查看解决方法的测试文件:只需
require
,启动 onload,然后使用delete window.onload
。我使用dispatchEvent
只是为了显示它的形式,但是由于覆盖问题对于window.addEventListener
不是问题(只是为了天真地设置window.onload
属性),它调用window.onload()
然后删除它可能会更好。虽毛茸茸,但并非难驾驭
过去几天我实际上一直在做与此类似的事情,并且最近提出了两个包来帮助处理类似的情况:enable-window-document (which exposes window
and document
globals) and enable-browser-mode(旨在完全模拟浏览器 运行 时间,将 global
对象设置为 window
并公开一个 window.include
函数以在全局上下文中评估导入的脚本,即 include('jquery.min.js')
,没有错误)。
对于这种情况(以及测试脚本的低复杂度),enable-window-document 就足够了。当 运行 在完全浏览器兼容模式下,我们实际上会失败,因为两个脚本中的 const url = ...
声明 - 当启用完全浏览器兼容性时,这些是在全局上下文中评估的,这导致尝试重新- 设置声明为 const
的 window.url
变量。简单地设置 window
和 document
全局变量 将 适用于此用例,但如果您开始加载复杂的脚本,您可能 运行 会遇到问题。
如果您的脚本可以在浏览器中 运行(即没有冲突的全局 const
变量),我建议使用 enable-browser-mode
,然后替换任何 require
使用 include()
调用浏览器 JS(test1.js
和 test2.js
)。这将确保 window 引用全局对象,并且您的普通浏览器 JS 将按预期执行。
加载所有脚本并解决 onload
冲突后,我们 运行 测试:
// inside index.js
...
console.log(document.head.outerHTML);
console.log("jQuery:", window.$);
$ node .
RUNNING TESTS...
<head><script src="https://code.jquery.com/jquery-3.5.1.min.js"></script><script src="https://hammerjs.github.io/dist/hammer.min.js"></script></head>
jQuery: undefined
奇怪的是我们无法在 运行 时访问 window.jQuery
。然而,在控制台中:
$ node
Welcome to Node.js v14.4.0.
Type ".help" for more information.
> require('.')
RUNNING TESTS...
<head><script src="https://code.jquery.com/jquery-3.5.1.min.js"></script><script src="https://hammerjs.github.io/dist/hammer.min.js"></script></head>
jQuery: undefined
{}
> window.jQuery
<ref *1> [Function: S] {
fn: S {
jquery: '3.5.1',
constructor: [Circular *1],
length: 0,
toArray: [Function: toArray]
...
因此,我建议您四处游玩,看看什么可以上班,什么不能上班。
脚注:Jest 是热门的 Facebook 垃圾,我不会关心调试它(声称 window
global 在 myFile.js
中不存在等等)。我们在这里做的事情很老套,似乎超出了套件的范围,或者以某种方式与其本机 JSDOM 接口冲突,尽管我可能会遗漏一些东西。如果你想花时间调试它,做我的客人:我离开了项目结构,这样你就可以 运行 jest
看看它在抱怨什么,但你需要取消注释掉 describe
语句等
无论如何,希望这对您有所帮助。
在查看了 Christian 的回答中的 repo 后,我意识到我需要在导入每个文件后触发 window.onload
事件。
此外,我不想运行 ('dangerously') 脚本,只是确保将它们作为脚本元素附加到一个文件中。就这些了。
以下作品:
const chai = require("chai");
const { expect } = chai;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const { window } = new JSDOM(`<!DOCTYPE html><head></head><p>Fake document</p>`, {
resources: "usable",
});
global.document = window.document;
global.window = window;
const downloaderPopup = require("../src/MyFile");
window.dispatchEvent(new window.Event("load"));
const downloaderMain = require("../src/MyFile2");
window.dispatchEvent(new window.Event("load"));
describe("Both tests", function () {
describe("Test 1", function () {
it("Dynamocally loads file 1", function () {
expect(window.document.head.children[1].src).to.equal("https://file-1.js");
});
});
describe("Test 2", function () {
it("Dynamically loads file 2", function () {
expect(window.document.head.children[0].src).to.equal("https://file-2.js");
});
});
});