Masquerade

Masquerade is an end-to-end testing library designed to automate UI testing for Jmix applications. The library helps you use the Page Object pattern for all parts of Jmix UI, including views, components, dialogs, notifications, and composites. It is based on Selenium WebDriver and Selenide.

Masquerade is for applications that are built with Jmix 2.6 or newer.

Install Masquerade

To install Masquerade, follow the steps below:

  1. Ensure that your project uses Jmix 2.6 or newer. See the Upgrading Project section for how to upgrade using Studio.

  2. Specify the dependency in the project build.gradle file as testImplementation:

    build.gradle
    testImplementation 'io.jmix.masquerade:jmix-masquerade'

Using Selenide

Masquerade is based on Selenide, so you can use any of its methods. Consider an example, typical of test automation, that logs in to a Jmix application:

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();
    }
}

The standard Selenide.$ method takes a selector as an argument, such as byId. As a result, a SelenideElement object will be returned, with which further actions can be performed.

Please see Selenide API to learn about more methods.

Create Test with Masquerade

Masquerade emphasizes the use of wrappers for various Jmix components, allowing for better organization and management of tests. Let’s create a test that logs in to a Jmix application using Masquerade. This will involve two steps:

Step 1. Create a View Wrapper

A basic Jmix project includes a login view from the start. Create a view wrapper to represent this view and its components under test:

  • In the src/test/java directory, within the com.company.testproject package create a new package named view. Then, create a Java class named LoginView:

    TestProject/
    └── src/
        ├── main/
        └── test/
            └── java/
                └── com.company.testproject
                    └── test_support
                        └── view
                           └── LoginView.java
  • Add component wrappers along with get methods for each component needed for the test:

    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;
        }
    }

Step 2. Create a Test Class

The test class will invoke methods from the wrapper to go through the login scenario. Follow the steps below to create it:

  • In the src/test/java directory, within the com.company.testproject package create a new package named ui-autotest. Then create a Java class named LoginUiTest:

    TestProject/
    └── src/
        ├── main/
        └── test/
            └── java/
                ├── com.company.testproject
                │   └── test_support
                │       └── view
                │          └── LoginView.java
                └────── ui_autotest
                        └── LoginUiTest.java
  • Define a sequence of actions for your test scenario:

    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 Open the login page using the standard Selenide method.
    2 Use the Masquerade.$j method to select the view wrapper and call its methods.

Jmix Test IDs

Masquerade may assist you in generating a special j-test-id (Jmix test ID) attribute for each component created with the UiComponents factory. Such ids simplify identifying elements on the page. To enable this feature, set jmix.ui.ui-test-mode to true:

application.properties
jmix.ui.ui-test-mode = true
ID generation can impact the performance. It is recommended to use this property only for a test application profile.

The default test id value will match the id already given to a component:

masquerade j test id

If a component had no id, its test id will be generated based on its data binding, action, or text attribute.

To work with an element directly by its j-test-id, use the Masquerade.$j method:

$j("myButton").click();

Wrappers

Wrappers are classes that represent different parts of Jmix UI and encapsulate interactions with them. They help you separate test scenarios from the complexities of the UI under test. There are five types of wrappers.

View Wrapper

A view wrapper encapsulates a view, providing a simplified and consistent interface to interact with its components. A view wrapper does not have to include all components of the corresponding view – add just those that are needed for tests.

Consider a simple view wrapper example:

@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 Annotate with io.jmix.masquerade.TestView. The id value must match the view ID specified through @ViewController:
@Route(value = "my-test-view", layout = MainView.class)
@ViewController(id = "MyView")
@ViewDescriptor(path = "my-test-view.xml")
public class MyView extends StandardView {

}

If you don’t specify an ID, then the simple class name for this view will be used as id.

2 Extend the io.jmix.masquerade.sys.View class with typing on itself. This is necessary to provide fluent API for writing tests.

Component Wrapper

Component wrappers encapsulate components within view wrappers. To designate a field as a component wrapper, use the @TestComponent or @FindBy annotations. This is illustrated in the following example:

@TestComponent
private EntityComboBox entityComboBox;

@TestComponent(path = "myButton")
private Button button;

@FindBy(xpath = "//vaadin-text-area[@class='my-text-area']")
private TextArea textArea;
  • @TestComponent indicates that the current field is a component wrapper. If the path value is not specified, the default value is assumed to be the value of the j-test-id attribute for the corresponding web element.

  • @FindBy specifies a selector to be used for this field.

Find the list of all available component wrappers in the io.jmix.masquerade.component package.

Component wrappers offer various methods for interacting with components. For instance, you can open an overlay or even a dialog window for an 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]"));
}

Component wrappers also offer specific conditions for checking a component’s state. You can find the list of all available conditions in the io.jmix.masquerade.condition package.

DialogWindows Wrapper

DialogWindow wrappers are similar to view wrappers, but extend io.jmix.masquerade.sys.DialogWindow. This class offers specific actions for dialogs, such as closing them and checking the header. Consider the following example:

@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;
    }
}

Notification Wrapper

Notification wrapper is a special type of component wrapper. Consider the following example working with a notification:

@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);
}

If you have multiple notifications open, you can select the one you want by xpath:

$j(Notification.class, xpath("{xpath-to-notification}"));

Composite Wrapper

Composite wrapper encapsulates a part of a view, also referred to as fragment. The way you access a fragment will vary depending on whether it is present in a view wrapper or not.

Assume we have a view composed of two fragments, but only one is added to the corresponding view wrapper:

@TestView
public class FragmentsView extends View<FragmentsView> {

    @TestComponent
    private TestFragment1 testFragment1Root;

    public TestFragment1 getTestFragment1() {
        return testFragment1Root;
    }
}

Composite wrappers are as follows:

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 Composite wrappers must extend io.jmix.masquerade.sys.Composite.
2 Composite wrappers can be optionally annotated with @TestComponent to provide a path value for use in Masquerade.$j.

To access the fragment in the view wrapper, chain it as you would with components:

FragmentsView fragmentsView = openFragmentsView();

fragmentsView.getTestFragment1()
        .getTestField()
        .shouldHave(value(""))
        .setValue("Fragment_1")
        .shouldHave(value("Fragment_1"));

To test the second fragment, not present in the view wrapper, select it using Masquerade.$j:

FragmentsView fragmentsView = openFragmentsView();

$j(TestFragment2.class)
        .getTestField()
        .shouldHave(value(""))
        .setValue("Fragment_2")
        .shouldHave(value("Fragment_2"));