Сквозные тесты UI
Сквозные (end-to-end) тесты пользовательского интерфейса имитируют реальные действия пользователя в веб-браузере, проверяя всю логику приложения. Они быстро запускаются и легко воспроизводимы, что экономит время по сравнению с ручным тестированием.
В Jmix вы можете использовать Masquerade для автоматизации UI-тестирования ваших приложений. Эта библиотека для сквозного тестирования помогает применять паттерн Page Object ко всем частям пользовательского интерфейса Jmix, включая представления, компоненты, диалоги, уведомления и композиты. Библиотека основана на Selenium WebDriver и Selenide.
| Masquerade предназначена для приложений, созданных с использованием Jmix 2.6 или новее. | 
Установка Masquerade
Чтобы установить Masquerade, выполните следующие шаги:
- 
Убедитесь, что в проекте используется Jmix 2.6 или новее. Посетите раздел Апгрейд Проекта, чтобы узнать как обновить проект с помощью Studio. 
- 
Укажите зависимость в файле build.gradleкакtestImplementation:build.gradletestImplementation 'io.jmix.masquerade:jmix-masquerade' 
Использование Selenide
Masquerade основан на Selenide, что позволяет использовать любые его методы. Рассмотрим пример, типичный для автоматизации тестирования, в котором выполняется вход в приложение Jmix:
public class SelenideTest {
    @Test
    void selenideLogin() {
        Selenide.open("/");
        $(byId("vaadinLoginUsername")).shouldHave(value("admin"));
        $(byChained(byId("vaadinLoginUsername"), byTagName("input")))
                .setValue("")
                .setValue("admin");
        $(byId("vaadinLoginPassword")).shouldHave(value("admin"));
        $(byChained(byId("vaadinLoginPassword"), byTagName("input")))
                .setValue("")
                .setValue("admin");
        $(byCssSelector("[slot='submit']")).click();
    }
}Стандартный метод Selenide.$ принимает селектор в качестве аргумента, например, byId. В результате будет возвращен объект SelenideElement, с которым можно выполнять дальнейшие действия.
| Ознакомьтесь с Selenide API, чтобы узнать о других методах. | 
Создание Теста в Masquerade
Masquerade предлагает использование оберток для различных компонентов Jmix, что помогает лучше организовать и управлять тестами. Давайте создадим тест, который выполняет вход в приложение Jmix с использованием Masquerade. Для этого выполним два шага:
Шаг 1. Создание Обертки Экрана
Базовый проект Jmix содержит экран логина по умолчанию. Для взаимодействия с этим экраном и его компонентами во время тестирования создадим для него обертку экрана:
- 
В каталоге src/test/java, в пакетеcom.company.testproject, создайте пакетview. Внутри создайте Java класс с именемLoginView:TestProject/ └── src/ ├── main/ └── test/ └── java/ └── com.company.testproject └── test_support └── view └── LoginView.java
- 
Добавьте обертки компонентов и методы get для каждого компонента, необходимого для теста: LoginView.java@TestView public class LoginView extends View<LoginView> { @FindBy(css = "[slot='submit']") private Button button; @FindBy(id = "vaadinLoginUsername") private TextField username; @FindBy(id = "vaadinLoginPassword") private PasswordField password; public Button getButton() { return button; } public TextField getUsernameField() { return username; } public PasswordField getPasswordField() { return password; } }
Шаг 2. Создание тестового класса
Тестовый класс будет вызывать методы из обертки для прохождения сценария по входу в приложение. Выполните следующие шаги для его создания:
- 
В каталоге src/test/java, в пакетеcom.company.testproject, создайте новый пакетui-autotest. Внутри создайте Java класс с именемLoginUiTest:TestProject/ └── src/ ├── main/ └── test/ └── java/ ├── com.company.testproject │ └── test_support │ └── view │ └── LoginView.java └────── ui_autotest └── LoginUiTest.java
- 
Определите последовательность действий для вашего тестового сценария: public class LoginUiTest { @Test public void loginAsAdmin() { Selenide.open("/"); (1) LoginView loginView = $j(LoginView.class); (2) loginView.getUsernameField() .shouldHave(value("admin")) .setValue("") .setValue("admin"); loginView.getPasswordField() .shouldHave(value("admin")) .setValue("") .setValue("admin"); loginView.getButton() .shouldHave(text("Log in")) .click(); } }1 Переход на страницу логина выполняется с помощью стандартного метода Selenide. 2 Используйте метод Masquerade.$j, чтобы выбрать класс обертки и вызывать его методы.
Генерация Jmix Test IDs
Masquerade может помочь вам с генерацией специального атрибута j-test-id (Jmix test ID) для каждого компонента, созданного с помощью фабрики UiComponents. Эти идентификаторы упрощают поиск элементов на странице. Чтобы включить эту функцию, установите значение jmix.ui.ui-test-mode в true:
jmix.ui.ui-test-mode = true| Генерация идентификаторов может повлиять на производительность. Рекомендуется использовать это свойство только для тестового профиля приложения. | 
Значение тестовых идентификаторов по умолчанию соответствует значение заданному в аттрибуте id для компонента:
 
Если id компонента не был задан, тестовый идентификатор j-test-id будет сгенерирован на основе привязки данных, атрибута action или text.
Чтобы работать с элементом напрямую по его j-test-id, используйте метод Masquerade.$j:
$j("myButton").click();Обертки
Обертки это классы, которые представляют различные части пользовательского интерфейса Jmix инкапсулируя взаимодействия с ними. Они помогают отделить тестовые сценарии от деталей взаимодействия с пользовательским интерфейсом. Всего существует пять типов оберток.
Обертка Экрана
Обертка экрана инкапсулирует экран, создавая упрощенный интерфейс для взаимодействия с его компонентами во время теста. Добавлять все компоненты в обертку не обязательно — можно добавить только те, которые необходимы для тестов.
Рассмотрим простой пример обертки экрана:
@TestView(id = "MyView") (1)
public class MyView extends View<MyView> { (2)
    @TestComponent
    private EntityComboBox entityComboBox;
    @TestComponent(path = "myButton")
    private Button button;
    @FindBy(xpath = "//vaadin-text-area[@class='my-text-area']")
    private TextArea textArea;
    public EntityComboBox getEntityComboBox() {
        return entityComboBox;
    }
    public Button getButton() {
        return button;
    }
}| 1 | Обертка экрана должна содержать аннотацию io.jmix.masquerade.TestView. Значениеidв аннотации должно соответствовать идентификатору представления, указанному через@ViewController:Если вы не укажете  | 
| 2 | Класс обертки наследуется от io.jmix.masquerade.sys.Viewи передает собственный классMyViewв качестве параметра типа. Это необходимо для предоставления API для написания тестов. | 
Обертка Компонента
Обертки компонентов инкапсулируют компоненты внутри оберток экрана. Чтобы указать что поле класса является оберткой компонента, используйте аннотации @TestComponent или @FindBy. Это показано в следующем примере:
@TestComponent
private EntityComboBox entityComboBox;
@TestComponent(path = "myButton")
private Button button;
@FindBy(xpath = "//vaadin-text-area[@class='my-text-area']")
private TextArea textArea;- 
@TestComponentуказывает, что поле является оберткой компонента. Если значение path не указано, предполагается, что по умолчанию используется значение атрибута j-test-id соответствующего веб-элемента.
- 
@FindByявно задает селектор, который будет использован при идентификации компонента.
| Список всех доступных оберток компонентов доступен в пакете io.jmix.masquerade.component. | 
Обертки компонентов предлагают различные методы для взаимодействия с компонентами. Например, вы можете открыть оверлей или даже диалоговое окно для EntityCombobox:
@Test
public void testEntityComboBox() {
    EntityComboBox entityComboBox = openMyView().getEntityComboBox();
    entityComboBox.shouldHave(label("EntityComboBox"))
            .setValue("[admin]")
            .shouldHave(value("[admin]"))
            .clickItemsOverlay()
            .shouldHave(visibleItems("[admin]", "[test]", "[test1]"))
            .shouldHave(visibleItemsCount(3))
            .shouldHave(visibleItemsContains("[test]"));
    sleep(3000);
    entityComboBox.getItemsOverlay()
            .select("[test]");
    sleep(3000);
    entityComboBox.shouldHave(value("[test]"))
            .triggerActionWithView(UserListDialog.class, HasActions.LOOKUP)
            .selectAdmin();
    sleep(3000);
    entityComboBox.shouldHave(value("[admin]"));
}Обертки компонентов также предлагают специфические условия проверки состояния компонентов. Вы можете найти список всех доступных условий в пакете io.jmix.masquerade.condition.
Обертка Диалоговых Окон
Обертки для диалоговых окон аналогичны оберткам экрана, но наследуются от класса io.jmix.masquerade.sys.DialogWindow. Этот класс предлагает специфические действия, такие как их закрытие окна и проверка заголовка. Для иллюстрации рассмотрим пример:
@TestView(id = "User.list")
public class UserListDialog extends DialogWindow<UserListDialog> {
    public UserListDialog selectAdmin() {
        $(By.xpath("//*[@id=\"usersDataGrid\"]/vaadin-grid-cell-content[22]"))
                .click();
        $(byChained(getBy(), byUiTestId("selectButton")))
                .shouldBe(VISIBLE)
                .shouldBe(ENABLED)
                .click();
        return this;
    }
}Обертка Уведомлений
Обертка для уведомления представляет собой специальный тип обертки компонента. Рассмотрим пример работы с уведомлением:
@Test
public void notificationTest() {
    MainView mainView = loginAsAdmin();
    UserListView userListView = mainView.openItem(UserListView.class,
            "applicationListItem", "user.listListItem");
    userListView.showUsername();
    Notification notification = $j(Notification.class);
    notification
            .shouldBe(VISIBLE)
            .shouldHave(notificationPosition(Notification.Position.BOTTOM_END))
            .shouldHave(notificationTheme(Notification.Theme.SUCCESS))
            .shouldHave(notificationTitle("Username:"))
            .should(notificationTitleContains("name:"))
            .shouldHave(notificationMessage("test"))
            .should(notificationMessageContains("te"));
    sleep(3000);
    notification.shouldNotBe(EXIST);
}Если на экране одновремнно открыто несколько уведомлений, вы можете выбрать нужное с помощью xpath:
$j(Notification.class, xpath("{xpath-to-notification}"));Обертка Композита
Обертка композита инкапсулирует некоторую часть экрана, также называемую фрагментом. Способ доступа к фрагменту будет различаться в зависимости от того, добавлен ли он в обертку экрана или нет.
Предположим, у нас есть экран, состоящий из двух фрагментов, но только один из них добавлен в соответствующую обертку экрана:
@TestView
public class FragmentsView extends View<FragmentsView> {
    @TestComponent
    private TestFragment1 testFragment1Root;
    public TestFragment1 getTestFragment1() {
        return testFragment1Root;
    }
}Обертки для упомянутых фрагментов выглядят следующим образом:
public class TestFragment1 extends Composite<TestFragment1> { (1)
    @TestComponent
    private TextField testFragment1TextField;
    public TextField getTestField() {
        return testFragment1TextField;
    }
}@TestComponent(path = {"FragmentsView", "testFragment2Root"}) (2)
public class TestFragment2 extends Composite<TestFragment2> {
    @TestComponent
    private TextField testFragment2TextField;
    public TextField getTestField() {
        return testFragment2TextField;
    }
}| 1 | Обертка композита наследуется от io.jmix.masquerade.sys.Composite. | 
| 2 | Обертки композита могут быть дополнительно аннотированы с помощью @TestComponent, чтобы предоставить значение пути для использования в Masquerade.$j. | 
Фрагмент из обертки экрана можно получить через цепочку методов, аналогично тому как это делается для компонентов:
FragmentsView fragmentsView = openFragmentsView();
fragmentsView.getTestFragment1()
        .getTestField()
        .shouldHave(value(""))
        .setValue("Fragment_1")
        .shouldHave(value("Fragment_1"));В случае если фрагмент не добавлен в обертку экрана, получить к нему доступ можно передав его класс в метод Masquerade.$j:
FragmentsView fragmentsView = openFragmentsView();
$j(TestFragment2.class)
        .getTestField()
        .shouldHave(value(""))
        .setValue("Fragment_2")
        .shouldHave(value("Fragment_2"));