Screenplay Selenium Webdriver tasks

Serenity provides a large number of built-in Screenplay tasks that make interacting with a web application quicker and easier to write. In this chapter, we will look at everything you need to know to write web tests with Serenity Screenplay.

Screenplay WebDriver abilities

Before a Screenplay actor can interact with a web page, we need to give them a WebDriver instance to work with. You do this with the BrowseTheWeb ability. In a JUnit test, this code could look like the following:

@Managed
WebDriver driver;
...
Actor sam = Actor.named("Sam");
sam.can(BrowseTheWeb.with(driver));

If you are using Cucumber, Serenity will manage the WebDriver instances for you. You just need to define a Cast object, whose job it is to provide actors to the scenarios. A simple approach is to use the OnlineCast, which returns actors who can all browse the web with the default browser:

@Before
public void setTheStage() {
    OnStage.setTheStage(new OnlineCast());
}

You can also use the whereEveryoneCan method to provide additional abilities, e.g.

@Before
public void setTheStage() {
    OnStage.setTheStage(
        OnlineCast.whereEveryoneCan(
            CallAnApi.at("http://my.server.endpoint")
        )
    );
}

This will provide actors who can both use WebDriver tasks and RestAssured tasks.

Opening a web page

In Screenplay, you open a new page using the Open interaction. This can work with a URL, e.g:

sam.attemptsTo(Open.url("https://google.com"));

If you have defined a Page Object with a default url, you can open a page object:

@DefaultUrl("https://google.com")
public class GooglePage extends PageObject {}
...

GooglePage googlePage;
...
sam.attemptsTo(Open.browserOn().the(googlePage));

Or you can use the class directly, e.g.

sam.attemptsTo(Open.browserOn().the(GooglePage.class));

Simple WebDriver interactions

Common WebDriver actions are represented by their own Interaction classes in Serenity Screenplay.

Clicking on an element

The Click interaction class allows you to perform a WebDriver click on an element.

Target SUBMIT_BUTTON = Target.the("Submit button").located(By.cssSelector("input[value='Go']"))

sam.attemptsTo(Click.on(SUBMIT_BUTTON));

In addition to a Target object, the Click.on(…​) method lets you click on a By locator, an XPath or CSS expression in String form, or a WebElementFacade. For example:

sam.attemptsTo(Click.on(By.cssSelector("input[value='Go']")));

sam.attemptsTo(Click.on("#submit-button"));

WebElementFacade submitButton = find(By.cssSelector("input[value='Go']"));
sam.attemptsTo(Click.on(submitButton));

You can also provide a list of locators, to progressively narrow down the search. For example, the following will click on the link with the text "Accounts" inside the main menu element:

sam.attemptsTo(Click.on(By.cssSelector("#mainMenu"), By.linkText("Accounts")));

As with a standard Serenity click operation, if the target is a form element (input, button, select, textarea, link, option), Serenity will wait until the element is enabled before clicking.

Typing values into fields

To type values into fields (where a conventional Selenium test would use the sendKeys() method), you use the Enter interaction class. The Enter class uses a simple DSL

Target NEW_TODO = Target.the("New Todo input field").locatedBy(".new-todo");

sam.attemptsTo(
    Enter.theValue("Walk the dog").into(NEW_TODO)
);

You can also use the thenHit() method as a convenience to enter a text value and then send an individual key to the same element:

sam.attemptsTo(
    Enter.theValue("Walk the dog").into(NEW_TODO).thenHit(Keys.ENTER)
);

Serenity waits until a field is enabled before typing a value into the field, and clear the field beforehand. For some UIs, this may not be necessary, or may even interfer with the screen behaviour. To precisely replicate the WebDriver sendKeys() behaviour, you can use the keyValues() method instead of enterValues():

sam.attemptsTo(
    Enter.keyValues("Walk the dog").into(NEW_TODO)
);

Hitting a key

If you just want to hit a single key into a field, you can also use the Hit interaction class. This class also uses sendKeys() under the hood, but can sometimes be more readable:

sam.attemptsTo(
    Hit.the(Keys.ENTER).into(NEW_TODO)
);

Or (for equivalent results):

Target SELECT_CITY = Target.the("City dropdown").locatedBy(".city-selection-list");

sam.attemptsTo(
    Hit.the(Keys.ARROW_DOWN).keyIn(SELECT_CITY)
);

Working with dropdowns

We can use the SelectFromOptions interaction to select a value in a dropdown list, either by value, by visible text, or by index. An example is shown below:

sam.attemptsTo(
    SelectFromOptions.byVisibleText("Paris").from(SELECT_CITY)
);

Moving the mouse

Some tests need us to hover over an element, for example to display a menu or icon. In Serenity Screenplay you can use the MoveMouse interaction class to do this. Like the other interaction classes, this will work with an element, an XPath or CSS selector, a locator or a sequence of locators.

Target PROFILE_BUTTON = Target.the("Profile Button").locatedBy("#profile");

sam.attemptsTo(
    MoveMouse.to(PROFILE_BUTTON)
);

Often when we move the mouse over an element, we want to perform another action afterwards. The MoveMouse interaction class lets us do this using the andThen() method, which takes a lambda expression as a parameter. The lambda takes an Actions object as a parameter, which can be used to define subsequent actions. For example to move over a button and then doubleclick, we could write the following:

sam.attemptsTo(
    MoveMouse.to(PROFILE_BUTTON).andThen( actions -> actions.doubleClick() )
);

Or we could shorten the code to a method reference:

sam.attemptsTo(
    MoveMouse.to(PROFILE_BUTTON).andThen( Actions::doubleClick() )
);

JavaScript Clicks

On some sites, a normal WebDriver click does not work, and we need to use JavaScript directly. We can do this using the JavaScriptClick interaction class. This has an identical usage as Click, as shown here:

sam.attemptsTo(JavaScriptClick.on(SUBMIT_BUTTON));

Scrolling

The ScrollTo interaction class lets you scroll to a specified element:

sam.attemptsTo(Scroll.to(SUBMIT_BUTTON));

You can also specify whether to align the scrolling with the top or bottom of the target element by andAlignToTop() and andAlignToBottom() methods:

sam.attemptsTo(Scroll.to(SUBMIT_BUTTON).andAlignToTop());

Switching Frames

You can switch to another window or frame using the Switch interaction, e.g.

sam.attemptsTo(Switch.toFrame(2));

This works for all of the WebDriver switchTo() methods. For example to switch to an alert:

sam.attemptsTo(Switch.toAlert());

Writing custom interaction classes

It is easy to write your own interaction class using the BrowseTheWeb ability.

Querying a web UI

Serenity Screenplay also gives you a large number of options when it comes to querying a web UI. Most involve special types of Question class.

In Screenplay web tests, you can simply implement a question which returns the object type you are interested, and then query the UI in a conventional Webdriver way. For example, suppose we want to read the user name on a page, which can be located with the ".user-name" CSS selector.

A Screenplay assertion to check the user name might look like this:

sam.should(seeThat(TheUserName.value(), equalTo("sam")));

We could create a TheUserName question class to query this field as follows:

@Subject("the displayed username")
public class TheUserName implements Question<String> {
    @Override
    public String answeredBy(Actor actor) {
        return BrowseTheWeb.as(actor).findBy(".user-name").getText();
    }

    public static Question<String> value() { return new TheUserName(); }
}

Here we use BrowseTheWeb.as(actor) to get the Serenity WebDriver API for the actor’s webdriver instance, which gives access to the full range of Serenity Page Object methods.

We could also use a Target to locate the user name, which we could store in a separate Page Compenent class:

public static Target USER_NAME = Target.the("User name").locatedBy(".user-name");

We can then use the resolveFor() method to find the element matching that target in the actor’s browser:

@Subject("the displayed username")
public class TheUserName implements Question<String> {

    @Override
    public String answeredBy(Actor actor) {
        return USER_NAME.resolveFor(actor).getText();
    }

    public static Question<String> value() { return new TheUserName(); }
}

Alternatively, we could write this class as a factory, and use a lambda expression instead of a fully blown Qeustion class:

public class TheUserName {

    public static Question<String> value() {
        return actor -> USER_NAME.resolveFor(actor).getText();
    }
}

In this case, the @Subject annotation will have no effect, so we need to pass in the name of the object we are checking in the Screenplay assertion:

sam.should(seeThat("the displayed username", TheUserName.value(), equalTo("sam")));

Serenity also provides a number of shortcuts related to querying web pages, which you will find in the net.serenitybdd.screenplay.questions package. In the following sections we look at how these work.

Reading text

Reading a value from an element is one of the most common ways we interact with a web page. The Text class can be used to read text values in a more fluent style. The basic usage pattern looks something like this:

Text.of(<target>).viewedBy(actor).as<Type>();

Using this style, we could rewrite the user name question from the previous section like this:

public class TheUserName {

    public static Question<String> value() {
        return actor -> Text.of(USER_NAME).viewedBy(actor).asString();
    }
}

Reading numbers

This would not be a very strong use case by itself. But the Text class makes it easy to do type conversions in a very readable way. For example, if we wanted to read an Integer value rather than a String, we can use the asInteger() method in the place of asString(). This might lead to a factory class like the following:

public class TheUser {

    public static Question<String> name() {
        return actor -> Text.of(USER_NAME).viewedBy(actor).asString();
    }

    public static Question<Integer> age() {
        return actor -> Text.of(USER_AGE).viewedBy(actor).asInteger();
    }

}

You can also use asLong(), asDouble(), asFloat() and asBigDecimal() to convert the value to other numerical types.

Reading lists

We can also return a list of values located by a given target, with the asList() method, illustrated in the following example:

@Subject("the displayed todo items")
public class DisplayedItems implements Question<List<String>> {

    @Override
    public List<String> answeredBy(Actor actor) {
        return Text.of(TodoList.ITEMS)
                .viewedBy(actor)
                .asList();
    }
}

Reading dates

You can convert a retrieved value to a LocalDate using the asLocalDate() method, e.g.

    public static Question<LocalDate> dateOfBirth() {
        return actor -> Text.of(USER_DOB).viewedBy(actor).asLocalDate("dd-MMM-yyyy");
    }

Reading enums

It is often convenient to convert displayed values directly to enums. You can do this by using the asEnum() method. For example, suppose we have the following enum class:

public enum TodoStatusFilter {
    All, Active, Completed
}

We could write a class to convert a text value on a page to the TodoStatusFilter enum as shown here:

public class CurrentFilter implements Question<TodoStatusFilter> {

    @Override
    public TodoStatusFilter answeredBy(Actor actor) {
        return Text.of(TodoList.SELECTED_FILTER)
                .viewedBy(actor)
                .asEnum(TodoStatusFilter.class);
    }

    public static CurrentFilter selected() {
        return new CurrentFilter();
    }
}

Reading off-screen text

The WebDriver getText() method may fail in certain cases if the element being read is not visible on the screen. For example, if you have a long list, elements below the bottom of the screen may not be accessible. To get around this issue, you can scroll to the element before retrieving the text, but this can be slow and not always reliable.

Another approach is to read th textContent attribute, which will return a value whether the element is displayed or not. You can use the TextContent class in the place of the Text class to achieve this. For example, you could read the full list of results, even if some where below the bottom of the page, like this:

@Subject("the displayed todo items")
public class DisplayedItems implements Question<List<String>> {

    @Override
    public List<String> answeredBy(Actor actor) {
        return TextContent.of(TodoList.ITEMS)
                .viewedBy(actor)
                .asList();
    }
}

Reading values

For HTML form elements, we are often more interested in the value attribute than the text content. In native WebDriver, you would get the value by calling element.getAttribute("value"). In Serenity Screenplay, we can use the Value class to achieve this.

For example, suppose we have a registration form

public class TheRegistrationForm {

    public static Question<String> firstName() {
        return actor -> Value.of(FIRST_NAME).viewedBy(actor).asString();
    }
}

The Value.of() method follows the same pattern as Text.of(), so all of the conversion strategies discussed in the previous section also apply to Value.of().

Reading from dropdown lists

You can read the selected value or values from a dropdown list using either the SelectedValue or SelectedVisibleTextValue class. For example, imagine our registration form had an age dropdown, with integer values. We could reead the selected value as follows:

public class TheRegistrationForm {

    public static Question<Integer> age() {
        return actor -> SelectedValue.of(AGE).viewedBy(actor).asInteger();
    }
}

If you needed to return the displayed value, you could use SelectedVisibleTextValue instead:

public class TheRegistrationForm {

    public static Question<String> age() {
        return actor -> SelectedVisibleTextValue.of(AGE).viewedBy(actor).asString();
    }
}

Reading attributes

Sometimes you need to read values directly HTML or CSS attributes. You can do this using the Attribute and CSSValue classes. Some examples are shown below:

public class TheRegistrationForm {

    public static Question<Boolean> hasOptedIn() {
        return actor ->  Attribute.of(OPT_IN).named("checked").viewedBy(actor).as(Boolean)
    }

    public static Question<String> titleFont() {
        return actor -> CSSValue.of(TITLE).named("font").viewedBy(actor).asString()
    }
}

Checking UI state

You can also ask about the state of elements on the page. State includes things like visibiliy or presence on the page, whether a field is enabled or disabled, and whether a field is selected.

The WebElementStateMatchers contains a set of Hamcrest matchers which can be used to check the state of an element, as well as whether the field contains a given text or value. Possible matchers include:

  • isVisible()

  • isNotVisible()

  • isCurrentlyVisible()

  • isNotCurrentlyVisible()

  • isEnabled()

  • isNotEnabled()

  • isCurrentlyEnabled()

  • isNotCurrentlyEnabled()

  • isPresent()

  • isNotPresent()

  • isSelected()

  • isNotSelected()

  • containsText()

  • containsOnlyText()

  • containsSelectOption()

  • hasValue()

To check the state of an element, you first need to convert the locator used to locate the element into a Question about the state of the element. This locator can be a Target, a By locator, or an XPath or CSS expression.

To convert a locator to a Question about the element’s state, we use the WebElementQuestion class. This class has three static DSL methods, which are all equivalent (you just use the one that reads the most fluently):

  • WebElementQuestion.stateOf(locator)

  • WebElementQuestion.valueOf(locator)

  • WebElementQuestion.the(locator)

Once you have converted the locator to a question, you can combine this with a WebElementStateMatchers method to form a Screenplay assertion. For example, to check the visibility of a field, we could write the following:

sam.should(
    seeThat(
        WebElementQuestion.the(TITLE),
        WebElementStateMatchers.isVisible()
    )
)

Using static imports will make the code more readable:

import static net.serenitybdd.screenplay.matchers.WebElementStateMatchers.*;
import static net.serenitybdd.screenplay.questions.WebElementQuestion.the;
.
.
.
sam.should(
    seeThat(the(TITLE), isVisible())
)

The following example checks the value of the AGE field:

sam.should(
    seeThat(the(AGE), hasValue("40"))
)

Performing waits

When writing web tests, you often need to wait for specific states or events. In Screenplay, you can use the WaitUntil interaction class to do this. This class lets you wait for a given state, using the same DSL as we saw in the previous section. For example, to wait until the title is visible, we could write the following:

sam.attemptsTo(
    WaitUntil.the(TITLE, isVisible())
)

We could also specify a maximum wait time:

sam.attemptsTo(
    WaitUntil.the(TITLE, isVisible()).forNoMoreThan(10).seconds()
)