Page Objects

"If you have WebDriver APIs in your test methods, You're Doing It Wrong."

Simon Stewart, creator of Selenium WebDriver

You have to take it seriously if the creator on an API gives you such a serious suggestion on how to use his API. tapir highly encourages you to use Page Objects by making the creation and usage as easy as possible.

Page Objects encapsulate implementation details of the SUT and just provide a business API for the test classes. The test classes just interact with the page objects and the implementation details are shadowed. This approach is very robust against functional and technical changes in the SUT as you always have only a single point to adjust. The test cases can focus on the behaviour of the application and do not have to deal with technical issues.

Hint
There is a lot of information about Page Objects around the web. One of the most fundamental abstracts is written by Martin Fowler].

In tapir’s Selenium integration you need these dependencies:

<dependency>
    <groupId>de.bmiag.tapir</groupId>
    <artifactId>tapir-page</artifactId>
</dependency>
<dependency>
    <groupId>de.bmiag.tapir</groupId>
    <artifactId>tapir-selenium</artifactId>
</dependency>
<dependency>
    <groupId>de.bmiag.tapir</groupId>
    <artifactId>tapir-html-basic-api</artifactId>
</dependency>
<dependency>
    <groupId>de.bmiag.tapir</groupId>
    <artifactId>tapir-html-basic-impl</artifactId>
</dependency>

A Page Object for the Google website looks like this:

@Page
class GooglePage {
    @SeleniumElement(name="q")
    TextField queryField

    @SeleniumElement(name="btnG")
    Button searchButton
}

You just have to provide some fundamental information without any boilerplate or glue code. @Page declares the class as a page object. There is a TextField called queryField and a Button called searchButton on the page. @SeleniumElement tells tapir to use its Selenium implementation. The provided name attributes “g” and “btnG” finally bind the TextField and the Button to their HTML counterparts.

You can use the page object in your test class like described in the example below. You can just inject the page as a field by using Spring’s @Autowired annotation.

@TestClass
class GoogleTest {

    @Autowired
    GooglePage googlePage

    @Step
    def void searchForTapir() {
        googlePage.queryField.text ="tapir"
        googlePage.searchButton.click
    }
}
Hint
Please notice the syntactic sugar Xtend provides. For more information why tapir chooses Xtend over plain Java, consult the chapter Why does tapir use Xtend?.

Check that a Page is active

Optionally, pages can implement PageActiveCheck. If so you have to implement isPageActive( ). The page checks by itself if it’s active, e.g. by checking for a specific element or a headline text. If the page is active, the method returns true, otherwise false.

Explicit assertions

Assertions using PageActiveCheckExtensions

Test classes can assert that a page is active or inactive by using the PageActiveCheckExtensions as an extension.

@UseExtension(PageActiveCheckExtensions)
class GoogleTest {

    @Autowired
    GooglePage googlePage

    @Step
    def void assertGooglePageActive() {
        googlePage.assertPageActive
    }
}

Assertions by declaring @AssertPostPage

Using the @AssertPostPage annotation you can ensure that the given page is active after the step is completed:

@Step
@AssertPostPage(GooglePage)
  def void assertGooglePageActive() {
}

Implicit assertions

Explicitly checking for a page to be active is not needed mostly, because tapir does this check for you whenever you interact with the element of your page (e.g. click a button or set a value into an input field). Before tapir interacts with an element on a page, it asserts that the expected page is active.

Private page fields

The fields of page objects are usually generated with a getter method to access them. However, sometimes it is useful to have pure private visible page object fields. This is especially useful if you need some elements inside your page active check method but don’t want them accessible from the outside. You can use the @Private annotation to make sure that no public getter is generated.

@Page
class MyPage {

    @Private
    @SeleniumElement(name="message")
    Label message

    ...

}

Now the field message can only be accessed from inside MyPage.

Page Component

In order to facilitate modularity you can specify Page components (annotated by @PageComponent) which are reusable parts of your pages. Several pages can reference or include the same page component. Page components do not “know” where they are embedded, in other words, they are contextless and can not implement PageActiveCheck.

Comparison of a page and a page component:

Page Page Component
Nestable
Standalone
PageActiveCheck
Showcase

Nested Pages

Pages and Page components can be nested by using the @Include and @Reference annotations.

@Reference

@Reference just generates a getter for the referenced Page (component).

@Page
class Page1 {
    @Reference
    PageComponent1 pageComponent1
}

@Include

In contrast to @Reference, @Include generates delegate methods for all public members of the annotated field. Include solves the problem of multiple inheritance as it is possible to include multiple pages / page components.

@Page
class Page1 {
    @Include
    PageComponent1 pageComponent1
}

Tables

Tables are special components, as they are very heterogeneous throughout different applications. Fortunately tapir provides a base implementation which allows you already to work with most default tables. The main difference between tables and the other basic HTML elements is that tables are typed with a generic - the row type.

 @SeleniumElement(xpath="//table")
 Table<ProductTableRow> products

For technical reasons, the row type has to be an interface for which you can define multiple (but at least one) implementation.

 interface ProductTableRow extends TableRow {

   def Label getProductID()

   def Label getName()

}

In most cases you should let your implementation of the row extend the base class DefaultSeleniumTableRow. This class provides a helpful method to define the columns of a single row: getTapirElementFromColumn. If your table row consists of columns, each containing only a single HTML component, you just have to specify the column number and the type of the component.

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class ProductTableRowImpl extends DefaultSeleniumTableRow implements ProductTableRow {

  override getProductID() {
    getTapirElementFromColumn(0, Label)
  }

  override getName() {
    getTapirElementFromColumn(1, Label)
  }

}

Note that your row implementation has to have the prototype scope, as the row implementation is created for each row of your table. For simple tables you are already finished. But let us assume that your column is a little bit more complex and contains two buttons. In this case you have to query the web element of the row directly and have to use the SeleniumElementFactory to create instances of the components.

 @Autowired
 SeleniumElementFactory seleniumElementFactory

 override getEdit() {
   val element = webElement.findElement(By.linkText('Edit'))
   seleniumElementFactory.getSeleniumElement(element, Button)
 }

 override getDelete() {
   val element = webElement.findElement(By.linkText('Delete'))
   seleniumElementFactory.getSeleniumElement(element, Button)
 }
Hint
The SeleniumElementFactory provides multiple methods to create components. Some of them work with instances of WebElementProvider. Together with WebElementQuery, you can define the query rather than just querying the web element yourself. This allows tapir to query the web elements in certain situations again and ensures in return a more stable test execution.