GithubHelp home page GithubHelp logo

masquerade's Introduction

masquerade

Cuba Platform UI testing library.

license Build Status

Overview

The library provides an ability to create UI tests for CUBA-based applications. It can help you to write better tests.

It is based on:

  • Java
  • Selenide
  • Selenium

Getting started

Creating test project

Create a simple Java project in IntelliJ IDEA. The project should have the following structure:

+ src
  + main 
    + java
  + test
    + java
      + com/company/demo
+ build.gradle
+ settings.gradle  

Here’s the complete build.gradle file:

apply plugin: 'java'

group = 'com.company.demo'
version = '0.1'

sourceCompatibility = 1.8

repositories {
    mavenCentral()    
    maven {
        url "https://dl.bintray.com/cuba-platform/main"
    }    
}

dependencies {
    testCompile('junit:junit:4.12')
    
     //the library for the UI testing
    testCompile('com.haulmont.masquerade:masquerade-web:<check the latest version>')
    
    //the library provides an ability to access web-services, JMX and etc.
    testCompile('com.haulmont.masquerade:masquerade-connector:<check the latest version>')
    
    // enable logging
    testCompile('org.slf4j:slf4j-simple:1.7.25')
}

Find the latest version number here: https://bintray.com/cuba-platform/main/masquerade

Creating a test

In the src/test/java folder create a new package in the com.company.demo and name it composite. Create a new Java class in this package and name it LoginWindow. This class should be inherited from the Composite\<T> where T is the name of your class. This class will be used as a helper class, usually it declares UI components of an application screen / frame / panel that is shown in a web page. Also, all test methods can be declared here.

All class attributes should be marked with the @Wire annotation. This annotation has optional path element which allows userService to define the path to the component using the cuba-id parameter. If the component does not have the cuba-id parameter, you can use the @FindBy annotation instead. This annotation has a list of optional parameters, like name, className, id and so on, which helps to identify the component.

The type of the attribute in the class corresponds to the type of the screen component. If the component has a type which is not defined in the library, use the Untyped type.

The name of the attribute corresponds to the cuba-id attribute of a DOM element that corresponds to the UI component.

import com.haulmont.masquerade.Wire;
import com.haulmont.masquerade.base.Composite;
import com.haulmont.masquerade.components.Button;
import com.haulmont.masquerade.components.CheckBox;
import com.haulmont.masquerade.components.Label;
import com.haulmont.masquerade.components.LookupField;
import com.haulmont.masquerade.components.PasswordField;
import com.haulmont.masquerade.components.TextField;
import org.openqa.selenium.support.FindBy;

public class LoginWindow extends Composite<LoginWindow> {

    @Wire
    public TextField loginField;

    @Wire
    public PasswordField passwordField;

    @Wire(path = "rememberMeCheckBox")
    public CheckBox rememberMeCheckBox;

    @Wire(path = {"loginFormLayout", "loginButton"})
    public Button loginSubmitButton;

    @Wire
    public LookupField localesSelect;

    @Wire
    public Label welcomeLabel;

    @FindBy(className = "c-login-caption")
    public Label welcomeLabelTest;
}

Create a Java class in the com.company.demo package in the src/test/java folder. Name it LoginWindowTest.

Create a new method and add @Test annotation to it. The @Test annotation tells JUnit that the public void method can be run as a test case.

You can use all JUnit annotations to improve the tests. Also it is possible to use a set of assertion methods provided by JUnit.

import com.company.demo.composite.LoginWindow;
import com.haulmont.masquerade.components.Untyped;
import org.junit.Test;

import static com.codeborne.selenide.Selenide.close;
import static com.codeborne.selenide.Selenide.open;
import static com.haulmont.masquerade.Components.$c;
import static com.haulmont.masquerade.Components.wire;
import static com.haulmont.masquerade.Conditions.*;

public class LoginWindowTest {

    @Test
    public void loginTest() {
        // open URL of an application
        open("http://localhost:8080/app");

        // obtain UI object
        LoginWindow loginWindow = $c(LoginWindow.class);

        loginWindow.loginField
                .shouldBe(EDITABLE)
                .shouldBe(ENABLED);

        // setting values
        loginWindow.loginField.setValue("admin");
        loginWindow.passwordField.setValue("admin");
        loginWindow.rememberMeCheckBox.setChecked(true);

        // fluent asserts
        loginWindow.welcomeLabel
                .shouldBe(VISIBLE);

        loginWindow.loginSubmitButton
                .shouldBe(VISIBLE)
                .shouldBe(ENABLED)
                .shouldHave(caption("Submit"));

        loginWindow.rememberMeCheckBox
                .shouldBe(VISIBLE)
                .shouldBe(CHECKED);

        // get values from Component
        String caption = loginWindow.loginSubmitButton.getCaption();
        boolean enabled = loginWindow.loginSubmitButton.is(ENABLED);

        Untyped loginFormLayout = wire(Untyped.class, "loginFormLayout");
        loginFormLayout.shouldBe(VISIBLE);

        loginWindow.loginSubmitButton.click();
        
        close();
    }
}

The open() method is a standard Selenide method. It opens a browser window with the given URL. The second line creates an instance of the masquerade Component and binds it to the UI component (LoginWindow) on the screen including all the annotated fields inside of the LoginWindow class. After that, you can access the screen components as class attributes. You can check the attributes visibility, get captions, set values, click the buttons and so on.

Tips & Tricks

Here are some useful tips on how to work with the library.

How to work with elements

The library has a special method $c to define any element on the screen. This method has three implementations:

  • The first implementation gets the element by its class:

    $c(Class<T> clazz)

  • The second implementation gets the element by its class and the path:

    $c(Class<T> clazz, String... path)

  • The third implementation gets the element by its class and by selector:

    $c(Class<T> clazz, By by)

For example, we can click the button on the screen:

import static com.haulmont.masquerade.Components.$c;

$c(Button, 'logoutButton').click();

How to check the state of an element

Selenide allows you to check some conditions.

To check if the element is enabled, visible or checked, use the shouldBe element. For example:

loginButton
   .shouldBe(VISIBLE)
   .shouldBe(ENABLED);

To check if the element has some properties, use the shouldHave element. For example:

welcomeLabel.shouldHave(Conditions.value('Welcome to CUBA!'));

How to work with the Selenide elements

If the component does not have the cuba-id parameter, you can use the @FindBy annotation. This annotation has a list of optional parameters, like name, className, id and so on, which helps to identify the component.

@FindBy(className = "c-login-caption")
public Label welcomeLabelTest;

Also, using this annotation, you can define SelenideElement type for the attribute instead of the types provides by masquerade. After that, you can use all test methods provided by Selenide. The name of the attribute can be any.

import com.codeborne.selenide.SelenideElement;

@FindBy(className = "c-login-caption")
public SelenideElement welcomeLabelTest;

Another way to define the SelenideElement type attribute is using the @Wire annotation. You can write the SelenideElement type instead of masquerade types, but the name of the attribute should correspond to the cuba-id attribute of a DOM element that corresponds to the UI component.

@Wire
public SelenideElement loginField;

The third way to work with the Selenide elements is to use getDelegate() method. This method returns the SelenideElement type component. After that, you can use all test methods provided by Selenide.

loginWindow.getDelegate().exists();

Useful tips for the Groovy tests

You can use any JVM language with the library including Groovy / Scala / Kotlin. There are some useful tips for those who use Groovy to write the tests.

  • .with() method.

Groovy closures have a delegate associated with them. The delegate can respond to method calls which happen inside of the closure. It enables you to use methods/properties within a with {} closure without having to repeat the object name each time.

loginWindow.with {
    loginField.value = 'testUser'
    passwordField.value = '1'
    rememberMeCheckBox.checked = true

    commit()
}
  • Ability to set the value of the element using "property access" syntax

In Groovy, getters and setters form what we call a "property", and offer a shortcut notation for accessing and setting such properties. So instead of the Java-way of calling getters / setters, you can use a field-like access notation:

loginField.value = 'testUser'
  • def

def means that the actual type of the value will be automatically inferred by the compiler. It eliminates the unnecessary boilerplate in variable declarations and makes your code shorter.

def loginWindow = $c(LoginWindow)

Run tests

Please note that you need to download one of the latest versions of the web driver depending on the browser you want to use to testing. For Chrome browser this is chromedriver, for Firefox this is geckodriver.

To run the test, first of all, you need to set cuba.testMode property to true in the web-app.properties file in the web module in your CUBA application. After that you should start the application using Studio or Gradle tasks. To start application with Gradle, run the following tasks in the terminal:

gradle setupTomcat deploy createDb start

If you run your tests in Chrome browser, you need to edit standard test configuration for the test project in IntelliJ. To do so, click the Select Run/Debug Configuration button and select Edit Configurations in the drop-down list. In the VM options field, add the following:

-Dselenide.browser=chrome -Dwebdriver.chrome.driver=<your_path>/chromedriver.exe

where <your_path> is the path to the chrome driver on your computer.

Create Configuration

After that select the simple test or the test class you want to run, right click on it and select Debug option.

To run the tests using Gradle, add the following task to the build.gradle file:

test {
     systemProperty 'selenide.browser', System.getProperty('selenide.browser')
     systemProperty 'webdriver.chrome.driver', System.getProperty('webdriver.chrome.driver')
}

After that, run the following task in the terminal:

gradle test -Dselenide.browser=chrome -Dwebdriver.chrome.driver=<your_path>/chromedriver.exe

where <your_path> is the path to the chrome driver on your computer.

masquerade's People

Contributors

belyaev-andrey avatar jreznot avatar knstvk avatar natfirst avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

masquerade's Issues

TokenList support

  • Use inner LookupPickerField (search/set a value, open optionlist and pick up a value, apply actions)
  • Get selected items, apply conditions
  • Remove an item from a tokenlist
  • Remove all by clicking 'Clear' button

Tutorial: Tables

  • Rows selection (single and multi-select)
  • Sorting
  • Data asserting (cells checking)

Menu items do not open in the new version of the Firefox

Method _$(AppMenu).openItem('cuba-id) doesn work for the lates Firefox.
Instead of //span[@cuba-id="cuba-id'] Firefox waits for the //span[@cuba-id="cuba-id']/span
Environment: chromedriver v0.19.1, Masqurade v. 1.0-SNAPSHOT, Firefox 58.0.2 (64-bit)

Table - get all rows

For example we need to check number of rows in a table after applying a filter.

Existing method:
ElementsCollection getRows(By rowBy);
does not have "empty" selector.

Table.selectRow() opens maxTextLength area instead of selecting row

Environment
Masquerade version: 1.0-SNAPSHOT
Cuba version: 6.8.3

Precondition
Table has maxTextLength column.
image

Actual result
Table.selectRow(byText("Not a text of maxLength cell")) does not select the row but clicks on the maxLength cell and opens the area with the full text.

Expected result
The row selected.

Support @FindBy annotation with SelenideElement

@FindBy(className = 'v-grid-editor')
 SelenideElement gridEditor
java.lang.RuntimeException: Unable to instantiate composite com.codeborne.selenide.SelenideElement

	at com.haulmont.masquerade.Components.wireClassBy(Components.java:125)
	at com.haulmont.masquerade.Components.getTargetFieldValue(Components.java:186)
	at com.haulmont.masquerade.Components.wireClassBy(Components.java:131)
	at com.haulmont.masquerade.Components.wire(Components.java:78)
	at com.haulmont.masquerade.Components._$(Components.java:94)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
	at org.codehaus.groovy.runtime.callsite.StaticMetaMethodSite$StaticMetaMethodSiteNoUnwrapNoCoerce.invoke(StaticMetaMethodSite.java:151)
	at org.codehaus.groovy.runtime.callsite.StaticMetaMethodSite.callStatic(StaticMetaMethodSite.java:102)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:56)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:194)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:206)
	at com.haulmont.rent.web.DataGridUiTest.checkInlineEditor(DataGridUiTest.groovy:119)com.haulmont.rent.web.rules.WebBrowserReuseRule$1.evaluate(WebBrowserReuseRule.groovy:24)

Caused by: java.lang.InstantiationException: com.codeborne.selenide.SelenideElement
	at java.lang.Class.newInstance(Class.java:427)
	at com.haulmont.masquerade.Components.wireClassBy(Components.java:123)
	... 55 more
Caused by: java.lang.NoSuchMethodException: com.codeborne.selenide.SelenideElement.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.newInstance(Class.java:412)
	... 56 more

QA

Please consider that the original case is incorrect, because inline editor is not shown by default

Review README

It seems that run options on the current Firefox do not require marionette setting any more

Problem with AppMenu component

Sometimes main menu does not open after the first click on first menu item element with 'path[0]' and we see error e.g. like this:
Element not found {By.chained({By.xpath: //span[contains(@class, 'v-menubar-menuitem') and @cuba-id="taxi$CustomerAccount.browse"],By.className: v-menubar-menuitem-caption})} Expected: visible

Chrome version: 67.0.3396.87

Tutorial: How to add Logging

  1. Enable logback-classic or slf4j-simple
  2. Customize logback-test.xml
<configuration debug="false">
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>

        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{0} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="CONSOLE"/>
    </root>

    <logger name="com.codeborne.selenide" level="WARN"/>
    <logger name="org.openqa.selenium" level="WARN"/>
</configuration>
  1. Custom logging - LoggerFactory.getLogger(BlaBla.class).info("")
    See also:
@Wire
public Logger logger;

Support GroupTable component

  • Expand / Collapse a node (selected by label).
  • Get a collection of inner rows for further assertions.

Updates

New component GroupTable is introduced.

It supports the following two conditions:

  • Conditions#EXPANDED - checks that all group rows are expanded
  • Conditions#COLLAPSED - checks that all group rows are collapsed

The GroupTable component has the following method:

  • expandAll() - expands all group rows
  • collapseAll() - collapses all group rows
  • expand(By groupRowSelector) - expands first met group row that matches passed selector
  • collapse(By groupRowSelector) - collapses first met group row that matches passed selector

Usage example

@Test
void testExpandAll() {
    $c(AppMenu)
            .openItem(Menus.PRODUCT_BROWSE)

    $c(GroupTable, 'productsTable')
            .shouldBe(Condition.visible)
            .expandAll()
            .shouldBe(Conditions.EXPANDED)
}

@Test
void testCollapseAll() {
    $c(AppMenu)
            .openItem(Menus.PRODUCT_BROWSE)

    $c(GroupTable, 'productsTable')
            .shouldBe(Condition.visible)
            .collapseAll()
            .shouldBe(Conditions.COLLAPSED)
}

@Test
void testExpandSpecified() {
    $c(AppMenu)
            .openItem(Menus.PRODUCT_BROWSE)

    // only two groups exist
    $c(GroupTable, 'productsTable')
            .expand(withText('123'))
            .expand(withText('321'))
            .shouldBe(Conditions.EXPANDED)
}

Support dateValue condition for DateField

DateField.shouldHave(Conditions.dateValue(String)) fails with a trace:

Element should have dateValue '20.02.2018' {By.cubaId: datepart}
Element: '<div class="v-datefield v-datefield-popupcalendar v-widget v-readonly c-datefield v-datefield-c-datefield v-has-width v-datefield-day" cuba-id="datepart" id="291fd50df35ed98758aa696f0378eb28"></div>'
Element should have dateValue '20.02.2018' {By.cubaId: datepart}
Element: '<div class="v-datefield v-datefield-popupcalendar v-widget v-readonly c-datefield v-datefield-c-datefield v-has-width v-datefield-day" cuba-id="datepart" id="291fd50df35ed98758aa696f0378eb28"></div>'

The same time, DateField.getDateValue().contentEquals(String) returns true.

Environment: chromedriver v. 2.35, Masqurade v. 1.0-SNAPSHOT

Support for LookupField with newOptionAllowed = true

Lookup field with newOptionAllowed = true allows to input new option which is not in list.

With existing method LookupField.setValue() you can't enter new option

Scenario looks like

  • input value
  • press enter

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.