August 8th, 2021

JavaScript: умная подсказка, пять автоматических тестов

Начало:
1. JavaScript: умная подсказка, разбор постановки задачи
2. JavaScript: умная подсказка, подключение автоматических тестов

Продолжаю разбирать задачу «"Умная" подсказка» к подразделу 3.2 «Движение мыши: mouseover/out, mouseenter/leave» второй части учебника по JavaScript.

Итак, мы пока еще не дописали ни одной новой строчки кода к тестовой HTML-странице и прилагающимся к ней скриптам и стилям, которые заданы в песочнице к обсуждаемой задаче. Только временно закомментировали скрипт демонстрационного примера на тестовой HTML-странице (см. предыдущий пост).

Откроем тестовую HTML-страницу в браузере. Автоматические тесты уже работают, всего их пять. Код этих тестов на языке JavaScript описан авторами задачи в файле test.js.

* * *

Как пользоваться этими тестами?

При запуске тестовой HTML-страницы запускаются все пять тестов последовательно. Если тест пройден успешно, слева от его названия появляется зеленая галка. Если тест пройден и получен отрицательный результат, слева от названия теста появляется красный крестик, название теста отражается красным цветом, а ниже названия теста появляется блок с информацией об ошибке, тоже красным цветом, в котором, на самом деле, большая часть информации нам, как пользователям теста, не нужна (достаточно проанализировать первую строчку этого блока).

На название каждого теста можно нажать мышкой, после чего под названием теста (а в случае наличия блока с информацией об ошибке — под этим блоком) появится блок с кодом теста на языке JavaScript (это тот же код, который описан в файле test.js), чтобы можно было мгновенно, не отвлекаясь в другие файлы, вспомнить содержание (смысл) теста. Повторным нажатием мыши на название теста блок с кодом теста можно убрать.

Справа от названия каждого теста есть кружок со стрелкой вправо. При нажатии мышью на этот кружок ранее выведенные результаты тестов будут стерты, и будет запущен только этот конкретный тест, следовательно, и результаты будут выведены только для этого конкретного теста. Чтобы вернуться к запуску всех пяти имеющихся тестов, нужно нажать мышью на заголовок над всеми пятью тестами (в нашем случае в этом заголовке содержится название тестируемого класса — «hoverIntent»).

Наверху HTML-страницы еще показывается небольшое горизонтальное меню, в котором выводится информация о прохождении тестов (в нашем случае — «passes: 3», то есть 3 теста из 5 пройдены с положительным результатом; «failures: 2», то есть 2 теста из 5 пройдены с отрицательным результатом; «duration: 0.02s», это время в секундах, ушедшее на прохождение всех 5 тестов (или только отобранного теста); «100%» в кружке — это процент пройденных тестов, не обязательно с положительным результатом). Два первых пункта в этом меню можно нажимать мышкой. Нажатие на пункт «passes» оставит на HTML-странице только результаты тестов, пройденных с положительным результатом. Нажатие на пункт «failures» оставит на HTML-странице только результаты тестов, пройденных с отрицательным результатом.

Как вообще пишутся автоматические тесты?

Об этом можно прочитать в подразделе 3.5 «Автоматическое тестирование c использованием фреймворка Mocha» первой части обсуждаемого учебника.

В принципе, заглянув в код файла test.js, разобраться несложно. В функции describe (из библиотеки «Mocha») первым параметром (это строка) задано название тестируемой сущности — название класса hoverIntent, оно отражается на тестовой HTML-странице. Вторым параметром задана функция с некоторыми настройками и пятью тестами.

Каждый из пяти автоматических тестов задан функцией it (из библиотеки «Mocha»). В этой функции первым параметром задана строка с названием теста (оно отражается на тестовой HTML-странице), вторым параметром — функция с кодом теста на языке JavaScript. Код каждого из пяти тестов заканчивается проверкой некоего утверждения. Результат такой проверки и будет являться результатом теста. Утверждение реализуется методами объекта assert (из библиотеки «Chai»).

С помощью функций before и after (обе из библиотеки «Mocha») задан код, который необходимо запустить перед нашей группой из пяти тестов и после нее. Тут в нашем случае выполняется подмена стандартного отсчета времени так, чтобы отсчет времени при запуске нашей группы из пяти тестов начинался с нуля. Например, на каждом шаге тестов мы можем измерять прошедшее время стандартным для языка JavaScript методом Date.now и этот метод выдаст количество прошедших миллисекунд не с 1 января 1970 года, как это должно быть по стандарту языка JavaScript, а с начала запуска нашей группы из пяти тестов. Такая подмена стандартного отсчета времени выполняется в данном случае с помощью метода sinon.useFakeTimers (из библиотеки «Sinon»). Эта подмена может быть очень полезна при тестировании.

С помощью функций beforeEach и afterEach (обе из библиотеки «Mocha») задан код, который будет запускаться перед и после каждого теста из пяти имеющихся. Из заданного там кода мы видим, что перед каждым тестом создается новый объект тестируемого класса hoverIntent, специально для этого теста. А после каждого теста запускается метод destroy этого объекта.

* * *

Названия тестов их авторы не потрудились перевести на русский. Но по мере рассмотрения каждого теста я буду пытаться перевести название теста на русский язык или хотя бы опишу смысл теста по-русски.

Почему три теста из пяти сразу проходятся с положительным результатом, если мы еще не вносили в код никаких изменений? Дело в том, что для прохождения 1-го, 3-го и 5-го тестов требуется, чтобы подсказка не была показана. А подсказка после загрузки тестовой HTML-страницы и так по умолчанию скрыта. Поэтому эти три теста и проходятся с положительными результатами. Но это может измениться, как только мы начнем дописывать класс hoverIntent.

1. Рассмотрим первый тест в подробностях. Его код:
mouse('mouseover', 10, 10);
assert.isFalse(isOver);

Функция mouse описана авторами задачи тут же, в файле test.js. В данном случае она генерирует событие мыши mouseover над HTML-элементом с идентификатором elem и координатами 10, 10 относительно левого верхнего угла области просмотра браузера.

Как мы видим из второй строчки этого кода, после вышеописанного события объект класса hoverIntent не должен показать подсказку, чтобы пройти тест с положительным результатом. Переменная isOver принимает значение true, если подсказка показана и false, если подсказка не показана. Утверждение assert.isFalse(isOver) будет верным (а, значит, тест будет пройден с положительным результатом), если переменная isOver после генерации вышеописанного события мыши примет значение false (подсказка не показана).

Если после генерации вышеописанного события мыши переменная isOver примет значение true (подсказка показана), то тест будет пройден с отрицательным результатом.

Тест называется «mouseover -> immediately no tooltip». Это можно перевести на русский язык как «сразу после события mouseover нет подсказки». Имеется в виду то, что в предыдущих задачах мы вешали функцию, показывающую подсказку, на событие мыши mouseover при заходе курсора мыши на целевой HTML-элемент. Умная подсказка должна работать по-другому: чтобы ее показать, нужно сначала проанализировать скорость движения курсора мыши. А это можно будет сделать только тогда, когда пользователь после захода курсором мыши на целевой HTML-элемент сделает хотя бы одно движение мышью, сгенерировав тем самым событие mousemove.

Дополнительный вывод. При работе с живым пользователем ситуации, описанной в этом тесте, не может произойти. Браузер может либо вообще не зарегистрировать никаких событий мыши (если провести мышью над целевым элементом очень быстро), либо, если зарегистрировано событие мыши mouseover, то обязательно будет зарегистрировано хотя бы одно событие mousemove и заключительное mouseout, когда курсор мыши покинет целевой HTML-элемент. См. про это мой пост с экспериментами. Получается, что в тестах могут проверяться ситуации, которые невозможны в работе с живыми пользователями. (С другой стороны, можно считать, что в этом тесте рассматривается только начало ситуации с живым пользователем, сразу после того, как пользователь навел курсор мыши на целевой HTML-элемент, а дальнейшие события отбрасываются.)

2. Код второго теста:
mouse('mouseover', 10, 10);
this.clock.tick(100);
assert.isTrue(isOver);

Напомню, что каждый новый тест проводится на новом, созданном специально для него, объекте класса hoverIntent. Объект этого класса, использовавшийся в предыдущем тесте, затёрт новым, только что созданным (новый объект записывается в ту же переменную, в которой хранился предыдущий объект). Таким образом, предыдущий объект будет уничтожен сборщиком мусора (про это было в подразделе 4.3 «Сборка мусора» первой части обсуждаемого учебника).

Метод this.clock.tick(100) (из библиотеки «Sinon») создает паузу в 100 миллисекунд.

Таким образом, в этом тесте сначала генерируется такое же событие мыши mouseover, как и в первом тесте, только тут после этого события еще создается пауза в 100 миллисекунд.

В такой ситуации, чтобы пройти данный тест с положительным результатом, тестируемый объект класса hoverIntent должен показать подсказку (об этом говорит утверждение в последней строке теста).

Название теста «mouseover -> pause shows tooltip» можно перевести на русский язык как «событие mouseover и пауза показывают подсказку».

Как это можно истолковать в рамках нашей задачи? Пауза означает то, что мышь не двигается, то есть скорость движения курсора мыши равна нулю, а это меньше, чем заданный в задаче показатель 0,1 пикселей в миллисекунду. Следовательно, в описанной ситуации для соответствия условиям задачи правильно будет показать подсказку.

3. Код третьего теста:
mouse('mouseover', 10, 10);
setTimeout(
  () => mouse('mouseout', 300, 300, { relatedTarget: document.body }),
  30
);
this.clock.tick(100);
assert.isFalse(isOver);

Ситуация, рассматриваемая в этом тесте: сначала генерируется такое же событие мыши mouseover, как и в предыдущих двух тестах. После этого в планировщик браузера записывается задание сгенерировать через 30 миллисекунд событие мыши mouseout в точке с координатами 300, 300 относительно левого верхнего угла области просмотра браузера (эта точка точно находится за пределами целевого HTML-элемента). Затем запускается пауза в 100 миллисекунд.

Чтобы пройти этот тест, тестируемый объект класса hoverIntent в описанной ситуации не должен показать подсказку.

Название этого теста «mouseover -> fast mouseout no tooltip» можно перевести на русский язык как «при событии mouseover и быстром последующем событии mouseout нет подсказки».

Почему здесь событие mouseout считается быстрым? Расстояние между точками с координатами 10, 10 и 300, 300 равно приблизительно 410 пикселям. Тогда скорость курсора мыши приблизительно равна 13,67 пикселей в миллисекунду (410 / 30), или 4,1 пикселей в секунду (410 / 100), если вспомнить, что по условиям задачи наименьший интервал между замерами — 100 миллисекунд. Это больше, чем заданные 0,1 пикселей в миллисекунду, а поэтому такая скорость считается быстрой. При быстром передвижении курсора мыши по условиям задачи умная подсказка не должна быть показана.

4. Код четвертого теста:
mouse('mouseover', 10, 10);
for(let i=10; i<200; i+= 10) {
  setTimeout(
    () => mouse('mousemove', i/5, 10),
    i
  );
}
this.clock.tick(200);
assert.isTrue(isOver);

В этом тесте имитируется заход курсора мыши на целевой HTML-элемент (первая строка теста), после чего в течение 200 миллисекунд имитируются движения курсора мыши, то есть генерируются события мыши mousemove. Всего событий мыши mousemove генерируется 19 штук с координатами от 2, 10 до 38, 10 с шагом по горизонтальной оси координат в 2 пикселя (координаты отмеряются относительно левого верхнего угла области просмотра браузера).

Другими словами, имитируется заход мыши на целевой HTML-элемент и медленное движение курсора мыши по горизонтали над целевым HTML-элементом (на последнем движении курсор мыши не выходит за пределы целевого HTML-элемента).

Почему движение курсора в данном случае считаем медленным? Как ранее было сказано, между замерами скорости движения курсора мыши по условиям задачи должно пройти не менее 100 миллисекунд. Точка отсчета — координаты захода курсора мыши на целевой HTML-элемент, то есть 10, 10. Следующий замер должен быть произведен через 100 миллисекунд. На этой временной отметке тест сгенерирует событие мыши mousemove с координатами 20, 10 (это десятый setTimeout из девятнадцати). Пройден путь в 10 пикселей за 100 миллисекунд, следовательно скорость курсора мыши составляет 0,1 пикселей в миллисекунду (10 / 100). По условиям задачи, если скорость курсора меньше или равна 0,1 пикселей в миллисекунду, то такая скорость считается медленной.

Таким образом, тестируемый объект класса hoverIntent должен для этого теста показать подсказку на временной отметке в 100 миллисекунд от начала теста, на десятом событии мыши mousemove. После этого мышь еще будет двигаться девять раз, но нам это уже будет неинтересно, потому что подсказка уже показана.

Название этого теста «mouseover -> slow move -> tooltips» можно перевести на русский язык как «при событии mouseover и последующем медленном движении курсора мыши появляется подсказка».

5. Код пятого теста:
mouse('mouseover', 10, 10);
for(let i=10; i<200; i+= 10) {
  setTimeout(
    () => mouse('mousemove', i, 10),
    i
  );
}
this.clock.tick(200);
assert.isFalse(isOver);

В этом тесте имитируется заход курсора мыши на целевой HTML-элемент (первая строка теста), после чего в течение 200 миллисекунд имитируются движения курсора мыши, точно так же, как и в предыдущем тесте. Тут движений курсора мыши тоже генерируется 19 штук, однако, разница состоит в координатах точек, в которых генерируются эти движения: от точки 10, 10 до точки 190, 10 с шагом по горизонтальной оси координат в 10 пикселей (координаты отмеряются относительно левого верхнего угла области просмотра браузера).

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

Почему тут движение курсора мыши считается быстрым? Точно так же, как и в предыдущем тесте, замер скорости движения курсора мыши следует производить через 100 миллисекунд после начала теста, а также через 200 миллисекунд после начала теста (то есть через 100 миллисекунд после предыдущего замера), если при первом замере подсказка не будет показана. Итак, через 100 миллисекунд после начала теста будет сгенерировано событие мыши mousemove в точке с координатами 100, 10. Точка отсчета — координаты точки захода на целевой HTML-элемент, то есть 10, 10. За 100 миллисекунд пройдено 90 пикселей, скорость курсора мыши — 0,9 пикселей в миллисекунду (90 / 100). Это больше заданной по условиям задачи скорости 0,1 пикселей в миллисекунду, а, следовательно, это быстрая скорость. Подсказку показывать не следует.

На временной отметке в 200 миллисекунд от начала теста производим второй замер. Предыдущая точка, в которой производился замер — 100, 10. На данной временной отметке тест не генерирует никаких событий мыши. Последнее, девятнадцатое, событие мыши mousemove было сгенерировано на временной отметке в 190 миллисекунд от начала теста в точке с координатами 190, 10. Возьмем эту точку в качестве текущей. Опять пройдено расстояние в 90 пикселей за 100 миллисекунд, скорость движения курсора мыши — 0,9 пикселей в миллисекунду. То есть подсказку на этой временной отметке тоже не показываем.

Название этого теста «mouseover -> fast move -> no tooltip» можно перевести на русский язык как «при событии mouseover и последующем быстром движении курсора мыши нет подсказки».

Продолжение тут.