Nested Page Objects in Protractor

2.2k views Asked by At

The Question:

What is the canonical way to define nested Page Objects in Protractor?

Use Case:

We have a complicated page that consists of multiple parts: a filter panel, a grid, a summary part, a control panel on a side. Putting all the element and method definitions into a single file and a single page object does not work and scale - it is becoming a mess which is difficult to maintain.

3

There are 3 answers

2
alecxe On BEST ANSWER

The idea is define the Page Object as a package - directory with index.js as an entry point. The parent page object would act as a container for child page objects which in this case have a "part of a screen" meaning.

The parent page object would be defined inside the index.js and it would contain all the child page object definitions, for example:

var ChildPage1 = require("./page.child1.po"),
    ChildPage2 = require("./page.child2.po"),

var ParentPage = function () {
    // some elements and methods can be defined on this level as well
    this.someElement = element(by.id("someid"));

    // child page objects
    this.childPage1 = new ChildPage1(this);
    this.childPage2 = new ChildPage2(this);
}

module.exports = new ParentPage();

Note how this is passed into the child page object constructors. This might be needed if a child page object would need access to the parent page object's elements or methods.

The child Page Object would look like this:

var ChildPage1 = function (parent) {
    // element and method definitions here
    this.someOtherElement = element(by.id("someotherid"));
}

module.exports = ChildPage1;

Now, it would be quite convenient to use this kind of page object. You simply require the parent page object and use the dot notation to get access to the sub page objects:

var parentPage = requirePO("parent");

describe("Test Something", function () {
    it("should test something", function () {
        // accessing parent
        parentPage.someElement.click();

        // accessing nested page object
        parentPage.childPage1.someOtherElement.sendKeys("test");
    });
});

requirePO() is a helper function to ease imports.


Sample nested page object directory structure from one of our test automation projects:

enter image description here

5
Ram Pasala On

This is more of a general topic when it comes to Page Objects and how to maintain them. Sometime back I stumbled upon one of the Page Object Design Pattern techniques which I liked and made a lot of sense to me.

Rather than instantiating child page objects in the parent page objects, it would be ideal to follow javascript's Prototypal Inheritance concept. This has quite a number of benefits but first let me show how we can achieve it:

First we would create our parent page object ParentPage:

// parentPage.js
var ParentPage = function () {
// defining common elements
this.someElement = element(by.id("someid"));

// defining common methods
ParentPage.prototype.open = function (path) {
browser.get('/' + path)
}
}

module.exports = new ParentPage();  //export instance of this parent page object

We will always export an instance of a page object and never create that instance in the test. Since we are writing end to end tests we always see the page as a stateless construct the same way as each http request is a stateless construct.

Now let's create our child page objects ChildPage, we would use Object.create method to inherit the prototype of our parent page:

//childPage.js
var ParentPage = require('./parentPage')
var ChildPage = Object.create(ParentPage, {
/**
 * define elements
 */
username: { get: function () { return element(by.css('#username')); } },
password: { get: function () { return element(by.css('#password')); } },
form:     { get: function () { return element(by.css('#login')); } },
/**
 * define or overwrite parent page methods
 */
open: { value: function() {
    ParentPage.open.call(this, 'login'); // we are overriding parent page's open method
} },
submit: { value: function() {
    this.form.click();
} }
});
module.exports = ChildPage

we are defining locators in getter functions, These functions get evaluated when you actually access the property and not when you generate the object. With that you always request the element before you do an action on it.

The Object.create method returns an instance of that page so we can start using it right away.

// childPage.spec.js
var ChildPage = require('../pageobjects/childPage');
describe('login form', function () {
it('test user login', function () {
    ChildPage.open();
    ChildPage.username.sendKeys('foo');
    ChildPage.password.sendKeys('bar');
    ChildPage.submit();
});

Notice above that we are only requiring the child page object and utilizing/overriding parent page objects in our specs. Following are the benefits of this design pattern:

  • removes tight coupling between parent and child page objects
  • promotes inheritance between page objects
  • lazy loading of elements
  • encapsulation of methods and action
  • cleaner & much easier to understand the elements relationship instead of parentPage.childPage.someElement.click();

I found this design pattern in webdriverIO's developer guide, most of the above methods I explained are taken from that guide. Feel free to explore it and let me know your thoughts!

4
Linh Nguyen On

I don't use Protractor, but maybe you can try the idea below - at least, it's been working well for me so far:

I use something you can call "Component Object" - I divide a page into components or parts, and suppose I am given the scope of each component, I search and add elements to the components based on their scopes. This way, I can easily reuse the same/similar components in different pages.

For example, with the page http://google.com, I divide it into 3 parts: enter image description here Let's say we will name those 3 parts as: Header, SearchForm, Footer

The code for each part will be something like this:

class Header {
   public Header(SearchContext context){
       _context = context;
   }

   WebElement GmailLink {
       get {
           return _context.FindElement(By.CssSelector("[data-pid='23']"));
       }
   }
   WebElement ImagesLink {
       get {
           return _context.FindElement(By.CssSelector("[data-pid='2']"));
       }
   } 

   SearchContext _context;
}

class SearchForm{
   public Header(SearchContext context){
       _context = context;
   }

   WebElement SearchTextBox {
       get {
           return _context.FindElement(By.Name("q")):
       }
   }

   WebElement SearchButton {
       get {
           return _context.FindElement(By.Name("btnK")):
       }
   }

   SearchContext _context;
}
..

And the code for the page google.com will be like:

class GoogleComPage{
   WebDriver _driver;
   public GoogleCompage(driver){
       _driver = driver;
   }
   public Header Header{
       get {
           return new Header(_driver.FindElement(By.Id("gb")));
       }
   }

   public SearchForm SearchForm{
       get {
           return new SearchForm(_driver.FindElement(By.Id("tsf")));
       }
   }
}