Separating concerns with the Page Object Model

In the previous post we put together a simple test using WebdriverIO, Mocha, and Chai. All aspects of testing were in the same place: what to do, how to do it, and how to know whether you succeeded. In this post we’re going to peel “how to do it” apart from the other two, via a design pattern called the Page Object Model.

Why separate? In the case of one simple test the benefits are not clear, but imagine half a dozen tests interacting with the same few pages, or with pages that share components. Then imagine something about those pages changes – selectors, titles. With POM you make each page-related update in one place and it carries over to all of the tests, rather than needing to change each test individually.

In this post we will POM-ify the test from the previous post; this will allow the same test code to work with three different sets of page objects (two for here and one for my craft blog; we’ll only go through part, but the full code is in my WDIO examples repository). Refresher: the test opens the blog archive, searches for “about”, and from the results visits the About page.

Page Objects

A Page Object is, simply, an object that holds the code for interacting with a page. Anything that requires knowing details about the page’s implementation should be hidden in the Page Object: the URL and title (they change sometimes!), CSS and XPath selectors for elements, the exact text of response messages for actions, whether you should be using selectByValue or selectByVisibleText for a form, etc. Essentially you are aliasing page elements and simple interactions to names that will stay the same even if the selectors for elements and details of interacting change.

This example is not going to look like most others online. Page Object Model tutorials generally give you a “base” page object that everything else inherits from, but which frequently doesn’t provide any functionality of its own (this could be the minimality of typical online examples, though). They also tend to use constructors and worry about prototypes – we’ll do inheritance in a different way, below. Our POM-ified example test has more of a “base chunk,” a plain old object that holds relevant functions shared by every non-front page on the site:

// rw-interior-page.js

const IDs = require('../helpers/identifiers.js');

module.exports = {
    searchInput: function () {
        return $('input.search-field');
    },
    searchSubmit: function () {
        return $('input.search-submit');
    },
    findSearch: function() {
        if (IDs.isAndroid()) {
            browser.execute("mobile: scroll", {direction: 'down'});
        }
    }
};

Other pages copy in this functionality and add their own:

// rw-search.js

var InteriorPage = require('./rw-interior-page.js');

module.exports = {
    titlePrefix: function() { 
        return "You searched for ";
    },
    resultTitles: function() {
        return browser.getText('article h2');
    },
    resultLink: function(url) {
        return 'article h2 a[href="' + url + '"]';
    }
};

// add any contents of InteriorPage that haven't been redefined

for (var prop in InteriorPage) { 
    (module.exports[prop] = module.exports[prop])
     || (module.exports[prop] = InteriorPage[prop]);
}

This method of “inheritance” lends itself well to multiple inheritance: two require statements, two for loops. In my real-life use of browser automation, I was working with checkout. The Payment page had some UX functionality in common with Billing/Shipping/Confirmation, and functionality to accept and apply promotional codes in common with Cart. After taking a wicked deep dive down the rabbit hole of JavaScript object inheritance I realized it wasn’t going to get any better than require/for. It’s just raw inclusion of one object’s properties and methods in another, but it’s enough. There is no reason to worry about the prototype chain because you are not in need of an ongoing inheritance relationship between your page objects.

In my larger test bank I was careful about consistent naming/grammar for my object methods, and about declaring variables to hold strings or elements that were used in multiple methods. The whole point is to make the number of changes a page update would require as close to 1 as possible.

For the “JS classical inheritance” version of Page Objects, see WebdriverIO’s page of documentation on the Page Object Pattern plus a little more sample code linked from there. I refer to that as “WDIO-idiomatic POM”. It has the aforementioned “base page,” which is defined as a constructor function, additions to the prototype of that constructor, and exporting of a new instance of the constructor. The child pages then include that base page via Object.create (or an ES6 extends statement). If your base page is an object defined by putting properties and methods into curly braces, rather than by constructor/prototype, the child pages will not be able to inherit via Object.create.

There is a version of this test in the repository that uses WDIO-idiomatic POM, called test/specs/wdio-rw.js and using pages in test/pages-2/. I prefer the purely module-based code, however; the Object.create version is more verbose and does not lend itself cleanly to multiple inheritance.

Logic-Only Test Code

Now that the page interaction is hidden behind aliases, the test code is slimmed down:

const Blog = require('../pages/rw-blog.js');
const Search = require('../pages/rw-search.js');
const About = require('../pages/rw-about.js');

describe('Finding About from Blog via Search', function() {

    it('should open the blog page', function() {
        Blog.open();
        expect(browser.getTitle()).to.equal(Blog.title());
    });

    it('should find the search bar', function() {
        Blog.findSearch();
        expect(Blog.searchInput().isVisible()).to.be.true;
    });

    it('should search for about', function() {
        Blog.searchInput().setValue('about');
        Blog.searchSubmit().click();
        expect(browser.getTitle()).to.contain(Search.titlePrefix() + 'about');
    });

    it('should see About in the search results', function() {
        expect(Search.resultTitles()).to.contain(About.pageName());
    });

    it('should reach the About page', function() {
        browser.click(Search.resultLink(About.url()));
        expect(browser.getTitle()).to.equal(About.title());
    });

});

The test itself no longer “knows” what the page titles are, or the HTML identity of the search input and submit fields, or what needs to be done to make the search bar visible. This exact test code, with the require statements modified to call the correct set of page objects, works with page objects as described above, page objects written in a more WDIO-idiomatic way, and a set of page objects I wrote to perform the same test on my craft blog.

All three sets of pages and their tests are in the example code repository. This concludes our brief introduction to browser automation via WebdriverIO, but there will be occasional additions in the future!


Socket wrench photo by StockSnap on Pixabay.

Leave a Reply

Your email address will not be published. Required fields are marked *