1. Introduction
In this article we look at how to interact with web pages in the Serenity Journey pattern. Beyond the standard shortcuts that come with Serenity (such as the Serenity PageObject
base class), the Serenity Journey Pattern implementation provides a number of shortcuts to streamline your tests further.
2. A simple Journey Pattern test
The following test is a simple example of a Serenity Journey Pattern test:
private CurrentFilter theCurrentFilter = new CurrentFilter();
@Test
public void should_indicate_what_filter_is_currently_being_used() {
givenThat(james).wasAbleTo(OpenTheApplication.onTheHomePage());
when(james).wasAbleTo(AddTodoItems.called("Walk the dog", "Put out the garbage"));
then(james).should(seeThat(theCurrentFilter, is(All)));
when(james).attemptsTo(FilterItems.byStatus(Active));
then(james).should(seeThat(theCurrentFilter, is(Active)));
}
Let’s see how we interact with the web application in this test.
In Serenity tests, you usually interact with a web page in two places:
1. in the Action
classes (such as FilterItems.byStatus(Active)
), where you actively do something to the page (click a button, enter a value in a field, etc), and
2. in the Question
classes (such as CurrentFilter
), where you observe the state of the application (read a value, check if a button is read-only, etc).
3. Interacting with the page in Action classes
Serenity describes how a user interacts with an application in terms of three layers: * Goals that represent the high level business objectives; * Tasks that describe the high-level steps the user takes to achieve these goals; and * Actions that describe how the user interacts with the application to perform each step.
You can of course define your own domain-specific actions, but Serenity provides a number of bundled ones for interacting with web pages that are designed to help you author your tests faster. These include actions that let you open the browser on a particular URL, click on things, enter values into fields, select values in drop-downs, and so on.
3.1. Opening a URL
Let’s step through the test presented above to see some examples. The first way we interact with a web application is to open a web page to the desired URL.
This happens at the start of the test, using the OpenTheApplication
task:
givenThat(james).wasAbleTo(OpenTheApplication.onTheHomePage());
The OpenTheApplication
task is a simple task that opens the browser to the home page of the TodoMVC application:
public class OpenTheApplication implements Task {
private ApplicationHomePage applicationHomePage; (1)
public static OpenTheApplication onTheHomePage() {
return instrumented(OpenTheApplication.class);
}
@Step("{0} opens the application on the home page")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(Open.browserOn().the(applicationHomePage)); (2)
}
}
1 | Page Objects are automatically instantiated inside Journey Pattern tasks |
2 | Use the Open action class to open the browser to the default URL for the applicationHomePage Page Object. |
The ApplicationHomePage
extends the Serenity PageObject
class as a convenient way to provide entry points to the application, using the @DefaultUrl
annotation as shown here:
@DefaultUrl("http://todomvc.com/examples/angularjs/#/")
public class ApplicationHomePage extends PageObject {}
3.2. Clicking on elements
To click on a link, a button, or any other element, you can use the Click
action object. An example is shown here in the ClearCompletedItems
class:
public class ClearCompletedItems implements Task {
@Step("{0} clears all the completed items")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(Click.on(ClearCompleted.BUTTON));
}
}
The ClearCompleted
class is the Journey Pattern equivalent of a Page Object - a simple class that knows how to locate a number of related web elements. The best way to locate an element is to use the Target
class, which lets you provide both a human-readable name and a (CSS or XPath) selector:
public class ClearCompleted {
public static Target BUTTON = Target.the("Clear completed button")
.locatedBy("#clear-completed");
}
3.3. Entering values into fields
You can also use the Serenity Action classes to enter values into a field. The AddATodoItem
task, shown below, is a good example of how this is done.
when(james).attemptsTo(AddATodoItem.called("Buy some milk"));
The implementation of this class uses the Enter
action to enter a value into a field, and then press the RETURN key:
public class AddATodoItem implements Task {
private final String thingToDo;
protected AddATodoItem(String thingToDo) { this.thingToDo = thingToDo; }
@Step("{0} adds a todo item called #thingToDo")
public <T extends Actor> void performAs(T theActor) {
theActor.attemptsTo(
Enter.theValue(thingToDo) (1)
.into(NewTodoForm.NEW_TODO_FIELD) (2)
.thenHit(Keys.RETURN) (3)
);
}
public static AddATodoItem called(String thingToDo) {
return Instrumented.instanceOf(AddATodoItem.class).withProperties(thingToDo);
}
}
1 | What value are we Entering |
2 | What field are we entering it into |
3 | (Optional) We can also hit one or more keys afterwards |
4. Reading values
The other way to interact with a web page is to observe the state of the page. In the Journey Pattern implementation in Serenity, this is generally done in a Question
, or as a precondition for a task or action.
4.1. UI Interaction classes
When you implement Question
classes, you often need to query the web page. You can do this in several ways. For example suppose we want to be able to write something like this:
then(james).should(seeThat(theCurrentFilter, is(Active))); (1)
1 | Active is an enum value from the TodoStatusFilter class |
One way to do this might look like the following:
@Subject("the displayed todo items")
public class CurrentFilter implements Question<TodoStatusFilter> {
@Override
public TodoStatusFilter answeredBy(Actor actor) {
String selectedValue = BrowseTheWeb.as(actor)
.findBy("#filters li .selected") (1)
.getText();
return TodoStatusFilter.valueOf(selectedValue); (2)
}
}
1 | Look up the field using a CSS selector |
2 | Convert the selected value to an enum |
We could also use the UI Action classes bundled with Serenity to simplify this code to the following:
@Subject("the displayed todo items")
public class CurrentFilter implements Question<TodoStatusFilter> {
@Override
public TodoStatusFilter answeredBy(Actor actor) {
return Text.of(FilterSelection.SELECTED_FILTER) (1)
.viewedBy(actor) (2)
.asEnum(TodoStatusFilter.class); (3)
}
}
1 | Read the text value from the SELECTED_FILTER field |
2 | As viewed by the current actor |
3 | And convert it to the TodoStatusFilter enum |
This saves typing and makes the intent of the code clearer. UI Action classes in the net.serenitybdd.screenplay.questions
package let you access almost anything visible on the web page, with direct mappings for most of the getter functions of the WebElementState
class, including:
Text: Return the text value attribute of a field
Value: Return the value
attribute of a field
SelectedStatus: Indicate whether a checkbox is selected
SelectedValue: Get the selected value in a drop-down list
SelectedOptions: Get the list of selected options in a drop-down list
CSSValue: Get the value of a given CSS attribute
Visibiliy: Indicate whether a checkbox is visible
You can also convert the retrieved values to other types, such as numbers, dates or enums. For example, the following code would return the retrieved value in the form of a Joda DateTime object:
return Text.of(ClientPage.DATE_OF_BIRTH).viewedBy(actor).asDate("dd/MM/yyyy")
If a target matches more than one element, you can also return lists of values, by using the asList()
method:
return Text.of(ClientPage.FAVORITE_COLOR).viewedBy(actor).asList()
You can also convert the returned results to a list of enums, e.g.
return Text.of(ClientPage.FAVORITE_COLOR).viewedBy(actor).asListOf(Color.class)
4.2. Web state matchers
The WebElementStateMatchers
class provides a number of Hamcrest matchers that you can use in your should()
methods, for example:
dana.should(seeThat(the(NewTodoForm.NEW_TODO_FIELD)), isVisible()));
dana.should(seeThat(the(NewTodoForm.NEW_TODO_FIELD)), isEnabled()));
dana.should(seeThat(the(NewTodoForm.NEW_TODO_FIELD)), containsText("Feed the cat")));
You can also enhance these with domain-specific exceptions using the orComplainWith()
method (see Semantic Exceptions for more details):
theActor.should(seeThat(the(deleteButtom), isEnabled())
.orComplainWith(DeleteButtonShouldBeEnabledException.class));