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.几点说明:

  1. 如果要加载外部脚本,我们需要为 JSDOM 设置 runScripts: "dangerously" 标志(参见 this issue)。

  2. 我们需要自己手动重新触发 load 事件 - 基本上,在您的脚本执行时,document.readyState 的状态是 "complete" ,即 load 事件已经触发。

    这里发生的事情是,一旦 JSDOM 完成编译我们在初始化时传递给它的 HTML 脚本,window 就准备好了。我们可以导入我们需要的东西,然后 手动触发 load 事件 - 只要我们不将任何脚本传递给初始 JSDOM 调用,我们就可以确保我们不会触发任何东西两次。

  3. 当加载事件触发时,生成的 <script> 标签实际上被添加到 DOM,但由于它们包含虚拟 URL,实际上没有任何内容加载,进程抛出:Error: Could not load script: "https://file-1.js/"。为了测试,我将那些 URL 更改为 jQuery 库和 Hammer.js,您将需要添加逻辑以确保 URL 是安全的。

  4. 因为两个脚本都设置了 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 = ... 声明 - 当启用完全浏览器兼容性时,这些是在全局上下文中评估的,这导致尝试重新- 设置声明为 constwindow.url 变量。简单地设置 windowdocument 全局变量 适用于此用例,但如果您开始加载复杂的脚本,您可能 运行 会遇到问题。

如果您的脚本可以在浏览器中 运行(即没有冲突的全局 const 变量),我建议使用 enable-browser-mode,然后替换任何 require 使用 include() 调用浏览器 JS(test1.jstest2.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");
    });
  });
});