Как мы тестировали drag&drop в HTML5

Так или иначе, все сталкивались с ситуациями, когда в банальной обстановке вдруг происходило что-то необычное. Примерно такой случай произошел с нами при тестировании нового приложения на проверенном сто раз окружении. Сюрпризом для нас стало использование некоторых возможностей HTML5 в работе front-end«а, а точнее невозможность стандартными средствами Selenium WebDriver автоматизировать тестирование drag&drop операций. Об этом опыте мы хотим рассказать.
vp3h0pprz35p2ycsyhynk5kjp0c.png
Представьте проект, который технологически очень похож на предыдущий (на наш взгляд это дало небольшой негативный эффект с точки зрения правильного понимания и анализа появившейся проблемы), но версия 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. Ну и напоследок немного лирики. Хочется посоветовать всем по возможности не принимать во внимание банальность ситуации или близость тестового окружения к какому-то шаблону при анализе проблем. Это может привести к неправильным выводам или потере времени.

© Habrahabr.ru