В тесте QUnit событие click срабатывает только в том случае, если ссылка получена до запуска теста. - PullRequest
6 голосов
/ 10 марта 2019

У меня есть следующий код, который я хочу проверить с помощью 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');
    });
}());

Как ни странно, если я вызову getElementById внутри тестового метода Qunit, обработчик событий, прикрепленный к кнопке, не будет вызван. Однако, если я переместу вызов на getElementById вне функции теста, событие вызовет обработчик события click.

Что делает QUnit, который мешает моему тесту работать должным образом, и каков правильный / рекомендуемый способ решения проблемы, с которой я сталкиваюсь?

Вот 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. Я еще не на той стадии, когда могу или хочу изменить тестируемый код.

1 Ответ

5 голосов
/ 10 марта 2019

Когда вы выполняете QUnit.test(), кажется, что все в DOM ниже <div id="qunit-fixture"> клонировано и заменено.Это означает, что прослушиватель событий, который вы добавили до этого вызова, находится в другом <button>, чем сейчас существует в DOM.btn.click(); работает, потому что запускает событие на исходном <button>, для которого вы сохранили ссылку с исходным var btn =.

Если btn определено в QUnit.test(), тоон ссылается на новый <button>, которому не назначен прослушиватель событий.Ожидается, что QUnit будет использоваться в основном с jQuery, поэтому он, вероятно, делает jQuery .clone(), который можно настроить для копирования слушателей событий на основе jQuery, но не прослушивателей vanilla JavaScript, поскольку базовый Node.cloneNode() не имеет такой возможности.

Вы можете подтвердить, что оригинал btn отключен от DOM с помощью console.log(btn.parentNode.parentNode); в пределах QUnit.test().Это выводит null для оригинального btn, но является <body> для того, который существует в QUnit.test().Чтобы продемонстрировать это, btn, определенный до запуска теста, присваивается btnBeforeQUnit в приведенном ниже коде.

QUnit клонирование содержимого HTML - это хороший способ справиться с независимостью друг от друга тестов, нодолжны быть задокументированы.Обычно желательно, чтобы юнит-тесты были независимыми.В конце концов, могут быть тесты, которые изменяют структуру DOM, которую следует восстанавливать между тестами.

Однако по какой-то причине QUnit не восстанавливает исходные элементы DOM в конце QUnit.test().Документация указывает, что он должен повторно клонировать оригинал до следующего теста, но он не восстанавливает оригинал после завершения теста.

В дополнение к btnBeforeQUnit и btn, 2-йродительские уровни для btnAfterQUnit и btnDelayedAfterQUnit также выводятся на консоль, чтобы более точно продемонстрировать, когда происходит подстановка 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;

Документация гласит :

QUnit.config.fixture (string) |по умолчанию: undefined

Определяет содержимое HTML для использования в контейнере фикстур, который сбрасывается в начале каждого теста.

По умолчанию QUnit будет использовать любой начальный контент #qunit-fixture как сброс устройства.Если вы не хотите, чтобы прибор сбрасывался между тестами, установите значение 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>

Использование атрибутов HTML упоминается Mat в чате .

...