Protractor Page Objects

Protractor Page objects provide a helpful way to organize your end-to-end test code. While your actual tests take actions and make assertions about how your application should respond, page objects encapsulate the details of how to perform those actions on a page.


var LoginPage = function() {
  this.userInput = element(by.model('username'));
  this.passwordInput = element(by.model('password'));
  this.loginButton = element(by.css('.app-login'));

  this.get = function() {
    browser.get('#/login');
  };

  this.login = function(username, password) {
    this.userInput.sendKeys(username);
    this.passwordInput.sendKeys(password);
    this.loginButton.click();
  };
};

describe('login', function() {
  it('welcomes the user', function() {
    var loginPage = new LoginPage();
    loginPage.get();
    loginPage.login('jsmith', 'Pa$$word77')

    // The rest of the test
  });
});

The loginPage object provides an interface for tests to interact with the application’s login page. This makes it easy for different tests to interact with the page without duplicating code.

Partial Page Objects

asdf
Tables are good candidates for partial page objects

As our applications grow complex, our pages tend to contain duplicated structure. For instance, the above table of users has the same first name, last name, and username structure on each of its rows. To exploit this duplicated structure, we’d like to write test code that looks like this:

expect(infoPage.getUser(2).lastName.getText()).toEqual(‘Bedelia’);

The new part is the getUser method—instead of a protractor element, this method returns another page object. To achieve this, we need to pass a parent element into the UserPartial constructor that will “wrap” all of the partial page’s elements:


function UserPartial(parent) {
  this.parent = parent;
  this.firstName = this.parent.element(By.css('.app-first-name'));
  this.lastName = this.parent.element(By.css('.app-last-name'));
  this.username = this.parent.element(By.css('.app-username'));
};

function InfoPage() {
  this.users = element.all(By.css('.app-user'));
};

InfoPage.prototype.getUser = function(i) {
  return new UserPartial(this.users.get(i));
};

Complications

This pattern would still work if we defined thirdLastName = infoPage.getUser(2).lastName before the page was even shown at all. Later on when we checked thirdLastName.getText(), it would find the last name. This is because the promise that element() returns isn’t resolved until getText is called.

It’s easy to accidentally write code that resolves these promises too early. Consider this change to the UserPartial constructor:


function UserPartial(parent) {
  this.parent = parent;
  // . . .
  this.userId = this.parent.getAttribute(‘data-id’);
};

Because getAttribute resolves the parent element’s promise immediately, this code will fail if the UserPartial is instantiated too early. We could try to keep this in mind and be careful when we instantiate our page objects, but it would give us more flexibility to define userId differently. We could also simply switch from a property to a function, but that would require us to access the attribute differently. We can achieve the late-binding quality of a function and still retain the property interface if we use Javascript getters:


get userId() {
  return this.parent.getAttribute(‘data-id’);
}

Getter syntax is not very browser-compatible. And normally that wouldn’t be much of an issue since this is test-only code, but it also means it’s unsupported by CoffeeScript. If a CoffeeScript solution is what you’re looking for, I recommend using Object.defineProperties as a workaround:


class UserPartial
  constructor: (@parent) ->

  Object.defineProperties @prototype,
    userId:
       get: -> @parent.getAttribute(‘data-id')
Conversation
  • grace says:

    Hi, Ben:
    I am new to Protractor. I am interested in the idea of “instead of a protractor element, this method returns another page object”. I only found your post having this idea. I am trying to do the similar thing for my project.I donot familar with coffeescript and wonder whether you can convert your code to Typescript. thanks.

  • Comments are closed.