Трюки с JavaScript

Трюки с JavaScript

Selenide Advent Calendar
День 24
24.12.19

Привет!

На дворе 24 декабря, католическое рождество. А это значит, что Advent Calendar подошёл к концу.

И напоследок мы поиграемся с JavaScript.

Как язык JavaScript, конечно, дно, но он даёт большие возможности при написании автотестов.
Он позволяет залезть в такие дыры, куда с обычным вебдрайвером и не снилось.

Приведу несколько примеров из реальных проектов.

Выбрать дату

Есть масса всевозможных элементов для выбора даты - т.н. “date picker”. И выбрать в них нужную дату - это вечная головная боль.

Если реализовывать такой метод в лоб:

@Test {
  setDateByName("recurrent.startDate", "16.01.2009");
}

Придётся сделать примерно следующие шаги:

  1. Тыкнуть иконку “календарик”
  2. Тыкнуть год
  3. Тыкнуть стрелку “месяц назад” (сколько раз?)
  4. Тыкнуть день
  5. Ой, фсё, сегодня 29 февраля. Тест упал.

Долго, сложно, ненадёжно.


А вот как этот метод можно реализовать с помощью JS:

void setDateByName(String name, String date) {
  executeJavaScript(
    String.format("$('[name=\"%s\"]').val('%s')", 
    name, date)
  );
}

Быстро и надёжно.

Возможно, вам смутит, что это не совсем “честный” способ. Но об этом мы поговорим в конце. Не переключайтесь.

Спрятать календарик

Допустим, календарик всё же открылся. Как его спрятать? Решение в лоб - тыкнуть крестик в углу. Опять же, это медленно и ненадёжно:

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

Идиотская ситуация.


А вот как этот метод можно реализовать с помощью JS:

executeJavaScript(
  "$('.datepicker').hide();"
);

Быстро и надёжно.

Перелистнуть перелистывалку

Представим себе страницу, на которой есть “перелистывалка” карт. Нужно выбрать нужную карту, двигая наманикюренным пальчиком.
Как сэмулировать это селениумом?
Можно, конечно. Всякие там Drag’n’Drop, Actions. Нажал, потянул, отпустил. Но всё это медленно и нестабильно.
Это легко может сломаться от малейших изменений дизайна, изменения размерна окна браузера, потери фокуса и т.д.

А вот как можно с помощью JS:

void selectAccount(String accountId) {
  executeJavaScript(
    String.format("$('[data-account-id=\"%s\"]').attr('data-card-account', 'true')", accountId)
  );
}

Это нетривиально. Пришлось поизучать код этой “перелистывалки” и понять, какой JS код она дёргает при перелистывании.
И дёрнуть из теста аналогичный JS код. Придётся поработать головой, но зато это быстро и надёжно.

Выбрать опцию в bootstrap select

Многие UI фреймворки заменяют стандартный <select> на какие-то свои самодельные супер-пупер красивые/динамичные/удобные элементы, сварганенные из нагромождения <div>, <span>, <li> и т.п. с кучей разных CSS-классов и стилей. Для автоматизатора это всегда боль.

Один из таких фреймворков - Bootstrap. И в нём тоже есть свой <select>. Мы долго пытались научиться выбирать из него значения: тыкали на один div, ждали появления следующего span, тыкали на него, искали нужный div с правильными атрибутами… Всё это работало долго и часто падало на дженкинсе. Причин падения так до конца и не удалось понять. Хотя мы честно пытались.

В итоге мы реализовали метод

@Test {
  selectBootstrap($(By.name("operationCode")), "11100");
}

с помощью JavaScript:

void selectBootstrap(WebElement select, String value) {
  executeJavaScript(
    "$(arguments[0]).val(arguments[1]).trigger('change')", 
     select, value);
}

Быстро и надёжно.

А с помощью метода execute это можно написать ещё красивее:

$(By.name("operationCode")).execute(selectBootstrap("11100"));

Слайдер

На странице слайдер. Можно таскать ползунок туда-сюда от 0 до 100.
Как это сделать в тесте?

@Test {
  setMaxYearlyFee(100);
}

Тут даже классический Drag’n’Drop не поможет, потому что нет целевого элемента, куда перетаскивать.
Снова Actions, снова нажал-потянул-отпустил (по координатам?), снова медленно и нестабильно.

А можно с помощью JS:

void setMaxYearlyFee(int value) {
  executeJavaScript(
    "$('#sld').data('slider').value[0] = arguments[0];" +
    "$('#sld').triggerHandler('slide');", 
    value
  );
}

Снова пришлось поковыряться в коде, чтобы узнать, какой JS код дёргает слайдер. Зато работает. Зато быстро и надёжно.

Заигнорить чёртов confirm

В Selenide есть удобные методы, чтобы нажать “Ok” или “Cancel” в модальном диалоге alert, prompt или confirm.
Но с этими модальными окошками периодически возникает проблемы. Иногда они почему-то не закрываются, и после этого вебдрайвер не может ничего сделать с браузером, в том числе снять скриншот. Подробнее об этом я рассказывал в видео Flaky tests.

И снова на помощь приходит JavaScript!

Вот так можно “типа нажать Ok” в любом confirm диалоге:

public void mockConfirm() {
  executeJavaScript(
    "window.confirm = function() {return true;};"
  );
}

Плагины Cordova

В одном проекте мы писали гибридное мобильное приложение с помощью фреймворка Cordova.
Он создаёт такое приложение, в котором очень маленькая нативная часть запускает браузер (т.н. WebView), а в нём уже открывает веб-приложение. А доступ к нативным функциями мобильника (контакты, телефон, геолокация и пр.) предоставляет в виде JavaScript-плагинов.

Это очень удобно для тестирования. Для запуска автотестов не нужны реальные мобильники, достаточно открыть урл в обычном браузере (можно ещё и в режиме эмуляции мобильника - вообще не отличишь).

Но вот как тестировать функционал, требующий доступа к нативным функциям мобильника?

И снова на помощь приходит JS. Мы можем эмулировать плагины Cordova, управляя из теста их поведением:

@Test {
  mockContactsAPI("+79110080075");
}

private void mockContactsAPI(String number) {
  executeJavaScript(
    "window.plugins = {" +
    "  contactNumberPicker: { " +
    "    pick: function(callback) {" +
    "      callback({" + 
    "        phoneNumber:\"" + number + "\""
    "  });}}}");
}

Выражение “управлять поведением” кажется диким? Тогда посмотрите видео Arrange, mazafaka! с конференции SeleniumCamp 2018.

А мы имеем функцию mockContactsAPI, которая позволяет “типа выбрать” нужный номер из “типа адресной книги” “типа телефона”.
Собственно, это единственный способ покрыть тестами все возможные случаи: и когда номер найден, и когда не найден, и когда это дубликат, и с 100500 китайскими символами и бог знает какие ещё. Быстро и надёжно.

Но это же не по-настоящему?

Я знаю, у многих из вас полыхает мысль: “Это же фейк, обман, это не по-настоящему! Тесты должны делать так же, как реальный пользователь - иначе есть риск, что тест не обнаружит реальную ошибку.”

Понимаю.

Но возражу.

Быстрые и надёжные тесты лучше, чем “реалистичные” (но медленные и нестабильные) тесты.

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

То какой к чёрту прок от ваших “настоящих” тестов?
Один вред.

  • Лучше пусть мои тесты будут быстрыми и стабильными.
  • И с лёгкостью проверять разные сложные случаи, которые “по-настоящему” повторить нереально.
  • А я спокойно буду жить с пониманием того, что целью автоматизации никогда и не было автоматизировать абсолютно всё.


Вы всё ещё хотите возразить:

И всё-таки моей душе спокойнее, когда всё по-настоящему.

Спешу вас огорчить.

  • Ваши “реалистичные” тесты на Selenium WebDriver по-любому не настоящие. Они работают не так, как реальный пользователь. Вебдрайвер формирует http-запрос на каждую вашу команду и даже - вот сюрприз! - запускает некую логику на JavaScript, чтобы определить видимость элементов и т.п.
  • О да, в некотором смысле вызов действия через JavaScript даже “реалистичнее”, чем через WebDriver. Это ближе к тому, что на самом деле делает браузер.
  • И даже ваши ручные тестировщики, прокликивающие ваши сценарии - не настоящие! Они работают не так, как реальный пользователь.

Живите теперь с этим. :)

Что теперь?

А всё. Это был последний пост рождественского календаря. Уффф!

Скажу честно: я всё это затеял, чтобы набить руку и приучиться много писать. Надеюсь, это поможет в обновлении сайта и написании документации. А там, глядишь, и до книги дойдёт. :)

Вот такие планы на следующий год.

С наступающим!


Андрей Солнцев

ru.selenide.org

24.12.19