在 QUnit 测试中,仅当在测试运行之前获得引用时才会触发 click 事件

Within a QUnit test the click event will only fire if the reference is obtained before the test runs

我想用 Qunit.

测试以下代码
// my code under test
document.getElementById('saveButton').addEventListener('click',save);

function save() {
  console.log('save clicked');
}

我的 QUnit 测试获取按钮的引用并调用点击函数:

(function () {
    "use strict";

    // HACK: with this line here click  works
    //var btn = document.getElementById('saveButton');

    // the test
    QUnit.test("click save", function (assert) {
         // with this line no click
        var btn = document.getElementById('saveButton');
        btn.click(); // will it click?
        assert.ok(btn !== null , 'button found');
    });
}());

奇怪的是,如果我在 Qunit 的测试方法中调用 getElementById,则不会调用附加到按钮的事件处理程序。但是,如果我将调用移至测试函数之外的 getElementById,该事件会触发点击事件处理程序。

QUnit 在那里做了什么阻止我的测试按预期工作?proper/recommended 解决我面临的问题的方法是什么?

这是一个演示非工作版本的 MCVE(也在 JSFiddle 上)。我已经评论了要更改什么才能使点击正常工作(此处的工作方式:将文本输出到控制台)

// code under test
document.getElementById('saveButton').addEventListener('click',save);

function save() {
  console.log('save clicked');
}

// QUnit tests
(function () {
 "use strict";
  // with this line here click  works
  //var btn = document.getElementById('saveButton');
  QUnit.test("click save", function (assert) {
    // with this line no click, comment it to test the working variant
    var btn = document.getElementById('saveButton');
    btn.click(); // will it click?
    assert.ok(btn !== null , 'button found');
  });
}());
<script src="https://code.jquery.com/qunit/qunit-2.9.2.js"></script>
<div id="qunit"></div>
<div id="qunit-fixture">
  <button id="saveButton">test</button>
</div>

值得注意的是,我在这里明确不使用 jQuery,因为我正在为其编写测试的代码也没有使用 jQuery。我还没有达到可以或愿意更改被测代码的阶段。

当您执行 QUnit.test() 时,似乎 <div id="qunit-fixture"> 下面的 DOM 中的所有内容都被克隆和替换。这意味着您在该调用之前添加的事件侦听器与现在存在于 DOM 中的 <button> 不同。 btn.click(); 起作用是因为它在原始 <button> 上触发事件,您已将引用保存到原始 var btn =.

如果 btn 是在 QUnit.test() 中定义的,那么它会引用新的 <button>,它没有分配给它的事件侦听器。 QUnit 预计主要与 jQuery 一起使用,因此它可能 jQuery .clone(), which can be set to copy the jQuery based event listeners, but not the vanilla JavaScript listeners, because the underlying Node.cloneNode() 没有那种能力。

您可以确认原来的btnQUnit.test()中被console.log(btn.parentNode.parentNode);与DOM断开了连接。原始 btn 输出 null,但 QUnit.test() 中存在的输出 <body>。为了证明这一点,在 运行 测试之前确定的 btn 在下面的代码中被分配给 btnBeforeQUnit

QUnit 克隆 HTML 内容是处理使测试彼此独立的好方法,但应记录在案。通常希望单元测试是独立的。毕竟,可能会有更改 DOM 结构的测试,应该在测试之间恢复。

但是,由于某种原因,QUnit 没有恢复 QUnit.test() 末尾的原始 DOM 个元素。文档表明它应该在下一次测试之前重新克隆原始文件,但在测试完成后它不会恢复原始文件。

除了btnBeforeQUnitbtn之外,btnAfterQUnitbtnDelayedAfterQUnit的二级父级也会输出到控制台,以更准确地演示何时DOM 替换发生在异步执行提供给 QUnit.test().

的回调中

// code under test
document.getElementById('saveButton').addEventListener('click',save);

function save() {
  console.log('save clicked');
}

// QUnit tests
(function () {
 "use strict";
  // with this line here click  works
  var btnBeforeQUnit = document.getElementById('saveButton');
  QUnit.test("click save", function (assert) {
    // with this line no click, comment it to test the working variant
    var btn = document.getElementById('saveButton');
    var btnInsideQUnit = btn;
    btn.click(); // will it click?
    console.log('btnBeforeQUnit.parentNode.parentNode', btnBeforeQUnit.parentNode.parentNode);
    console.log('btnInsideQUnit.parentNode.parentNode', btnInsideQUnit.parentNode.parentNode)
    assert.ok(btn !== null , 'buttin found');
  });
  var btnAfterQUnit = document.getElementById('saveButton');
  console.log('btnAfterQUnit.parentNode.parentNode', btnAfterQUnit.parentNode.parentNode);
  setTimeout(function() {
    var btnDelayedAfterQUnit = document.getElementById('saveButton');
    console.log('btnDelayedAfterQUnit.parentNode.parentNode', btnAfterQUnit.parentNode.parentNode);
  }, 1000);
}());
<script src="https://code.jquery.com/qunit/qunit-2.9.2.js"></script>
<div id="qunit"></div>
<div id="qunit-fixture">
  <button id="saveButton">test</button>
</div>

调整此行为

您可以通过将 QUnit.config.fixture 设置为 null 来获得您期望的行为:

QUnit.config.fixture = null;

documentation says:

QUnit.config.fixture (string) | default: undefined

Defines the HTML content to use in the fixture container which is reset at the start of each test.

By default QUnit will use whatever the starting content of #qunit-fixture is as the fixture reset. If you do not want the fixture to be reset in between tests, set the value to null.

但是,将此选项设置为 null 时应小心。这将意味着 DOM 对于所有设置为 null 的 运行 测试不是独立的。换句话说,您在一个测试中对 DOM 所做的更改将影响其他测试中的更改。

IMO,QUnit.test() 在克隆 DOM 中的整体行为没有明确记录。肯定应该在主文档中提到一些行为。特别是副作用。应该明确提及现有的事件侦听器,但我没有在 QUinit 文档中找到任何明确描述此过程及其影响的内容。

以下是相同的代码,但添加了 QUnit.config.fixture = null;。如您所见,"save clicked" 是从测试输出到控制台的,而它不是原始输出。

// code under test
document.getElementById('saveButton').addEventListener('click',save);

function save() {
  console.log('save clicked');
}

// QUnit tests
(function () {
 "use strict";
  QUnit.config.fixture = null;
  // with this line here click  works
  var btnBeforeQUnit = document.getElementById('saveButton');
  QUnit.test("click save", function (assert) {
    // with this line no click, comment it to test the working variant
    var btn = document.getElementById('saveButton');
    var btnInsideQUnit = btn;
    btn.click(); // will it click?
    console.log('btnBeforeQUnit.parentNode.parentNode', btnBeforeQUnit.parentNode.parentNode);
    console.log('btnInsideQUnit.parentNode.parentNode', btnInsideQUnit.parentNode.parentNode)
    assert.ok(btn !== null , 'buttin found');
  });
  var btnAfterQUnit = document.getElementById('saveButton');
  console.log('btnAfterQUnit.parentNode.parentNode', btnAfterQUnit.parentNode.parentNode);
  setTimeout(function() {
    var btnDelayedAfterQUnit = document.getElementById('saveButton');
    console.log('btnDelayedAfterQUnit.parentNode.parentNode', btnAfterQUnit.parentNode.parentNode);
  }, 1000);
}());
<script src="https://code.jquery.com/qunit/qunit-2.9.2.js"></script>
<div id="qunit"></div>
<div id="qunit-fixture">
  <button id="saveButton">test</button>
</div>

或者,为您的预定义事件侦听器使用 HTML 属性

对于您遇到的特定问题,即希望在测试之前设置事件侦听器,您可以使用 HTML 属性来定义侦听器(例如,在您的案例中为 onclick="save()" .

// code under test
document.getElementById('saveButton').addEventListener('click',save);

function save() {
  console.log('save clicked');
}

// QUnit tests
(function () {
 "use strict";
  // with this line here click  works
  var btnBeforeQUnit = document.getElementById('saveButton');
  QUnit.test("click save", function (assert) {
    // with this line no click, comment it to test the working variant
    var btn = document.getElementById('saveButton');
    var btnInsideQUnit = btn;
    btn.click(); // will it click?
    console.log('btnBeforeQUnit.parentNode.parentNode', btnBeforeQUnit.parentNode.parentNode);
    console.log('btnInsideQUnit.parentNode.parentNode', btnInsideQUnit.parentNode.parentNode)
    assert.ok(btn !== null , 'buttin found');
  });
  var btnAfterQUnit = document.getElementById('saveButton');
  console.log('btnAfterQUnit.parentNode.parentNode', btnAfterQUnit.parentNode.parentNode);
  setTimeout(function() {
    var btnDelayedAfterQUnit = document.getElementById('saveButton');
    console.log('btnDelayedAfterQUnit.parentNode.parentNode', btnAfterQUnit.parentNode.parentNode);
  }, 1000);
}());
<script src="https://code.jquery.com/qunit/qunit-2.9.2.js"></script>
<div id="qunit"></div>
<div id="qunit-fixture">
  <button id="saveButton" onclick="save()">test</button>
</div>

Mat in chat 提到了使用 HTML 属性。