1. Dependencies

WebTester generally handles its dependencies by relying on the host project to provide them in the version it wants them as.

This means, as an example, that even though you declare a dependency on info.novatec.testit:webtester-support-assertj3 you will not inherit assertj automatically.

For a base setup of WebTester you could declare the following dependencies:

<dependencies>
    <dependency>
        <groupId>info.novatec.testit</groupId>
        <artifactId>webtester-core</artifactId>
    </dependency>
    <dependency>
        <groupId>info.novatec.testit</groupId>
        <artifactId>webtester-support-assertj3</artifactId>
    </dependency>
    <dependency>
        <groupId>info.novatec.testit</groupId>
        <artifactId>webtester-support-junit5</artifactId>
    </dependency>

    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-support</artifactId>
    </dependency>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-chrome-driver</artifactId>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
    </dependency>
</dependencies>

2. Browser Abstraction

WebTester provides an abstraction layer on top of Selenium’s WebDriver called (rather fittingly) Browser. There are several important interfaces related to browsers.

2.1. Browser

A Browser provides a streamlined and context centric API for the interaction with a web browser. It is the main entry point to the framework.

2.2. BrowserBuilder

A BrowserBuilder provides a builder API for initializing Browser instances and setting custom service implementations like the Configuration.

2.3. WebDriverBrowser

The WebDriverBrowser class implements Browser and is used to wrap a Selenium WebDriver. Instances can be created by using the WebDriverBrowser’s factory methods:

  1. WebDriverBrowser.forWebDriver(webDriver).build();

  2. WebDriverBrowser.buildForWebDriver(webDriver);

Both of these are equal. The first method can be used to customize same aspects of the browser before it is build.

Examples

// Initialization of a new WebDriverBrowser instance
WebDriver webDriver = createWebDriver();
Browser browser = WebDriverBrowser.buildForWebDriver(webDriver);
Browser browser = WebDriverBrowser.forWebDriver(webDriver).build();
Browser browser = new WebDriverBrowserBuilder(webDriver).build();

// Initialization of a new WebDriverBrowser instance with custom service implementations
Configuration config = createConfiguration();
PageObjectFactory factory = createFactory();

Browser browser = WebDriverBrowser.forWebDriver(webDriver)
                        .withConfiguration(config)
                        .withFactory(factory)
                        .build();

Browser browser = new WebDriverBrowserBuilder(webDriver)
                        .withConfiguration(config)
                        .withFactory(factory)
                        .build();

3. Creating Browser Instances

3.1. BrowserFactory

A BrowserFactory creates an abstraction over the Browser initialization based on project-global settings. They are intended to allow easy browser initialization and encapsulation of the underlying configuration / initialization processes. Most projects implement their own factories according to their specific environment. In case you just want to get started, we provide factories for the most common browsers:

  • ChromeFactory

  • EdgeFactory

  • FirefoxFactory

  • MarionetteFactory (new Firefox Geckodriver)

  • OperaFactory

  • InternetExplorerFactory

  • RemoteFactory

All of these are provided by the webtester-core module, but need you to provide the corresponding Selenium WebDriver dependencies yourself.

3.2. ProxyConfiguration

In order to configure a proxy you can either configure it manually when initializing the WebDriver or you can implement a ProxyConfiguration and provide it to the BrowserFactory before creating a Browser instance.

ProxyConfiguration pc = createProxyConfiguration();
Browser browser = new FirefoxFactory().withProxyConfiguration(pc).createBrowser();

3.3. Provided Factories

The webtester-core modules provides a number of BrowserFactory implementations out of the box.

3.3.1. ChromeFactory

This BrowserFactory uses the selenium-chrome-driver to create new Chrome Browser instances.

Default Driver Configuration

In order to optimize testing the following properties are set when creating a WebDriver using the ChromeFactory:

  • Native events are disabled → Selenium does not simulate human typing.

  • Untrusted certificates are always accepted.

Additional Service Executable

The ChromeDriver needs an additional executable to communicate with a Chrome browser. It can be downloaded here. The path to the executable must be declared as a system or environment property named: webdriver.chrome.driver

You can also declare this property within your WebTester configuration file(s). This will trigger the framework to expose this property for you.

Additional Information:

3.3.2. EdgeFactory

This BrowserFactory uses the selenium-edge-driver to create new Edge Browser instances.

Default Driver Configuration

In order to optimize testing the following properties are set when creating a WebDriver using the EdgeFactory:

  • Native events are disabled → Selenium does not simulate human typing.

  • Untrusted certificates are always accepted.

Additional Service Executable

The EdgeDriver needs an additional executable to communicate with an Edge browser. It can be downloaded here. Please make sure to choose the release version equal to your Windows 10 build. The path to the executable must be declared as a system or environment property named: webdriver.edge.driver

You can also declare this property within your WebTester configuration file(s). This will trigger the framework to expose this property for you.

3.3.3. FirefoxFactory and MarionetteFactory

These BrowserFactory implementations use the selenium-firefox-driver to create new Firefox Browser instances. To drive Firefox browsers up to version 46, the FirefoxFactory can be used. In order to drive newer versions (47++), the MarionetteFactory must be used. The only real difference between these two factories is the activation of the marionnette capability, but sadly in doing so you will need to provide the location of a GeckoDriver installation.

Default Driver Configuration

In order to optimize testing the following properties are set when creating a WebDriver using the FirefoxFactory:

  • Native events are disabled → Selenium does not simulate human typing.

  • Untrusted certificates are always accepted.

Additional Service Executable

Using the Marionette-activated WebDriver will force you to also specify the location of a GeckoDriver instance. This is basically a proxy between Selenium and the actual Firefox (like with the ChromeDriver). it can be downloaded here The path to the executable must be declared as a system or environment property named: webdriver.gecko.driver

You can also declare this property within your WebTester configuration file(s). This will trigger the framework to expose this property for you.

Additional Information:

3.3.4. InternetExplorerFactory

This BrowserFactory uses the selenium-ie-driver to create new Internet Explorer Browser instances.

Default Driver Configuration

In order to optimize testing the following properties are set when creating a WebDriver using the InternetExplorerFactory:

  • Native events are disabled → Selenium does not simulate human typing.

  • Untrusted certificates are always accepted.

Additional Service Executable

The InternetExplorerDriver needs an additional executable to communicate with a IE browser. It can be downloaded here. The path to the executable must be declared as a system or environment property named: webdriver.ie.driver

You can also declare this property within your WebTester configuration file(s). This will trigger the framework to expose this property for you.

Additional Information:

3.3.5. RemoteFactory

This BrowserFactory uses the RemoteWebDriver to connect to a Selenium Grid.

Default Driver Configuration

In order to optimize testing the following properties are set when creating a WebDriver using the RemoteFactory:

  • Native events are disabled → Selenium does not simulate human typing.

  • Untrusted certificates are always accepted.

  • Selenium Grid Host: localhost:4444

  • Default Browser: firefox with Marionette activated

The connection to the Selenium Grid can be configured in two ways:

  1. Set properties in configuration file.

  2. Set system properties to override the configuration at runtime (eg. -Dremote.browser.name=chrome).

4. Configuration

The behavior of WebTester can be configured on a browser by browser basis by providing a Configuration instance while creating the Browser.

A Configuration instance can be created by using a ConfigurationBuilder implementation. That instance can then be used to customize the browser’s configuration using a BrowserBuilder.

4.1. BaseConfigurationBuilder

The BaseConfigurationBuilder will use a BaseConfiguration instance as a starting point. It takes ConfigurationAdapter and ConfigurationExporter instances as options before the build() operation.

4.2. ConfigurationAdapter

A ConfigurationAdapter is used to change properties of an existing Configuration. This is done using a callback method adapt(Configuration c).

The following adapters are provided by the webtester-core module:

  • ClasspathPropertiesFileConfigurationAdapter

  • GlobalFileConfigurationAdapter

  • LocalFileConfigurationAdapter

4.2.1. ClasspathPropertiesFileConfigurationAdapter

This adapter can be used to load properties from any file on the classpath.

new ClasspathPropertiesFileConfigurationAdapter("config/foo.properties");

In addition to the file name, you can optionally provide an importance level like this:

new ClasspathPropertiesFileConfigurationAdapter("config/foo.properties", Importance.REQUIRED);

This will define the behaviour in case the file is not found on the classpath.

There are three levels of importance:

  • OPTIONAL: there will be an info log message

  • RECOMMENDED: there will be a warning log message

  • REQUIRED: there will be an exception

4.2.2. GlobalFileConfigurationAdapter

This adapter extends ClasspathPropertiesFileConfigurationAdapter and uses the a testit-webtester-global.properties file on the classpath’s root level. Its importance level is set to OPTIONAL.

It is intended to be used in collaboration with the LocalFileConfigurationAdapter. The global adapter would define defaults for any number of projects and be provided by a common codebase / framework.

4.2.3. LocalFileConfigurationAdapter

This adapter extends ClasspathPropertiesFileConfigurationAdapter and uses the a testit-webtester.properties file on the classpath’s root level. Its importance level is set to RECOMMENDED.

As stated by the importance level, this file is recommended to be present on the classpath because it is the default way of configuring WebTester for your project.

4.3. ConfigurationExporter

A ConfigurationExporter is used to "export" a Configuration to another System. This is done by using a callback method export(String key, Object value) for each key / value pair of the Configuration.

The following exporters are provided by the webtester-core module:

  • SystemPropertyConfigurationExporter

4.3.1. SystemPropertyConfigurationExporter

This exporter can be used to export each key / value pair as system properties in order to make them accessible using System#getProperty(String key).

4.4. Default Configuration

Since not everyone needs to customize the configuration in the context of his or her project a 'DefaultConfigurationBuilder' is provided which uses the following adapters (in order) and no exporters to build a Configuration :

  • GlobalFileConfigurationAdapter

  • LocalFileConfigurationAdapter

This builder is also used in case a Browser is build without providing a custom Configuration instance.

ConfigurationAdapter adapter1 = ...;
ConfigurationAdapter adapter2 = ...;
ConfigurationExporter exporter = ...;

// this will adapt a base configuration first with 'adapter1' and then with 'adapter2'
// after that the final configuration will be exported using the 'exporter'
Configuration config = new BaseConfigurationBuilder()
                            .withAdapters(adapter1, adapter2)
                            .withExporter(exporter)
                            .build();

4.5. Default Properties

These are all the named properties loaded by default:

# Whether or not the events should be fired.
# TYPE: boolean [true, false]
events.enabled = true

# The amount of time actions should be decelerated (i.e. for demonstrations).
# TYPE: int [milliseconds]
actions.deceleration = 0

# URL of the default entry point for the application under test.
# TYPE: String [Resource URL]
# defaults.entry-point =

# Folder in which to save screenshots if not otherwise specified.
# TYPE: String [absolute or relative path to be initialized as a java.io.File instance]
folders.screenshots = screenshots

# Folder in which to save source code of pages if not otherwise specified.
# TYPE: String [absolute or relative path to be initialized as a java.io.File instance]
folders.page-sources = sourcecode

# Folder in which to save log files if not otherwise specified.
# TYPE: String [absolute or relative path to be initialized as a java.io.File instance]
folders.logs = logs

# Whether or not color highlighting of used elements should be active or not.
# TYPE: boolean [true, false]
markings.enabled = false

# Color to use for the background of used elements if color highlighting is active.
# TYPE: String [HEX RGB code starting with'#']
markings.used.background = #ffd2a5

# Color to use for the outline of used elements if color highlighting is active.
# TYPE: String [HEX RGB code starting with'#']
markings.used.outline = #916f22

# Color to use for the background of read elements if color highlighting is active.
# TYPE: String [HEX RGB code starting with'#']
markings.read.background = #90ee90

# Color to use for the outline of read elements if color highlighting is active.
# TYPE: String [HEX RGB code starting with'#']
markings.read.outline = #008000

# Default timeout for wait operations.
# TYPE: int [seconds]
wait.timeout = 2

# Default interval in which to check a condition for wait operations.
# TYPE: int [milliseconds]
wait.interval = 100

# Name of the browser to use in Selenium Grid
# TYPE: String [firefox, chrome, safari, ...]
remote.browser.name = firefox

# Version of the browser to use in Selenium Grid. If not specified, any version will be used!
# TYPE: String [eg. 46.0.1]
# remote.browser.version =

# Whether the Marionette driver (Firefox 47++) should be used.
# TYPE: boolean [true, false]
remote.firefox.marionette = true

# Host or IO address where Selenium Grid is running
# TYPE: String [localhost, 192.168.0.1, ...]
remote.host = localhost

# Host or IO address where Selenium Grid is running
# TYPE: Integer
remote.port = 4444

5. Page Objects

5.1. The Page Object Pattern

The WebTester framework’s architecture and design is based around the Page Object Pattern. For more information about Page Object Pattern see:

5.2. Pages

The Page interface is the parent for all pages used to implement the Page Object Pattern. It provides base methods every page will need. Pages can be initialized using the create(pageClass) method of a Browser instance or from within another page.

The following example represents an application with two pages. On each page the same navigation menu is displayed. The page’s content differs from page to page. This demonstrates composition of page information.

// the navigation menu widget
public interface NavigationMenu extends PageFragment {

    @IdentifyUsing("#firstLink")
    Link firstLink();

    @IdentifyUsing("#secondLink")
    Link secondLink();

}

// a 'trait' interface declaring the property of having a navigation menu
public interface HasNavigationMenu {

    /* The navigation menu is identified by its ID
     * This automatically limits the search scope for the navigation menu's fragments to
     * everything contained inside the tag with the ID "navMenu" */
    @IdentifyUsing("#navMenu")
    NavigationMenu navigation();

}

// a page containing a table with the ID "fooTable" and inheriting the navigation menu trait
public interface FooPage extends Page, HasNavigationMenu {

    @IdentifyUsing("#fooTable")
    Table fooTable();

    ...

}

// a page containing a table with the ID "barTable" and inheriting the navigation menu trait
public interface BarPage extends Page, HasNavigationMenu {

    @IdentifyUsing("#barTable")
    Table fooTable();

    ...

}

5.3. Page Fragments

As can be seen in the previous example a page consists of different PageFragment declarations. The fragments can be accessed / initialized by invoking a method annotated with @IdentifyUsing. For mor information about page fragments see the next chapter.

5.4. Collections of Page Fragments

Multiple page fragments of a page can be retrieved as a Set, List or Stream by simply declaring any of those as the return type of a @IdentifyUsing annotated method:

public interface CollectionPage extends Page {

    @IdentifyUsing(".text-field")
    List<TextField> textFieldList();

    @IdentifyUsing(".text-field")
    Set<TextField> textFieldSet();

    @IdentifyUsing(".text-field")
    Stream<TextField> textFieldStream();

}

5.5. Relevant Annotations

There are several annotations which can be used within the context of a Page:

  • @Action

  • @IdentifyUsing

  • @Named

  • @PostConstruct

  • @PostConstructMustBe

  • @WaitUntil

5.6. Anatomy of a Page

Pages generally provide a number of distinct method types. The following sections will describe the most important ones.

5.6.1. Actions

Actions are methods which change the state of a page without leaving it. This could be the input of text in a text field or the selection of a value in a select menu.

Rules:

  • Method name represents an action: "setUsername", "changeDataOfBirth" etc.

  • Method returns the same instance of the Page for fluent API support.

  • Method does not change multiple states.

default LoginPage setFirstName(String name){
    firstName.setText(name);
    return this;
}

default LoginPage selectBirthMonth(String month){
    birthMonth.selectByText(month);
    return this;
}

5.6.2. Navigations

Navigations are methods which execute an action that leads to a page change. This could be the click of a link or the direct opening of an URL. The difference between navigations and actions is, that a navigation has to declare what page comes "next". This means that for a single navigation "action" there might me multiple methods. I.g. this is necessary to declare "bad case" paths through the application. E.g. if a login fails or a process could not be finished.

Rules:

  • Method name represents an action: "clickLogin", "clickLoginExpectingError" etc.

  • Method returns a new instance of the target page’s Page for fluent API support.

  • Method does not change multiple states.

default MainPage clickLogin(){
    login.click();
    return create(MainPage.class);
}

default LoginPage clickLoginExpectingError(){
    login.click();
    return create(LoginPage.class);
}

5.6.3. Workflows

Workflows combine different methods in order to allow for "fast" navigation over pages. I.g. they combine a set of actions with a navigation. This could e.g. be a single method to log into a system.

Rules:

  • Method name represents a process: "login", "register" etc.

  • Method’s return type and value depends on the last command in the workflow: Is it an action or a navigation?

  • Method does change multiple states.

default MainPage login(User user){
    return setUsername(user.getUsername())
        .setPassword(user.getPassword())
        .clickLogin();
}

default LoginPage loginExpectingError(User user){
    return setUsername(user.getUsername())
        .setPassword(user.getPassword())
        .clickLoginExpectingError();
}

5.6.4. Information Getter

Information getter are methods which retrieve information from a page. This could be the text of a displayed error message or the content of a certain text field.

Rules:

  • Method name represents a request: "getErrorMessages", "getNumberOfDisplayedTableEntries" etc.

  • Method’s return type might be anything but a Page.

  • Method does not change any states.

default String getErrorMessage () {
    return errorMessage.getText();
}

default int getNumberOfSearchResults () {
    int counter = 0;
    // some logic
    return counter;
}

6. Page Fragments

Page fragments are parts of a Page. They extend the PageFragment interface and represent any number of thing. From a single text field to a widget. WebTester provides a number of functional page fragments out of the box. These map more or less to HTML elements:

  • Button

  • Checkbox

  • Div

  • EmailField

  • Form

  • Headline

  • IFrame

  • Image

  • Link

  • ListItem

  • MultiSelect

  • NumberField

  • OrderedList

  • Paragraph

  • PasswordField

  • RadioButton

  • SearchField

  • SingleSelect

  • Span

  • Table

  • TableField

  • TableRow

  • TelephoneField

  • TextArea

  • TextField

  • UnorderedList

  • UrlField

  • GenericElement

  • GenericList

  • GenericSelect

  • GenericTextField

The Button for example is mapped to <button/>, <input type="reset">, <input type="submit"> and <input type="button"> while the SingleSelect maps to a very specific type of <select> (where the multiple attribute is not set).

In contrast to the generic interactions offered by Selenium’s WebElement interface these functional classes provide only those methods which are useful for the given context / their type. A SingleSelect does not provide methods to change its text, but it will have methods to change selection based on index, value or text.

Example

public interface SearchWidget extends PageFragment {

    @IdentifyUsing("#query")
    SearchField query();

    @IdentifyUsing("#submit")
    Button submit();

}

6.1. Validation

By default a PageFragment will match any HTML tag in form of a WebElement. Functional page fragments on the other hand are limited to a certain amount of valid HTML tags and attribute combinations. This is done by annotating them with @Mapping for a single mapping. This annotation can be used multiple times in case there is more then one valid combination.

Annotating any page fragment with @Mapping will trigger a validation logic anytime the underlying web element is resolved. In case the validation fails a MappingException is thrown.

The @Mapping annotation is used to define a valid combination of tag, attribute and attribute values of a web element to be used with a page fragment class.

There are a number of different ways to use this:

  • @Mapping(tag="div") Will be evaluated as 'valid' in case the web element has the tag 'div'.

  • @Mapping(tag="select", attribute="multiple") Will be evaluated as 'valid' in case the web element has the tag 'select' and the 'multiple' attribute is present.

  • @Mapping(tag="select", attribute="!multiple") Will be evaluated as 'valid' in case the web element has the tag 'select' and the 'multiple' attribute is not present.

  • @Mapping(tag="input", attribute="type", values={"text", "password"}) Will be evaluated as 'valid' in case the web element has the tag 'input' and the 'type' attribute has either the 'text' oder 'password' value.

  • @Mapping(validator=FooValidator.class) Will create a new instance of the given validator class and use it to evaluate the web element.

@Mapping(tag = "span")
public interface Span extends PageFragment {
    ...
}

@Mapping(tag = "select", attribute = "multiple")
public interface MultiSelect extends PageFragment {
    ...
}

6.2. Inheritance

Since all PageFragments are interfaces and Java currently does not support inheritance of annotations on interfaces because of a conceptual problem with multiple inheritance, it is neccessary to re-annotate PageFragment sub-classes when extending or methods when overriding them.

@Mapping(tag = "span")
public interface Span extends PageFragment {
    ...
}

public interface MySpan extends Span {
    // will no longer check @Mapping validity
}

6.3. Relevant Annotations

There are several annotations which can be used within the context of a PageFragment:

  • @Action

  • @Attribute

  • @IdentifyUsing

  • @Mark

  • @Named

  • @PostConstruct

  • @PostConstructMustBe

  • @Produces

  • @WaitUntil

6.4. Generic Page Element

The GenericElement PageFragment interface is basically the WebElement of page fragments. It opens up all methods of WebElement which were not already implemented in PageFragment (maybe with a different name).

It mainly intended for the Ad-Hoc find API in order to minimize the number of calls needed to make when rapidly prototyping or looking up deeply nested elements.

6.4.1. Casting

GenericElement provides a method as(Class) which allows the 'cast' of the generic element to any other PageFragment interface.

// find returns a GenericElement
Button b = browser.find(#button).as(Button.class);

7. Ad-Hoc Finding

It is not always the best solution to declare page fragments via public @IdentifyUsing method. Sometimes it is necessary to find a certain fragment programmatically:

  • Maybe the fragment is only used in very special cases and should therefore not be public…​

  • Maybe you need a list of fragments fitting certain parameters which can not be expressed with CSS or XPath..

  • Or maybe are just prototyping your approach and don’t want to implement page objects, yet…​

For these cases the Ad-Hoc finding API was developed. This API can be accessed through a Browser, Page or a PageFragment. Depending on where the API is accessed the search context for fragments might differ:

  • Browser: The whole HTML page is searched for the fragment.

  • Page: The whole HTML page is searched for the fragment.

  • PageFragment: The area between the page fragment’s open and close tags is searched for the fragment.

There are several ways to start finding fragments, here are a few examples:

// find an element by it's ID 'fooId' as a generic element
GenericElement element = getBrowser()
    .find("#fooId");

// find many elements by their shared CSS class 'foo' as a stream of generic elements
Stream<GenericElement> elements = getBrowser()
    .findMany(".foo");

// find an element by it's ID 'textField' as a text field (identifier first)
TextField textField = getBrowser()
    .findBy(id("textField"))
    .as(TextField.class);

// find an element by it's ID 'textField' as a text field (class first)
TextField textField = getBrowser()
    .find(TextField.class)
    .by(id("textField"));

// find all all elements with the CSS class 'foo'
// within an element with ID 'group' as a stream of text fields
Stream<TextField> textFields = getBrowser()
    .find("#group")
    .findBy(css(".foo"))
    .asMany(TextField.class);

8. Event System

WebTester provides an event mechanism where listeners can be registered to be infomed of things that occured inside the framework (e.g. navigation, button clicks etc.).

Examples

static EventListener customListener;

@BeforeClass
public static void registerEventListener () {
    customListener = (event) -> System.out.println(event);
    browser.events().register(customListener);
}

@AfterClass
public static void deregisterEventListener () {
    browser.events().deregister(customListener);
}

8.1. The EventSystem Class

The EventSystem is a service providing methods for firing and listening for Events. EventListener instances can be registered at the system as well as deregistered once they are no longer needed. Each Browser instance has it’s own instance of EventSystem

8.2. The Event Interface

An Event contains all the information needed to understand what happened. Since it is an interface implementing a custom event is very easy. In general it is recommended to treat events as data objects. They should not contain references to services or larger parts of the system. The webtester-core module provides a number of events for it’s actions:

Event Description

AcceptedAlertEvent

An alert was closed by accepting it.

ClosedBrowserEvent

The browser was closed.

ClosedWindowEvent

A browser window was closed.

DeclinedAlertEvent

An Alert was closed by declining it.

MaximizedWindowEvent

A Window was maximized.

NavigatedBackwardsEvent

A backwards navigation was executed.

NavigatedForwardsEvent

A forwards navigation was executed.

OpenedUrlEvent

An URL was opened.

RefreshedPageEvent

The current page was refreshed.

SavedSourceCodeEvent

Source code was saved.

SetWindowPositionEvent

The window positon was changed.

SetWindowSizeEvent

The window size was changed.

SwitchedToDefaultContentEvent

The WebDriver focus was changed back to the 'default' content.

SwitchedToFrameEvent

The WebDriver focus was changed to a frame.

SwitchedToWindowEvent

The WebDriver focus was changed to a window.

TookScreenshotEvent

A screenshot was taken.

ClearedEvent

An input element was clear of any set value.

ClickedEvent

A click was executed.

ContextClickedEvent

A context click was executed.

DeselectedAllEvent

Every option of a MultiSelect was deselected.

DeselectedByIndicesEvent

An option of a MultiSelect was deselected by index.

DeselectedByTextsEvent

An option of a MultiSelect was deselected by text.

DeselectedByValuesEvent

An option of a MultiSelect was deselected by value.

DoubleClickedEvent

A double click was executed.

EnterPressedEvent

The 'Enter' key was pressed.

FormSubmittedEvent

A form was submitted.

NumberSetEvent

The value of a NumberField was set.

SelectedByIndexEvent

An option of a SingleSelect or MultiSelect was selected by index.

SelectedByIndicesEvent

Multiple options of a MultiSelect were selected by index.

SelectedByTextEvent

An option of a SingleSelect or MultiSelect was selected by text.

SelectedByTextsEvent

Multiple options of a MultiSelect were selected by text.

SelectedByValueEvent

An option of a SingleSelect or MultiSelect was selected by value.

SelectedByValuesEvent

Multiple options of a MultiSelect were selected by value.

SelectionChangedEvent

The selection of a RadioButton or Checkbox was changed.

TextAppendedEvent

The value of an input field was appended with additional text.

TextSetEvent

The value of an input field was changed.

8.3. The EventListener Interface

An EventListener is a simple interface providing a single method void eventOccurred(Event event);. Instances of this interface can be registered at the EventSystem in order to be called every time an Event is fired.

8.4. Configuration

The firing of events can be disabled by setting the events.enabled property to false. This will disable the firing of all events except ExceptionEvent instances fired by using 'EventSystem#fireExceptionEvent(e)'.

9. Annotations

9.1. @Action

This annotation can be added to methods of Page or PageFragment subclasses in order to mark these methods as actions. Currently the only effect of this annotation the option to delay the execution of annotated methods by setting the property actions.deceleration to a certain amount of milliseconds.

Examples

// actions work on pages...
public interface FooPage extends Page {

    @Action
    default void doSomething() {
        ...
    }

}

// ... as well as on page fragments
public interface BarFragment extends PageFragment {

    @Action
    default void doSomething() {
        ...
    }

}

9.2. @Attribute

This annotation can be used within a PageFragment subclass in order to retrieve attributes of the underlying element. Attribute values (which are returned as Strings from the WebElement) will be parsed to the method’s return type.

Supported Types:

  • String

  • Boolean

  • Long

  • Integer

  • Float

  • Double

  • Optional of any of these types

Constraints:

  • Annotated method must not have arguments!

  • Annotated methods have to be part of a page fragment!

It is possible to declare annotated methods in any interface, as long as they are used from a page fragment.

Example of different attribute methods:

public interface FooFragment extends PageFragment {

    // returns the string value of the 'value' attribute
    @Attribute("value")
    String value();

    // returns the long value of the 'number' attribute
    @Attribute("number")
    Long number();

    // returns the optional string value of the 'optional' attribute
    @Attribute("optional")
    Optional<String> optional();

}

Example of trait interface with attribute method:

public interface HasValue {

    @Attribute("value")
    String value();

}

public interface BarFragment extends PageFragment, HasValue {
    // will have access to working 'value()' method
}

9.3. @IdentifyUsing

This annotation is used to tell the framework how the fragment(s) should be resolved when a page fragment returning method is invoked. The annotation provides all necessary information to identify the corresponding element(s) in the DOM of the displayed page:

  • how: Which ByProducer to use. I.e. CssSelector, Id, XPath etc. Defaults to CssSelector.

  • value: The value to be used by the mechanism.

Methods annotated with @IdentifyUsing must not have arguments and can only return:

  • Subclass of PageFragment

  • List of subclass of PageFragment

  • Set of subclass of PageFragment

  • Stream of subclass of PageFragment

Examples

public interface FooPage extends Page {

    // using the default CSS Selector and an ID
    @IdentifyUsing("#foo")
    TextField fooField();

    // using an explicit ID selector
    @IdentifyUsing(value = "#bar", how = Id.class)
    TextField barField();

    // all text fields as a stream identified by their common class 'text-field'
    @IdentifyUsing(".text-field")
    Stream<TextField> allTextFields();

}

public interface SearchWidget extends PageFragment {

    @IdentifyUsing("#query")
    SearchField query();

    @IdentifyUsing("#submit")
    Button submit();

}

9.4. @Mark

This annotation can be added to methods of PageFragment subclasses in order to mark the fragment in case the method is invoked. The 'marking' is done by setting different style attributes for the underlying element.

The following marking types are available:

  • USED - the state of the fragment was changed

  • READ - the state of the fragment was read

The marking feature can be activated / deactivated by setting the markings.enabled property. The color of each of these types can be configured by changing any of the following properties:

  • markings.used.background

  • markings.used.outline

  • markings.read.background

  • markings.read.outline

Colors are specified as HEX RGB color codes, i.e. #ffaa99.

Example

public interface FooFragment extends PageFragment {

    // calling this method will mark the FooFragment as used
    @Mark(As.USED)
    default void doSomething() {
        ...
    }

}

9.5. @Named

This annotation can be added to @IdentifyUsing annotated PageFragment returning methods of Page or PageFragment subclasses in order to override the name of the returned fragment.

Collections of fragments can’t be named at the moment!

This is useful in cases where the element IDs are not very clear or event cryptic. The name is used in any logs where the fragment is referenced. As well as events fired by the framework.

Example

public interface FooPage extends Page {

    @Named("The Foo Widget 42")
    @IdentifyUsing("#foo")
    FooWidget widget();

    ...

}

9.6. @PostConstruct

This annotation can be added to methods of Page or PageFragment subclasses. Every annotated method will be invoked after an instance of this subclass was initialized. These methods should be used to verify that the correct page is displayed or the fragment has 'working' state. The order in which multiple annotated methods are invoked is not deterministic.

Each method should work on it’s own and not depend on another method being invoked!

Since these methods are invoked using reflection, it is not possible to have method arguments!

As an alternative for @PostConstruct the @PostConstructMustBe annotation can be used on page fragment returning methods.

Examples

public interface FooPage extends Page {

    @IdentifyUsing("#foo")
    FooWidget widget();

    @PostConstruct
    void assertThatWidgetIsVisible () {
        assertThat(widget).is(visible());
    }

    ...

}

public interface FooWidget extends PageFragment {

    @IdentifyUsing("#one")
    TextField fieldOne();

    @IdentifyUsing("#two")
    TextField fieldTwo();

    @PostConstruct
    void assertThatTextFieldsAreVisible () {
        assertThat(fieldOne).is(visible());
        assertThat(fieldTwo).is(visible());
    }

    ...

}

9.7. @PostConstructMustBe

This annotation can be added to @IdentifyUsing annotated methods of Page or PageFragment subclasses. Every annotated method will be invoked after an instance of this subclass was initialized and the condition provided by the annotation will be checked. This mechanism is intended to be used in order to prevent unnecessary @PostConstruct methods to check basic conditions of parts of the page / fragment. As with @PostConstruct, the order in which these methods are invoked / checked is not deterministic!

It is important to note that not all Predicate classes will work with this annotation. The mechanism with which the predicate is evaluated will initialize the given class via reflection and needs a default constructor to work!

Collection and Streams are currently NOT supported!

Examples

public interface FooPage extends Page {

    @PostConstructMustBe(Visible.class)
    @IdentifyUsing("#foo")
    FooWidget widget();

    ...

}

public interface FooWidget extends PageFragment {

    @PostConstructMustBe(Visible.class)
    @IdentifyUsing("#one")
    TextField fieldOne();

    @PostConstructMustBe(Visible.class)
    @IdentifyUsing("#two")
    TextField fieldTwo();

    ...

}

9.7.1. Combination with @WaitUntil

The @PostConstructMustBe annotation can be used in combination with WaitUntil. This is especially useful in AJAX heavy applications where a fragment might be created with a short delay.

Example

In this example the widget is checked as soon as BarPage is initialized. But WaitUntil will be triggered when invoking the method assuring that the widget is present before checking if it is visible.

public interface BarPage extends Page {

    @PostConstructMustBe(Visible.class)
    @WaitUntil(Present.class)
    @IdentifyUsing("#bar")
    BarWidget widget();

    ...

}

9.8. @Produces

This annotation can be used on methods of PageFragment sub-classes in order to trigger the creation and firing of an Event whenever that method is executed.

9.8.1. Constraints

  • Only Event classes extending AbstractPageFragmentEvent are supported

  • Only works in PageFragment classes, not Page

  • Used Event classes need to define a PageFragmentEventBuilder inner class

9.8.2. PageFragmentEventBuilder

Instances of this interface are used to build Event instances when @Produces is used. The implementation classes need to be declared as static inner classes of the Event named Builder. This convention allows for the dynamic used of these builders without having to manage all builder types manually.

public class ClickedEvent extends AbstractPageFragmentEvent {

    public ClickedEvent(PageFragment fragment) {
        super(fragment);
    }

    @Override
    public String describe() {
        return "clicked: " + getPageFragmentName();
    }

    public static class Builder extends AbstractPageFragmentEventBuilder<ClickedEvent> {

        @Override
        protected ClickedEvent buildWith(PageFragment fragment) {
            return new ClickedEvent(fragment);
        }

    }

}

In cases where data from the WebElement is needed to build the Event there are two hooks which are called by WebTester before and after invoking the annotated method. These are only used by the framework if the builder specifies that it needs the data.

public class TextSetEvent extends AbstractPageFragmentEvent {

    private final String before;
    private final String after;

    public TextSetEvent(PageFragment fragment, String before, String after) {
        super(fragment);
        this.before = before;
        this.after = after;
    }

    @Override
    public String describe() {
        return "text of '" + getPageFragmentName() + "' was set to '" + after + "' (was '" + before + "')";
    }

    public static class Builder extends AbstractPageFragmentEventBuilder<TextSetEvent> {

        private String before;
        private String after;

        @Override
        public boolean needsBeforeData() {
            return true;
        }

        @Override
        public PageFragmentEventBuilder<TextSetEvent> setBeforeData(WebElement webElement) {
            this.before = webElement.getAttribute("value");
            return this;
        }

        @Override
        public boolean needsAfterData() {
            return true;
        }

        @Override
        public PageFragmentEventBuilder<TextSetEvent> setAfterData(WebElement webElement) {
            this.after = webElement.getAttribute("value");
            return this;
        }

        @Override
        protected TextSetEvent buildWith(PageFragment fragment) {
            return new TextSetEvent(fragment, before, after);
        }

    }

}

9.9. @WaitUntil

This annotation can be added to @IdentifyUsing annotated methods of Page or PageFragment subclasses. When the annotated method is invoked a 'wait until' operation is executed using the annotations condition. The condition is provided via a class reference in order to support custom conditions. See Conditions for a set of provided page fragment related predicates.

It is important to note that not all Predicate classes will work with this annotation. The mechanism with which the predicate is evaluated will initialize the given class via reflection and needs a default constructor to work!

Collection and Streams are currently NOT supported!

A timeout can be configured by setting the timeout and unit properties of the annotation. If no custom timeout is set the host browser’s configuration defaults are used.

Examples

public interface FooPage extends Page {

    @WaitUntil(Visible.class)
    @IdentifyUsing("#foo")
    FooWidget widget();

    ...

}

public interface FooWidget extends PageFragment {

    @WaitUntil(Visible.class)
    @IdentifyUsing("#one")
    TextField fieldOne();

    @WaitUntil(value=Visible.class, timeout=500, unit=TimeUnit.MILLISECONDS)
    @IdentifyUsing("#two")
    TextField fieldTwo();

    ...

}

10. Utilities

10.1. Conditions

The utility class Conditions provides several factory methods for creating Condition instances. These are specialized subclasses of Java 8’s Predicate interface and can be used in the following sub-systems:

  • Waiting: @Wait and Wait.until(..)

  • Post Construct Assertions: @PostConstructMustBe(…​)

  • Filtering of PageFragement Streams

// ad-hoc finding of page fragments with filter
browser.findMany(".textfield").filter(Conditions.is(Conditions.visible()));
// waiting until a certain condition is met
Wait.until(textField).has(Conditions.text("foo"));

10.1.1. Syntax

Conditions are designed to be readable. They take a lot of inspiration from Hamecrest’s Matcher and AssertJ’s fluent API. It is generally recommended to use static imports when working with the Conditions class.

We provide two kinds of out of the box conditions in the info.novatec.testit.webtester.conditions package:

  • Syntax operations like Has, Is, Not and Either in order to make conditions more readable

  • Page fragment conditions like Attribute, Visible, Selected etc.

10.1.2. List of current Conditions

Syntax:

  • Either

  • Has

  • Is

  • Not

Page Fragment:

  • Attribute

  • AttributeWithValue

  • Disabled

  • Editable

  • Enabled

  • Interactable

  • Invisible

  • Present

  • PresentAndVisible

  • ReadOnly

  • Selected

  • SelectedIndex

  • SelectedIndices

  • SelectedText

  • SelectedTexts

  • SelectedValue

  • SelectedValues

  • Visible

  • VisibleTextContains

  • VisibleTextEquals

10.2. By-Producers

The utility class ByProducers provides several factory methods for creating ByProducer instances. These are used by WebTester as an abstraction over Selenium’s By classes. They are relevant to the following (sub-)systems:

// Ad-Hoc finding of page fragment
browser.findBy(ByProducers.id("username"));

10.3. Simulating Mouse Actions

The Mouse utility class contains all kinds of methods which allow you to use or at least simulate the use (depending on the WebDriver implementation) of mouse actions. These are the currently implemented mouse actions:

  • click(PageFragment)

  • doubleClick(PageFragment)

  • contextClick(PageFragment)

  • moveTo(PageFragment)

  • moveToEach(PageFragment, PageFragment…​)

  • moveToEach(Collection<PageFragment>)

// clicks a button
Mouse.click(button);

// double clicks an image
Mouse.doubleClick(image);

// moves the mouse to the link
Mouse.moveTo(link);

// moves the mouse to each link as they appear
Mouse.moveToEach(fileMenu, fileMenuNew, fileMenuNewPage);

10.3.1. Mouse.click()

Executes a click on the given PageFragment by first moving the mouse to the center of it.

10.3.2. Mouse.doubleClick()

Executes a double click on the given PageFragment by first moving the mouse to the center of it.

10.3.3. Mouse.contextClick()

Executes a context click on the given PageFragment by first moving the mouse to the center of it.

10.3.4. Mouse.moveToEach()

Moves the mouse to each of the given `PageFragment`s in turn. The page fragments have to be visible in order to move the mouse to it. This method can be used to navigate dynamically displayed menu structures because it waits for each page fragment to be displayed before moving the mouse to it.

10.3.5. Mouse.moveTo()

Moves the mouse to the given PageFragment. The page fragment has to be visible in order to move the mouse to it.

10.3.6. Fluent API for Mouse Actions

In addition to these single actions the Mouse utility class provides several methods for execution a number of actions with a fluent syntax:

  • on(PageFragment)

  • sequence()

// actions on fragment
Mouse.on(button).click();
Mouse.on(button).doubleClick();
Mouse.on(button).contextClick();

// sequence
Mouse.sequence().moveTo(image).click();
Mouse.sequence().moveTo(image).moveTo(otherImage).click();
Mouse.sequence().click(image).doubleClick(otherImage);

10.4. Waiting

The Wait utility class provides a fluent API for all kinds of wait operations. This includes waiting an exact amount of time and waiting for certain conditions with a timeout.

There are 4 distinct kinds of Wait operations:

  1. Waiting an exact amount of time: Wait.exactly(..)

  2. Waiting until an object state is reached: Wait.until(..)

  3. Waiting until an object supplier return value’s state is reached: Wait.untilSupplied(..)

  4. Waiting like "2." but executing another action during waiting for a given condition: Wait.untilWithAction(..)

Examples

// waits 5 seconds
Wait.exactly(5, TimeUnit.SECONDS);

// waits 150 milliseconds
Wait.exactly(150, TimeUnit.MILLISECONDS);

// waits 1 hour
Wait.exactly(1, TimeUnit.HOURS);

// waits until the hidden field is visible on the DOM - with default timeout
PageFragment hiddenField = ...;
Wait.until(hiddenField).is(visible());

// waits until the hidden field is visible on the DOM - with custom timeout
Wait.withTimeoutOf(10, TimeUnit.SECONDS).until(hiddenField).is(visible());

// waits until the call to 'findMany(".foo")' returns a non empty list
Wait.untilSupplied(() -> findMany(".foo")).is((foos) -> !foos.isEmpty());

// waits until the hidden field is visible on the DOM - with default timeout
PageFragment hiddenField = ...;
PageFragment wrongField = ...;
WaitingAction action = new WaitingAction(isVisible(wrongField), () -> refresh())
Wait.untilWithAction(isVisible(hiddenField), );

10.4.1. Wait.exactly(…​)

This is the most primitive wait operation. It allows to wait for a specific amount of time. That amount is specified by to parameters: the amount and the time unit.

The maximum precision for the wait operation is milliseconds. If any more precise unit is defined (e.g. nanoseconds), there will be no wait.

10.4.2. Wait.until(..)

This kind of wait operation will take any object instance and allows for the definition of several conditions to be waited on in order. It is important to note that the conditions will always be evaluated against the initially specified instance!

In the above example you can see a command which will wait until a 'hidden' field is visible. This will work because the given object is a PageFragment. Since page fragments act as proxies for WebElement instances, which are not cached, the check on visibility can return a different result for each invocation.

But let’s say, as an example, the given object is a list of page fragments and you want to wait until the list has a certain size. In this case the size of the list will never change unless it’s contents is manipulated asynchronously.

In order to check something like this take a look at Wait.untilSupplied(..).

10.4.3. Wait.untilWithAction(..)

This behaves essentially like the Wait.until(..) operation, but with the option to execute a given command. This is managed by an instance of the WaitingAction class. This class takes an a condition in form of an object supplier to check if the given command has to be executed. The command can be any method call provided by an empty lamda.

The operation is designed to help you stabilize tests in flaky environments.

10.4.4. Wait.untilSupplied(..)

This kind of wait operation will take an object supplier as its parameter. The supplier is invoked every time a condition is checked. With this approach you can wait until a dynamic object - like a list of page fragments - has a certain state (e.g. size).

11. Support Modules

11.1. AssertJ 3 Assertions

The support module webtester-support-assertj3 provides AssertJ 3 assertion implementations for many properties of page fragments:

  • ButtonAssert

  • GenericTextFieldAssert

  • MultiSelectAssert

  • PageFragmentAssert

  • SelectableAssert

  • SingleSelectAssert

All of these can be accessed through a single utility class named WebTesterAssertions. Which extends AssertJ’s Assertions class and therefore provides all of AssertJ’s default assertions as well.

TextField username = ...;
WebTesterAssertions.assertThat(username).hasText("fooUser");

11.2. Hamcrest Matchers

The support module webtester-support-hamcrest provides Hamcrest Matcher implementations for many properties of page fragments:

  • AttributeMatcher

  • AttributeValueMatcher

  • ButtonLabelMatcher

  • DisabledMatcher

  • EnabledMatcher

  • InvisibleMatcher

  • NoOptionsMatcher

  • NoSelectedOptionsMatcher

  • NumberOfOptionsMatcher

  • NumberOfSelectedOptionsMatcher

  • OptionsMatcher

  • OptionsTextsMatcher

  • OptionsValuesMatcher

  • PresentMatcher

  • SelectedMatcher

  • SelectedOptionsMatcher

  • SelectionIndexMatcher

  • SelectionIndicesMatcher

  • SelectionTextMatcher

  • SelectionTextsMatcher

  • SelectionValueMatcher

  • SelectionValuesMatcher

  • TagMatcher

  • TextContainingMatcher

  • TextMatcher

  • VisibleMatcher

  • VisibleTextContainingMatcher

  • VisibleTextMatcher

All of these can be accessed through a single utility class named WebTesterMatchers. Which extends Hamcres’s Matchers class and therefore provides all of Hamcrest’s default matchers as well.

TextField username = ...;
// without static imports
WebTesterMatchers.assertThat(username, WebTesterMatchers.has(WebTesterMatchers.text("fooUser")));
// with static imports
assertThat(username, has(text("fooUser")));

11.3. JUnit 4 Runner

The support module webtester-support-junit4 provides a custom JUnit Runner. This runner includes the following features:

  • Life cycle management of class and test level Browser instances.

  • Automatic navigation to an entry point to the application under test before each test.

  • Injection of custom configuration properties into the following basic types: String, Integer, Long, Float, Double and Boolean

11.3.1. Test Runner Life Cycle

The WebTesterJUnitRunner is based on the default BlockJUnit4ClassRunner and extends its life cycle at certain points. The following shows the workflow of a test class with two test methods.

  • static rules' before() methods

  • static Browser creation and injection

  • injection of configuration properties into static fields annotated with @ConfigurationValue

  • static methods annotated with @BeforeClass

  • instance rules' before() methods

  • instance Browser creation and injection

  • injection of configuration properties into instance fields annotated with @ConfigurationValue

  • instance methods annotated with @Before

  • test method 1

  • instance methods annotated with @After

  • instance Browsers are closed

  • instance rules' after() methods

  • instance rules' before() methods

  • instance Browser creation and injection

  • injection of configuration properties into instance fields annotated with @ConfigurationValue

  • instance methods annotated with @Before

  • test method 2

  • instance methods annotated with @After

  • instance Browsers are closed

  • instance rules' after() methods

  • static methods annotated with @AfterClass

  • static Browsers are closed

  • static rules' after() methods

11.3.2. Browser Life Cycle Management

All Browser’s life cycle is managed on class as well as test level (static / non static fields). To use this feature simply declare a Browser field in your test class and annotate it with @Resource. Every uninitialized (null) field annotated in that way will be injected with a new Browser instance. Fields which are already initialized with a Browser will be included in the life cycle handling as well, but no new instances are created by the runner.

@RunWith ( WebTesterJUnitRunner.class )
public class DifferentBrowserFieldModifiersTest {

    // A pre-initialized Browser which will not be initialized with a new
    // browser but the instance will be handled as part of the life cycle.
    @Resource
    static Browser preInitializedBrowser = new Browser(new FirefoxDriver());

    // A static Browser which will be initialized with a new instance before
    // the first and closed after the last test is executed.
    @Resource
    @CreateUsing ( ... )
    static Browser classScopedBrowser;

    // An instance Browser which will be initialized with a new instance
    // before and closed after each test is executed.
    @Resource
    @CreateUsing ( ... )
    Browser testScopedBrowser;

    // An instance Browser field which will be ignored by the runner since
    // the @Resource annotation is missing.
    Browser notAManagedBrowser;

    ...

}

11.3.3. Configuring a BrowserFactory

In order to configure the BrowserFactory used to create the Browser instances, the @CreateUsing annotation must be used.

@RunWith ( WebTesterJUnitRunner.class )
public class DifferentBrowserFactoriesTest {

    // Uses the FirefoxFactory to create Firefox instances.
    @Resource
    @CreateUsing ( FirefoxFactory.class )
    static Browser firefox;

    // Uses the InternetExplorerFactoryto create IE instances.
    @Resource
    @CreateUsing ( InternetExplorerFactory.class )
    Browser internetExplorer;

    ...

}

11.3.4. Defining an Entry Point

In order for a Browser to be automatically navigated to an applications entry point before each test, the @EntryPoint annotation can used. The navigation at the beginning of each test is done whether or not a Browser is static!

The URL can be static or contain variables. These variables are resolved against the annotated Browser’s Configuration as String values. In case a variable could not be resolved an IllegalStateException is thrown before the first test.

@RunWith ( WebTesterJUnitRunner.class )
public class EntryPointsTest {

    // Will begin each test on Google.
    @Resource
    @CreateUsing ( ... )
    @EntryPoint ( "http://www.google.com" )
    static Browser classScopedBrowser;

    // Will begin each test on Bing.
    @Resource
    @CreateUsing ( ... )
    @EntryPoint ( "http://www.bing.com" )
    Browser testScopedBrowser;

    // Will use url provided by property
    @Resource
    @CreateUsing ( ... )
    @EntryPoint("${properties.url}")
    Browser variableUrl;

    ...

}

11.3.5. Configuration Property Injection

All custom configuration properties can be injected into the following base field types: String, Integer, Long, Float, Double and Boolean. The injection is done for all fields which are annotated with @ConfigurationValue.

@RunWith ( WebTesterJUnitRunner.class )
public class ConfigurationValuesTest {

    // Injects the integer value of "customer.integer"
    @ConfigurationValue ( "custom.integer" )
    static Integer customInteger;

    // Injects the string value of "customer.string"
    @ConfigurationValue ( "custom.string" )
    String customString;

    ...

}

11.3.6. Multiple Browser Instances and Configuration Property Injection

Since every Browser has it’s own Configuration instance a "primary" Browser has to be declared when using multiple Browser instances and the Configuration property injection feature. The primary browser will be the source for the configuration properties injected by the WebTesterJUnitRunner. If only one browser is managed it is automatically used as the primary browser!

In case you want to inject property values into a static field your primary browser has to be static as well!

@RunWith ( WebTesterJUnitRunner.class )
public class MultiBrowserConfigurationValuesTest {

    @Primary
    @Resource
    @CreateUsing ( ... )
    static Browser primaryBrowser;

    @Resource
    @CreateUsing ( ... )
    Browser anotherBrowser;

    @ConfigurationValue ( "custom.integer" )
    static Integer customInteger;

    @ConfigurationValue ( "custom.string" )
    String customString;

    ...

}

11.4. JUnit 5 Extensions

The support module webtester-support-junit5 provides a set of JUnit 5 extensions:

  • ManagedBrowserExtension:

    • Initialization of static and instance Browser fields.

    • Automatic opening and closing of managed Browser depending on field scope.

  • EntryPointExtension:

    • Automatic navigation to 'entry point' URL.

    • Support variables which are resolved against a Configuration.

  • RegisteredEventListenerExtension:

    • Initialization of instance EventListener fields.

    • Automatic registration and unregistration of EventListener to managed Browser.

  • PageInitializerExtension:

    • Initialization of Page fields before each test.

    • Supports multiple Browser instances.

  • ConfigurationValueExtension:

    • Injection of configuration values into instance fields of the following types:

      • String

      • Integer

      • Long

      • Float

      • Double

      • Boolean

    • Custom field types are supported via extensions

11.4.1. ManagedBrowserExtension

By annotating a Browser field with @Managed the extension is triggered and will manage this field’s life cycle: - For static fields the initialization is done before the first @BeforeAll annotated method is invoked and the browser will be closed after the last @AfterAll annotated method was invoked. - For instance fields the initialization is done before the first @BeforeEach annotated method is invoked and the browser will be closed after the last @AfterEach annotated method was invoked.

In case more than one Browser is used, each has to have a unique name. The name can be provided by setting the value property of the @Managed annotation.

In order for WebTester to know what kind of Browser should be created for each field there are two annotations: - @CreateBrowsersUsing can be used to annotate a test class and set the BrowserFactory to be used for the whole class. - @CreateUsing can be used to annotate a Browser field directly in order to set the BrowserFactory for this field specifically. This will override any global definition!

@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {

    @Managed("browser-1")
    static Browser staticBrowser;

    @Managed("browser-2")
    Browser instanceBrowser;

    @Managed("browser-3")
    @CreateUsing(BarFactory.class)
    Browser differentFactory;

    ...

}

11.4.2. EntryPointExtension

By annotating any @Managed Browser field with @EntryPoint you can specify an URL which will be navigated to before each test execution. The URL can be static or contain variables. These variables are resolved against the annotated Browser’s Configuration as String values. In case a variable could not be resolved an UnknownConfigurationKeyException is thrown before the first test.

@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {

    @Managed("browser-1")
    @EntryPoint("http://www.example.com")
    Browser staticUrl;

    @Managed("browser-2")
    @EntryPoint("${properties.url}")
    Browser variableUrl;

    @Managed("browser-3")
    @EntryPoint("http://${host}:${port}/index.html")
    Browser staticMixedWithVariableUrl;

    ...

}

11.4.3. RegisteredEventListenerExtension

By annotating any instantiable EventListener field with @Registered you can specify a browser to which the EventListener has to be registered and unregistered automatically.

The extension will initialize the field if it’s not pre-initialized and register the EventListener before the first @BeforeEach annotated method is invoked. The unregistration will be done after the last @AfterEach annotated method was invoked.

In case more than one Browser is used, the target browsers must be specified explicitly.

This extension does not support static fields!

@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {

   @Managed
   Browser browser;

   @Registered
   MyEventListener created; // will have new instance

   @Registered
   EventListener preInitialized = new MyEventListener(); // this instance will be used

   ...

}
@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {

   @Managed("browser-1")
   Browser browser1;

   @Managed("browser-2")
   Browser browser2;

   @Managed("browser-3")
   Browser browser3;

   @Registered(targets = { "browser-1", "browser-2" })
   CustomEventListener listener;

   ...

}

11.4.4. PageInitializerExtension

By annotating any Page field with @Initialized it will be initialized with a new instance of that Page class before the first @BeforeEach annotated method is invoked. In case the test class has multiple @Managed Browser instances the source property of the annotation needs to specify which browser should be used to initialize the Page.

This extension does not support static fields!

@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {

    @Managed("browser-1")
    Browser browser1;

    @Managed("browser-2")
    Browser browser2;

    @Initialized(source = "browser-1")
    FooPage page1;

    @Initialized(source = "browser-2")
    BarPage page2;

    ...

}

11.4.5. ConfigurationValueExtension

By annotating any field with @ConfigurationValue and providing a key by setting the value property the specified value will be retrieved from the Configuration and injected into the field. This is done before the first @BeforeEach annotated method is executed.

Currently the primitive object types String, Integer, Long, Float, Double and Boolean are supported out of the box. Custom types can be used when providing a matching ConfigurationUnmarshaller implementation / class reference as the annotation’s using property

As with @Initialize for Page fields, a source can be specified in cases where multiple browsers are managed for a single test.

This extension does not support static fields!

@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {

    @Managed
    Browser browser;

    @ConfigurationValue("stringValue")
    String stringValue;

    @ConfigurationValue("integerVaue")
    Integer integerVaue;

    @ConfigurationValue("longValue")
    Long longValue;

    @ConfigurationValue("floatValue")
    Float floatValue;

    @ConfigurationValue("doubleValue")
    Double doubleValue;

    @ConfigurationValue("booleanValue")
    Boolean booleanValue;

    @ConfigurationValue(value = "fooValue", using = FooTypeUnmarshaller.class)
    FooType fooValue;

    ...

}

In case you are declaring multiple Browser fields, the source of the configuration properties must be declared. This is necessary because each browser might have a different configuration and the extension cannot decide which is the 'correct' one.

@EnableWebTesterExtensions
@CreateBrowsersUsing(FooFactory.class)
public class ExampleUiTest {

    @Managed("browser-1")
    Browser browser1;

    @Managed("browser-2")
    Browser browser2;

    @ConfigurationValue(value = "stringValue", source = "browser-1")
    String stringValueOfBrowser1;

    @ConfigurationValue(value = "stringValue", source = "browser-2")
    String stringValueOfBrowser2;

    ...

}

11.5. Spring 4/5 Integration

11.5.1. Spring Configuration Adapter

The webtester-support-spring4 and webtester-support-spring5 modules each provides a ConfigurationAdapter implementation called SpringEnvironmentConfigurationAdapter. This adapter can be used to resolve properties from the existing Configuration against a Spring Environment. All keys which could successfully be resolved against the environment will then be overridden in the configuration.

# WebTester Configuration
foo = hello world!
bar = welcome world!

# Spring Environment
foo = hello spring world!

# Resulting Configuration
foo = hello spring world!
bar = welcome world!

11.5.2. Factory Beans

In addition to the configuration adapter the module also provides FactoryBean implementations which can be used to easily initialize different WebTester services as beans.

  • DefaultSpringConfigurationFactoryBean

  • ConfigurationBuilderFactoryBean

  • PrototypeConfigurationBuilderFactoryBean

DefaultSpringConfigurationFactoryBean

Creates a Configuration instance which results from using the DefaultConfigurationBuilder in conjunction with the SpringEnvironmentConfigurationAdapter.

ConfigurationBuilderFactoryBean

Creates a singleton ConfigurationBuilder instance. ConfigurationAdapter and ConfigurationExporter beans can be added via setters.

PrototypeConfigurationBuilderFactoryBean

Creates a prototyped ConfigurationBuilder instance. ConfigurationAdapter and ConfigurationExporter beans can be added via setters.

12. Kotlin

Since version 2.3 WebTester supports the use of Kotlin. Up until then the declarative nature of WebTester would not work with the way Kotlin is implementing default methods on interfaces.

In order to work with Kotlin, you must add the webtester-kotlin module to your test dependencies. Within this module you’ll find two classes:

  • info.novatec.testit.webtester.kotlin.pages.Page

  • info.novatec.testit.webtester.kotlin.pagefragments.PageFragment

Both of these are alias classes for their corresponding Java counterparts. In addition to providing a more Kotlin-esk API, their use will also act as a flag for WebTester to consider Kotlin when it’s generating code.

Other than using these special classes when creating pages and page fragments, everything else should work the same as with Java.