Page Object Design
Page Object Design
Page Object is a Design Pattern which has become popular in RPA for enhancing automation scripts maintenance and reducing code duplication. A page object is an object-oriented class that serves as an interface to a page of your application. The automation scripts then use the methods of this page object class whenever they need to interact with the UI of that page. The benefit is that if the UI changes for the page, the automation scripts themselves don’t need to change, only the code within the page object needs to change. Subsequently all changes to support that new UI are located in one place.
The Page Object Design Pattern provides the following advantages:
- There is a clean separation between automation scripts code and page specific code such as locators and layout.
- There is a single repository for the services or operations offered by the page rather than having these services scattered throughout the automation scripts.
Implementation
Page-Object approach supporting can achieved in the 3 following steps:
Extend Application class
In the EasyRPA Page Object design we create an application class as entry point of any applications. All application classes should extend the Application class.
Our Application class has a constructor which takes a Driver object which will be responsible to initialization of pages via PageFactory (using the method "createPage"). Also as the entry point of any applications it has the method "open" which accept the any application arguments and should be responsible for opening application and returning the start page (usually it's login page).
So, the applications class which automation "Invoice Plane" web application will looks like:
import eu.ibagroup.easyrpa.engine.rpa.Application; import eu.ibagroup.easyrpa.engine.rpa.driver.BrowserDriver; import eu.ibagroup.easyrpa.engine.rpa.element.UiElement; public class InvoicePlaneApplication extends Application<BrowserDriver, BrowserElement, BrowserSearch> { public InvoicePlaneApplication(BrowserDriver driver) { super(driver); } @Override public LoginPage open(String... args) { ... } }
Return a Page object from method "open"
Method "open" should return a page object, so the whole InvoicePlaneApplication.java class will looks like the following:
import eu.ibagroup.easyrpa.engine.rpa.Application; import eu.ibagroup.easyrpa.engine.rpa.driver.BrowserDriver; import eu.ibagroup.easyrpa.engine.rpa.element.UiElement; import eu.ibagroup.easyrpa.system.invoiceplane.page.LoginPage; public class InvoicePlaneApplication extends Application<BrowserDriver, BrowserElement> { public InvoicePlaneApplication(BrowserDriver driver) { super(driver); } @Override public LoginPage open(String... args) { String invoicePlaneUrl = args[0]; getDriver().get(invoicePlaneUrl); getDriver().manage().window().maximize(); LoginPage loginPage = createPage(LoginPage.class); return loginPage; } }
Extend appropriate Page class
Any class of page object should extend appropriate basic page:
- WebPage - if page works with BrowserDriver
- DesktopPage - if page works with DesktopDriver
- ScreenPage - if page works with ScreenDriver
- SapPage - if page works with SapDriver
In our example LoginPage should extends WebPage as it describe the page of "Invoice Plane" web application:
import eu.ibagroup.easyrpa.engine.model.SecretCredentials; import eu.ibagroup.easyrpa.engine.rpa.page.WebPage; public class LoginPage extends WebPage { public Dashboard login(SecretCredentials invoicePlaneCredentials) { ... return createPage(Dashboard.class); } }
Selector-annotations
@FindBy annotation
All drivers have different limitation to use selector-annotations attributes. Use the compatibility matrix bellow to check which attributes can be used for @FindBy annotation:
BrowserDriver | DesktopDriver | JavaDriver | SapDriver | ScreenDriver | |
---|---|---|---|---|---|
id | ✔ | ✔ | ✔ | ✔ | ✘ |
name | ✔ | ✔ | ✔ | ✔ | ✘ |
className | ✔ | ✔ | ✔ | ✘ | ✘ |
css | ✔ | ✘ | ✔ | ✘ | ✘ |
tagName | ✔ | ✔ | ✔ | ✘ | ✘ |
linkText | ✔ | ✘ | ✔ | ✘ | ✘ |
partialLinkText | ✔ | ✘ | ✔ | ✘ | ✘ |
xpath | ✔ | ✘ | ✔ | ✘ | ✘ |
image | ✘ | ✘ | ✘ | ✘ | ✔ |
nameRegexPattern | ✘ | ✔ | ✘ | ✘ | ✘ |
localizedControlType | ✘ | ✔ | ✘ | ✘ | ✘ |
controlType | ✘ | ✔ | ✘ | ✘ | ✘ |
row | ✘ | ✔ | ✘ | ✘ | ✘ |
column | ✘ | ✔ | ✘ | ✘ | ✘ |
treeScope | ✘ | ✔ | ✘ | ✘ | ✘ |
Wait annotations
Usually we use selector annotation (FindBy) together with wait-annotation.
Annotation @Wait contains tree attributes:
- value - to define a wait timeout (default is 5 sec)
- waitFunc - to define a wait type function
- required - to specify that must return a result
So usage examples can be the following:
import eu.ibagroup.easyrpa.engine.rpa.element.BrowserElement; import eu.ibagroup.easyrpa.engine.rpa.page.WebPage; import eu.ibagroup.easyrpa.engine.rpa.po.annotation.FindBy; public class MyPage extends WebPage { @FindBy(id = "email") private BrowserElement email; @FindBy(xpath = "//*[@id='ip-navbar-collapse']//li/a[text()='Dashboard']") private BrowserElement dashboardMenu; ... }
OR with a Wait function:
import eu.ibagroup.easyrpa.engine.rpa.page.WebPage; import eu.ibagroup.easyrpa.engine.rpa.po.annotation.Wait; import eu.ibagroup.easyrpa.engine.rpa.element.Browserlement; import eu.ibagroup.easyrpa.engine.rpa.po.annotation.FindBy; public class MyPage extends WebPage { @FindBy(css = "input#invoice_discount_percent") @Wait(value = 20, waitFunc = Wait.WaitFunc.VISIBLE) private BrowserElement invoiceDiscountPercent; @FindBy(xpath = "//div[@class='alert alert-danger']") @Wait(value = 5, required = false) private List<BrowserElement> addFailed; ... }
Use of Components inside Pages and other Components
Component abstraction
To further generalize and encapsulate some common UI behavior into a java class a Component abstraction can be used. Here are the basic ideas behind abstraction:
- A component always initialized with single 'wrapped' UI element available by 'getWrappedElement()' call
- Depending on the way component was declared, wrapped element can be cached or resolved each time dynamically
- All internal UI fields of a component are considered to be relative to the 'wrapped' one hence should follow relative locators rules (e.g. @FindBy(xpath = ".//tbody/tr") )
- Component's tree is recursively initialized with a page initialization
- A component class must extends Component abstract class
- A component can be specified inside either Page or another Component
Implementation Example
In the TableComponent implementation example we see UI abstraction of a web table, wrapping <table> element and providing methods to work with the internal <tr> row elements:
import eu.ibagroup.easyrpa.engine.rpa.element.BrowserElement; import eu.ibagroup.easyrpa.engine.rpa.page.Component; import eu.ibagroup.easyrpa.engine.rpa.po.annotation.FindBy; import eu.ibagroup.easyrpa.engine.rpa.po.annotation.Wait; import eu.ibagroup.easyrpa.system.invoiceplane.to.BaseTO; public class TableComponent extends Component<BrowserElement> { @FindBy(xpath = ".//tbody/tr") @Wait(required = false) private List<BrowserElement> items; public int getRowsCount() { return items.size(); } public <T extends BaseTO<?>> List<T> getRowsAsTO(Function<Document, T> toTO) { List<T> result = new ArrayList<>(); if (items != null) { result = items.stream().map(this::asDocument).map(toTO).collect(toList()); } return result; } ... }
Component use
Component can be used exactly same way as any regular ui element field - declared in a page or component class together with selector annotations.
Below is ListPage example declaring and using TableComponent on a page:
import eu.ibagroup.easyrpa.engine.rpa.element.BrowserElement; import eu.ibagroup.easyrpa.engine.rpa.locator.BrowserSearch; import eu.ibagroup.easyrpa.engine.rpa.po.annotation.FindBy; import eu.ibagroup.easyrpa.engine.rpa.po.annotation.Wait; import eu.ibagroup.easyrpa.system.invoiceplane.to.BaseTO; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class ListPage<T extends BaseTO<?>, P extends NavigationBar> extends NavigationBar { @Getter @FindBy(xpath = "//div[@id='content']//table") @Wait(required = false) private TableComponent itemsTable; ... public List<T> getAllFromPage(String filterText) { setFilter(filterText); return itemsTable.getRowsAsTO(this::createFromRow); } ... }
Complete Page object example
Using the correct class structures and selector-annotations, complete LoginPage.java class for web application may looks like the following:
package eu.ibagroup.easyrpa.system.invoiceplane.page; import eu.ibagroup.easyrpa.engine.annotation.AfterInit; import eu.ibagroup.easyrpa.engine.exception.RpaException; import eu.ibagroup.easyrpa.engine.model.SecretCredentials; import eu.ibagroup.easyrpa.engine.rpa.element.BrowserElement; import eu.ibagroup.easyrpa.engine.rpa.locator.BrowserSearch; import eu.ibagroup.easyrpa.engine.rpa.page.WebPage; import eu.ibagroup.easyrpa.engine.rpa.po.annotation.FindBy; import eu.ibagroup.easyrpa.engine.rpa.po.annotation.Wait; import eu.ibagroup.easyrpa.system.invoiceplane.exception.InvoicePlaneError; import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.TimeoutException; import static eu.ibagroup.easyrpa.engine.rpa.locator.BrowserSearch.ExpectedConditions.or; import static eu.ibagroup.easyrpa.engine.rpa.locator.BrowserSearch.ExpectedConditions.presenceOfElementLocated; import static eu.ibagroup.easyrpa.engine.rpa.locator.BrowserSearch.ExpectedConditions.visibilityOfElementLocated; @Slf4j public class LoginPage extends WebPage { @FindBy(id = "email") @Wait(waitFunc = Wait.WaitFunc.CLICKABLE) private BrowserElement email; @FindBy(id = "password") @Wait(waitFunc = Wait.WaitFunc.CLICKABLE) private BrowserElement password; @FindBy(xpath = "//button[@type='submit']") @Wait(waitFunc = Wait.WaitFunc.CLICKABLE) private BrowserElement submit; @AfterInit public void init() { getElement(visibilityOfElementLocated(BrowserSearch.cssSelector(".container #login")), 20); } public Dashboard login(SecretCredentials invoicePlaneCredentials) { email.click(); email.clear(); email.sendKeys(invoicePlaneCredentials.getUser()); password.click(); password.clear(); password.sendKeys(invoicePlaneCredentials.getPassword()); submit.click(); try { waitFor(or(presenceOfElementLocated(BrowserSearch.cssSelector("div#main-area")), presenceOfElementLocated(BrowserSearch.cssSelector("#login .alert"))), 20); boolean loginFailedAppeared = getDriver().findElements(BrowserSearch.cssSelector("#login .alert")).size() > 0; if (loginFailedAppeared) { throw new RpaException(InvoicePlaneError.LOGIN_FAILED, invoicePlaneCredentials.getUser()); } } catch (TimeoutException e) { log.debug("Unknown error during InvoiceTO Place authorisation process."); throw new RuntimeException(); } return createPage(Dashboard.class); } }
So LoginPage has only "login" method which accept the secret credentials. After it clicks button - there're 2 possible elements can be appeared: for success login flow and for failed login. So in the example above we create a wait condition as the combination of 2 possible conditions for different selectors. If login has done successfully - LoginPage returns DashboardPage.
Use your page-objects inside tasks (business-logic level)
After the page-objects structure is created, in your task you can easily start using it like the following:
InvoicePlaneApplication invoicePlaneApplication = new InvoicePlaneApplication(browserDriver); LoginPage loginPage = invoicePlaneApplication.open(invoicePlaneUrl); Dashboard dashboard = loginPage.login(invoicePlaneCredentials); ProductsPage productsPage = dashboard.openProducts(); List<ProductTO> productsFromPage = productsPage.getProducts();
Additional rules
There is a lot of flexibility in how the page objects may be designed, but there are a few basic rules for getting the desired maintainability of your automation code.
- Page objects themselves should never contains any business specific logic. This is part of your automation scripts and should always be within the scripts’s code, never in an page object. The page object will contain the representation of the page, and the services the page provides via methods but no code related to what is being automated should be within the page object.
- A page object does not necessarily need to represent all the parts of a page itself. The same principles used for page objects can be used to create “Page Component Objects” that represent discrete chunks of the page and can be included in page objects. These component objects can provide references the elements inside those discrete chunks, and methods to leverage the functionality provided by them. You can even nest component objects inside other component objects for more complex pages. If a page in the application you automate has multiple components, or common components used throughout the site (e.g. a navigation bar), then it may improve maintainability and reduce code duplication.