Как мы тестировали drag&drop в HTML5
Так или иначе, все сталкивались с ситуациями, когда в банальной обстановке вдруг происходило что-то необычное. Примерно такой случай произошел с нами при тестировании нового приложения на проверенном сто раз окружении. Сюрпризом для нас стало использование некоторых возможностей HTML5 в работе front-end«а, а точнее невозможность стандартными средствами Selenium WebDriver автоматизировать тестирование drag&drop операций. Об этом опыте мы хотим рассказать.
Представьте проект, который технологически очень похож на предыдущий (на наш взгляд это дало небольшой негативный эффект с точки зрения правильного понимания и анализа появившейся проблемы), но версия Angular между проектами изменилась с 1.x на 5.x и добавилась библиотека Selenide для UI автотестов.
Разрабатываемое веб-приложение имело страницу с неким набором сущностей, которые можно было перемещать между собой. Каково же было наше удивление, когда попытка выполнить автотест проверки drag&drop функции средствами Selenide не увенчалась успехом. Казалось бы, что могло пойти не так? На предыдущем проекте на аналогичном тестовом окружении все работало отлично.
Первым делом проверили работу drag&drop функций Selenide и Selenium в текущем браузере на примере другого веб-приложения. Всё работает. Обновили версии, мало ли…
Решили проверить то ли мы тащим и туда ли. А ошибиться в выборе элементов при использовании Angular довольно легко. Сели с front-end разработчиком и разобрались, что drag и drop элементы выбраны верно.
В общем, тестовое окружение исправное, тестовые методы написаны верно, drag&drop «руками» работает, однако автотест не работает. И причин для этого на первый взгляд нет.
Наконец мы смирились с фактом проблемы и пошли искать решение в интернете. Каково же было наше удивление, когда мы нашли открытую issue Chrome WebDriver #3604 от 04.03.2016. Только задумайтесь, с весны 2016 года официально существует проблема с неработающим drag&drop в Chrome WebDriver, не говоря уже о других браузерах. Нет, он конечно работает, но только не при использовании HTML5. А как выяснилось в процессе анализа проблемы, наше приложение использовало реализацию drag&drop средствами HTML5.
Каковы же варианты реализации drag&drop для тестирования в условиях HTML5? На просторах интернета было найдено два решения:
- Использовать Java библиотеку awt.Robot (или какой-то сторонний кликер);
- Использовать JavaScript.
Вероятно, мы слегка заработались или закопались в проблеме, но сразу оговорюсь, что первое выбранное решение нам не подошло :)Что можно сказать о реализации на Robot:
- Перехватываем мышь, эмулируя полноценные действия пользователя;
- Используем Selenium для определения координат элементов;
- Так как используются элементы Selenium, то не потребуется изменять локаторы. Мы на проекте стараемся использовать xpath;
- Написан на Java, синтаксис интуитивно понятен, хорошая документация.
А вот про реализацию на JavaScript пришло на ум нечто такое:
- Все происходит на JavaScript внутри браузера (действия скрыты от глаз тестировщика и эти действия вмешиваются в код);
- Из js-библиотек для тестирования drag&drop в интернете была найдена одна, исходники которой найти было не так просто;
- Найденную библиотеку придется допиливать напильником под свои нужды, так как она реализует только чистый drag&drop. А нам, например, было необходимо drag → move → hold → drop;
- Реализована библиотека в виде дополнения JQuery, а следовательно потребуется разбираться в структуре jQuery;
- Придется приводить локаторы к css (jquery не работает с xpath);
- Невозможно использовать поиск элементов Selenium, придется склеивать локаторы «ручками».
На первый взгляд первое решение было куда удобнее и было опробовано.
//Setup robot
Robot robot = new Robot();
robot.setAutoDelay(50);
//Fullscreen page so selenium coordinates work
robot.keyPress(KeyEvent.VK_F11);
Thread.sleep(2000);
//Get size of elements
Dimension fromSize = dragFrom.getSize();
Dimension toSize = dragTo.getSize();
//Get centre distance
int xCentreFrom = fromSize.width / 2;
int yCentreFrom = fromSize.height / 2;
int xCentreTo = toSize.width / 2;
int yCentreTo = toSize.height / 2;
//Get x and y of WebElement to drag to
Point toLocation = dragTo.getLocation();
Point fromLocation = dragFrom.getLocation();
//Make Mouse coordinate centre of element
toLocation.x += xOffset + xCentreTo;
toLocation.y += yCentreTo;
fromLocation.x += xCentreFrom;
fromLocation.y += yCentreFrom;
//Move mouse to drag from location
robot.mouseMove(fromLocation.x, fromLocation.y);
//Click and drag
robot.mousePress(InputEvent.BUTTON1_MASK);
//Move to final position
robot.mouseMove(toLocation.x, toLocation.y);
//Drop
robot.mouseRelease(InputEvent.BUTTON1_MASK);
В общем-то решение рабочее… Однако в процессе его проработки стали понятны его проблемные места.
- Движение мыши или сворачивание браузера во время выполнения тестов приводит к вмешательству в ход тестов и их падению;
- Невозможен параллельный запуск тестов средствами JUnit/TestNG. Разве что распараллелить через отдельные таски в CI.
- Невозможно управлять мышью на удаленной машине через Selenium Grid/Selenoid;
- В случае падения браузера Robot может запросто что-то нажать/перетащить на рабочем столе или в другом открытом приложении.
В итоге все же JavaScript реализация…
Сразу хочется сказать, что проблему использования xpath локаторов удалось решить использованием JQuery-плагина jquery.xpath.js.
А основным инструментом для js управления операциями drag&drop стала библиотека drag_and_drop_helper.js (исходник тут). Разбирать ее работу смысла особого нет, а вот про то, как мы ее дорабатывали чуть позже.
Теперь непосредственно о реализации в тестах. В Selenide все просто. Перед началом использования drag&drop требуется загрузить используемые JS библиотеки:
StringBuilder sb = new StringBuilder();
sb.append(readFile("jquery-3.3.1.min.js"));
sb.append(readFile("jquery.xpath.min.js"));
sb.append(readFile("drag_and_drop_helper.js"));
executeJavaScript(sb.toString());
Естественно jQuery необходимо загружать в том случае, если ее еще нет в приложении.
В исходной версии библиотеки достаточно прописать следующее:
executeJavaScript("$('" + source + "') .simulateDragDrop({ dropTarget: '" + target + "'});");
source и target — это css-локаторы drag и drop элементов.
Как оговаривалось выше, мы в проекте чаще используем xpath-локаторы, поэтому после небольшой доработки библиотека стала их принимать:
executeJavaScript("$(document).xpath('" + source + "').simulateDragDrop({ dropTarget: '" + target + "'});");
Теперь, собственно, о библиотеке drag_and_drop_helper.js. В блоке кода simulateEvent есть куски, отвечающие за определенные события мыши. Список возможных событий drag&drop операций в HTML5 приводить смысла нет, эту информацию легко найти.
Для тестирования нам требовалось реализовать функцию, которая перемещает элемент и удерживает мышь на целевом элементе. А этого как в исходной библиотеке не предусмотрено.
По аналогии добавили событие dragenter в библиотеку (между dragstart и drop).
/*Simulating dragenter*/
type = 'dragenter';
var dragenterEvent = this.createEvent(type, {});
dragenterEvent.dataTransfer = event.dataTransfer;
this.dispatchEvent($(options.dropTarget)[0], type, dragenterEvent);
Однако, этого не достаточно. Ведь событие удержания будет мгновенно закончено. Ставить фиксированную паузу между dragEnter и drop событиями показалось не самым удобным вариантом. Ведь изначально неизвестно, сколько требуется времени приложению на обработку того или иного события, неизвестно число и время проверок в тестах. Задержка между этими событиями должна быть как минимум управляема. Вместо этого мы решили разбить тестирование drag&drop на этапы и не делать эмуляцию полного набора событий мыши, то есть добавить возможность управлять перечнем задействованных событий через параметр.
И вроде бы все хорошо, новых недостатков не проявилось, да и некоторые старые более не являются таковыми, а главное поставленные задачи выполняются. Казалось бы, все идеально. Однако современные средства разработки закладывают обработку далеко не двух событий и используют различные параметры перемещаемого элемента. Допустим, у нас данное решение при выполнении drag&drop вызывает ошибки dragStartListener. Но так как оно ничего не ломает, то мы мы ничего больше и не стали менять. Однако в каком-то другом приложении вероятно придется допиливать и этот момент.
Хотим подвести итог вышесказанному. Удивительно, но факт! HTML5 вышел в далеком 2013 году, браузеры поддерживают его уже тоже не первый год, разрабатываются приложения заточенные под него, а вот webDriver, увы, до сих пор не умеет использовать его возможности. И тестирование операций drag&drop приходится реализовывать сторонними средствами, усложнять архитектуру и идти на всякие ухищрения. Да, такие средства есть и «танцы с бубном» делают нас только сильнее, но хочется все же иметь рабочее решение из коробки.
По нашему опыту можем сказать, что подобные проблемы на сегодняшний день встречаются не так часто, хотя drag&drop применяется повсеместно. Вероятно, дело в выборе технологий разработки веб-приложений. Однако, процент приложений с использованием HTML5 неуклонно растет, развиваются фреймворки, и было бы замечательно, если разработчики браузеров и драйверов к ним тоже не отставали.
P.S. Ну и напоследок немного лирики. Хочется посоветовать всем по возможности не принимать во внимание банальность ситуации или близость тестового окружения к какому-то шаблону при анализе проблем. Это может привести к неправильным выводам или потере времени.