The most successful test design pattern is by far the Page Object pattern for enhancing test maintenance and reducing code duplication. A page object is an object-oriented class that serves as an interface to a page of your automation project. The tests 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 tests 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.1
NEW RESEARCH: LEARN HOW DECISION-MAKERS ARE PRIORITIZING DIGITAL INITIATIVES IN 2024.
WebdriverIO – Page Object advanced concepts
Webdriver also provides support for the Page Object pattern. The goal is to abstract any page information away from the actual tests.
Main Page Object
You need to declare a main page object that will contain all the general selectors and functions. This will be inherited by all the other page objects.
Below is an ES6 example of a root page object:
"use strict"; class Page { constructor() { } open(path) { browser.url('/' + path); } } module.exports = Page;
Pages into Page Objects
Now that you have the main page object you can start creating the rest of the objects. Let’s use WebdriverIO as the app under test for our example.
First let’s create a page object for WebdriverIO home page. This is how the page might look:
let Page = require('./page'); class HomePage extends Page { get search() { return browser.element('//div[@id="docsearch"]//input'); } get searchResult() {return browser.element('//div[@class="algolia-docsearch-suggestion--wrapper"]'); } get title() { return browser.element('//header/h1'); } get description() { return browser.element('//header/h2'); } open() { super.open(''); } doSearch(searchCriteria) { this.search.setValue(searchCriteria); browser.pause(3000); } goToSearchResult() { this.searchResult.click(); } getTitle() { this.title.getText(); } getDescription() { this.description.getText(); } } module.exports = new HomePage();
Let’s create the second page object for guide page. This is how the page might look:
let Page = require('./page'); class GuidePage extends Page { get search() { return browser.element('//div[@id="docsearch"]//input'); } get searchResult() {return browser.element('//div[@class="algolia-docsearch-suggestion--wrapper"]'); } open() { super.open('guide.html'); } doSearch(searchCriteria) { this.search.setValue(searchCriteria); browser.pause(3000); } goToSearchResult() { this.searchResult.click(); } } module.exports = new GuidePage();
Identify the problem
Everything seems to look great and we’ve successfully implemented the Page Object pattern across our test project, but we may have missed something. We have some duplication of code due to the presence of the same navbar on both Home and Guide pages.
These kinds of situations occur frequently in real life testing so we need a solution. My suggestion is to uncouple the common section into separate Page Objects that we can also call Page Components and use Multiple Inheritance.
The solution – Page Components and Multiple Inheritance
There are a lot of topics and approaches on ES6 Multiple Inheritance. I propose using the xmultiple npm package to handle mixins.
Refactoring the above code gives us the following
"use strict"; class Page { constructor() { } open(path) { browser.url('/' + path); } } module.exports = Page; "use strict"; class NavigationBarSection { constructor() { } get search() { return browser.element('//div[@id="docsearch"]//input'); } get searchResult() {return browser.element('//div[@class="algolia-docsearch-suggestion--wrapper"]'); } doSearch(searchCriteria) { this.search.setValue(searchCriteria); browser.pause(3000); } goToSearchResult() { this.searchResult.click(); } } module.exports = NavigationBarSection; "use strict"; let mixin = require('xmultiple'); let Page = require('./page'); let NavigationBar = require('./sections/navigationBar.section'); class HomePage extends mixin(Page, NavigationBar) { get title() { return browser.element('//header/h1'); } get description() { return browser.element('//header/h2'); } open() { super.open(''); } getTitle() { this.title.getText(); } getDescription() { this.description.getText(); } } module.exports = new HomePage(); "use strict"; let mixin = require('xmultiple'); let Page = require('./page'); let NavigationBar = require('./sections/navigationBar.section'); class GuidePage extends mixin(Page, NavigationBar) { open() { super.open('guide.html'); } } module.exports = new GuidePage();
What we did was to extract the common section into a different class. After this I used the multiple inheritance capability of the xmultiple library to provide my new objects with the necessary implementation.
Now we have clean code to handle our testing.
Conclusion
It seems easy at the beginning to implement the Page Object pattern into a WebdriverIO test project. However, a closer look will reveal the complexity of the problem and the necessity of applying different approaches according to the situation.
There are many ways to handle multiple inheritance in your WebdriverIO project. If this does not satisfy your needs you can still use the JavaScript Subclass Factories approach. Regardless of the approach used, the key to a successful automation project is to keep your code clean, and more importantly, to keep your page objects cleaner.
Here you can find the full working example used in this article.
References:
- Source: http://docs.seleniumhq.org/docs/06_test_design_considerations.jsp#page-object-design-pattern ↩
Sergiu Popescu
Related Posts
-
Using the Page Object Design Pattern in Sencha Test
Don’t Repeat Yourself (DRY) is a software development principle that is equally important to automation…
-
Using the Page Object Design Pattern in Sencha Test
Don’t Repeat Yourself (DRY) is a software development principle that is equally important to automation…