【问题标题】:JSDOM script element being overwritten in MochaJSDOM 脚本元素在 Mocha 中被覆盖
【发布时间】:2020-06-13 03:21:38
【问题描述】:

我有两个几乎相同的 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 中有两个孩子,但根据上述情况,只有 1 个。看来 Mocha 是首先在所有测试中加载所有必需的文件,而第二个文件中的 appendChild 正在覆盖第一个文件中的值。

有没有办法解决这个问题?我尝试了 done() 或在调用 require 的地方移动,但结果相同。

【问题讨论】:

    标签: javascript testing mocha.js global jsdom


    【解决方案1】:

    在查看 Christian 的回答中的 repo 后,我意识到我需要在导入每个文件后触发 window.onload 事件。

    另外,我不想运行(“危险”)脚本,只需确保将它们作为脚本元素附加到文档中。就是这样。

    以下作品:

    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");
        });
      });
    });
    

    【讨论】:

    • 如果您不知道这些脚本将包含什么,您可以为每个脚本执行downloaderPopup = ...; if(window.onload) window.onload(); delete window.onload;(因为这里只是手动设置window.onload,这是一个问题),然后触发实际 加载事件,一旦你的所有“预处理”都用window.dispatchEvent 完成。我之所以提到这一点,是因为您最终可能需要不符合预期的脚本,而这种范例可能会让您头疼。
    【解决方案2】:

    I created a repo for you to look at.几点注意事项:

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

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

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

    3. 当 load 事件触发时,生成的 &lt;script&gt; 标签实际上被添加到 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(暴露了window 和@ 987654344@ globals)和 enable-browser-mode(旨在完全模拟浏览器运行时,将 global 对象设置为 window 并公开 window.include 函数以评估全局中的导入脚本上下文,即include('jquery.min.js'),没有错误)。

    对于这种情况(以及测试脚本的低复杂性),enable-window-document 就足够了。在完全浏览器兼容模式下运行时,我们实际上会因为两个脚本中的 const url = ... 声明而失败 - 当启用完全浏览器兼容性时,它们会在全局上下文中进行评估,这会导致尝试重新设置 window.url 变量声明为const。只需设置 window 和 document 全局变量将适用于此用例,但如果您开始加载复杂的脚本,您可能会遇到问题。

    如果您的脚本可以在浏览器中运行(即没有冲突的全局const 变量),我建议使用enable-browser-mode,然后替换对浏览器JS 的任何require 调用(test1.js 和@ 987654358@) 和include()。这将确保 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 中不存在等等)。我们在这里所做的事情非常 hacky,似乎超出了套件的范围,或者与它的原生 JSDOM 接口发生冲突,尽管我可能会遗漏一些东西。如果您想花时间调试它,请做我的客人:我离开了项目结构,以便您可以运行 jest 并查看它在抱怨什么,但您需要取消注释掉 describe 语句等。

    无论如何,希望这会有所帮助。

    【讨论】:

    • 感谢您非常彻底、详细的回复。我真诚地感谢您为此付出的大量思考和努力。在查看您的代码后,我意识到问题是 onload 事件需要在需要每个文件后触发 - 所以有一个更简单、更清洁的解决方案,我将很快发布。但如果没有你的帮助,我是不可能做到的。谢谢。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-30
    相关资源
    最近更新 更多