Protractor style guide

This style guide was originally created by Carmen Popoviciu and Andres Dominguez. It is based on Carmen's Protractor style guide and Google's Protractor style guide.

🔗Video

Carmen and Andres gave a talk about this style guide at AngularConnect in London. Here's the video in case you want to watch it.

Protractor styleguide @AngularConnect

🔗Table of contents

Test suites

Don't e2e test what’s been unit tested

Why?

Make your tests independent at least at the file level

Do not add logic to your test

Don't mock unless you need to

This rule is a bit controversial, in the sense that opinions are very divided when it comes to what the best practice is. Some developers argue that e2e tests should use mocks for everything in order to avoid external network calls and have a second set of integration tests to test the APIs and database. Other developers argue that e2e tests should operate on the entire system and be as close to the 'real deal' as possible.

Why?

Use Jasmine2

Why?

Make your tests independent from each other

This rule holds true unless the operations performed to initialize the state of the tests are too expensive. For example, if your e2e tests would require that you create a new user before each spec is executed, you might end up with too high test run times. However, this does not mean you should make tests directly depend on one another. So, instead of creating a user in one of your tests and expect that record to be there for all other subsequent tests, you could harvest the power of jasmine's beforeAll (since Jasmine 2.1) to create the user.

/* avoid */

it('should create user', function() {
   browser.get('#/user-list');
   userList.newButton.click();

   userProperties.name.sendKeys('Teddy B');
   userProperties.saveButton.click();

   browser.get('#/user-list');
   userList.search('Teddy B');
   expect(userList.getNames()).toEqual(['Teddy B']);
});

it('should update user', function() {
   browser.get('#/user-list');
   userList.clickOn('Teddy B');

   userProperties.name.clear().sendKeys('Teddy C');
   userProperties.saveButton.click();

   browser.get('#/user-list');
   userList.search('Teddy C');
   expect(userList.getNames()).toEqual(['Teddy C']);
});
/* recommended */

describe('when the user Teddy B is created', function(){

  beforeAll(function() { 
    browser.get('#/user-list'); 
    userList.newButton.click(); 

    userProperties.name.sendKeys('Teddy B'); 
    userProperties.saveButton.click(); 
    browser.get('#/user-list'); 
  });

  it('should exist', function() { 
    userList.search('Teddy B'); 
    expect(userList.getNames()).toEqual(['Teddy B']); 
    userList.clear(); 
  });

  describe('and gets updated to Teddy C', function() {
    beforeAll(function() { 
      userList.clickOn('Teddy B'); 
      userProperties.name.clear().sendKeys('Teddy C'); 
      userProperties.saveButton.click(); 

      browser.get('#/user-list'); 
    }); 

    it('should be Teddy C', function() { 
      userList.search('Teddy C'); 
      expect(userList.getNames()).toEqual(['Teddy C']); 
      userList.clear(); 
    }); 
  });
});

Why?

Why?

Have a suite that navigates through the major routes of the app

Why?

Locator strategies

NEVER use xpath

Why?

/* avoid */

var elem = element(by.xpath('/*/p[2]/b[2]/following-sibling::node()' +
 '[count(.|/*/p[2]/b[2]/following-sibling::br[1]/preceding-sibling::node())' +
 '=' +
 ' count((/*/p[2]/b[2]/following-sibling::br[1]/preceding-sibling::node()))' +
 ']'));

Prefer protractor locator strategies when possible

<ul class="red">
  <li>{{color.name}}</li>
  <li>{{color.shade}}</li>
  <li>{{color.code}}</li>
</ul>

<div class="details">
  <div class="personal">
    <input ng-model="person.name">
  </div>
</div>
/* avoid */

var nameElement = element.all(by.css('.red li')).get(0);
var personName = element(by.css('.details .personal input'));

/* recommended */

var nameElement = element(by.binding('color.name'));
var personName = element(by.model('person.name'));

Why?

Prefer by.id and by.css when no Protractor locators are available

Why?

Avoid text locators for text that changes frequently

Why?

Page objects

Page Objects help you write cleaner tests by encapsulating information about the elements on your application page. A page object can be reused across multiple tests, and if the template of your application changes, you only need to update the page object.

Use Page Objects to interact with page under test

Why?

/* avoid */

/* question-spec.js */
describe('Question page', function() {
  it('should answer any question', function() {
    var question = element(by.model('question.text'));
    var answer = element(by.binding('answer'));
    var button = element(by.css('.question-button'));

    question.sendKeys('What is the purpose of life?');
    button.click();
    expect(answer.getText()).toEqual("Chocolate!");
  });
});
/* recommended */

/* question-spec.js */
var QuestionPage = require('./question-page');

describe('Question page', function() {
  var question = new QuestionPage();

  it('should ask any question', function() {
    question.ask('What is the purpose of meaning?');
    expect(question.answer.getText()).toEqual('Chocolate');
  });
});

/* recommended */

/* question-page.js */
var QuestionPage = function() {
  this.question = element(by.model('question.text'));
  this.answer = element(by.binding('answer'));
  this.button = element(by.className('question-button'));

  this.ask = function(question) {
    this.question.sendKeys(question);
    this.button.click();
  };
};

module.exports = QuestionPage;

Declare one page object per file

Why?

Use a single module.exports at the end of the page object file

Why?

/* avoid */

var UserProfilePage = function() {};
var UserSettingsPage = function() {};

module.exports = UserPropertiesPage;
module.exports = UserSettingsPage;
/* recommended */

/** @constructor */
var UserPropertiesPage = function() {};

module.exports = UserPropertiesPage;

Require all the modules at the top

var UserPage = require('./user-properties-page');
var MenuPage = require('./menu-page');
var FooterPage = require('./footer-page');

describe('User properties page', function() {
    ...
});

Why?

Instantiate all page objects at the beginning of the test suite

var UserPropertiesPage = require('./user-properties-page');
var MenuPage = require('./menu-page');
var FooterPage = require('./footer-page');

describe('User properties page', function() {
  var userProperties = new UserPropertiesPage();
  var menu = new MenuPage();
  var footer = new FooterPage();

  // specs
});

Why?

Declare all the page object public elements in the constructor

<form>
  Name: <input type="text" ng-model="ctrl.user.name">
  E-mail: <input type="text" ng-model="ctrl.user.email">
  <button id="save-button">Save</button>
</form>
/** @constructor */
var UserPropertiesPage = function() {
  // List all public elements here.
  this.name = element(by.model('ctrl.user.name'));
  this.email = element(by.model('ctrl.user.email'));
  this.saveButton = $('#save-button');
};

Why?

Declare page object functions for operations that require more than one step

/**
 * Page object for the user properties view.
 * @constructor
 */
var UserPropertiesPage = function() {
  this.newPhoneButton = $('button.new-phone');

  /**
   * Encapsulate complex operations in a function.
   * @param {string} phone Phone number.
   * @param {string} contactType Phone type (work, home, etc.).
   */
  this.addContactPhone = function(phone, contactType) {
    this.newPhoneButton.click();
    $$('#phone-list .phone-row').first().then(function(row) {
      row.element(by.model('item.phoneNumber')).sendKeys(phone);
      row.element(by.model('item.contactType')).sendKeys(contactType);
    });
  };
};

Why?

Avoid using expect() in page objects

Why?

Add page object wrappers for directives, dialogs, and common elements

For example, the Protractor website has navigation bar with multiple dropdown menus. Each menu has multiple options. A page object for the menu would look like this:

/**
 * Page object for Protractor website menu.
 * @constructor
 */
var MenuPage = function() {
  this.dropdown = function(dropdownName) {
    /**
     * Dropdown api. Used to click on an element under a dropdown.
     * @param {string} dropdownName
     * @return {{option: Function}}
     */
    var openDropdown = function() {
      element(by.css('.navbar-nav'))
          .element(by.linkText(dropdownName))
          .click();
    };

    return {
      /**
       * Get an option element under a dropdown.
       * @param {string} optionName
       * @return {ElementFinder}
       */
      option: function(optionName) {
        openDropdown();
        return element(by.css('.dropdown.open'))
            .element(by.linkText(optionName));
      }
    }
  };
};

module.exports = MenuPage;
var Menu = require('./menu');

describe('protractor website', function() {

  var menu = new Menu();

  it('should navigate to API view', function() {
    browser.get('http://www.protractortest.org/#/');

    menu.dropdown('Reference').option('Protractor API').click();

    expect(browser.getCurrentUrl())
        .toBe('http://www.protractortest.org/#/api');
  });
});

Why?

Project structure

Group your e2e tests in a structure that makes sense to the structure of your project

Why?

/* avoid */

|-- project-folder
  |-- app
    |-- css
    |-- img
    |-- partials
        home.html
        profile.html
        contacts.html
    |-- js
      |-- controllers
      |-- directives
      |-- services
      app.js
      ...
    index.html
  |-- test
    |-- unit
    |-- e2e
        home-page.js
        home-spec.js
        profile-page.js
        profile-spec.js
        contacts-page.js
        contacts-spec.js

/* recommended */

|-- project-folder
  |-- app
    |-- css
    |-- img
    |-- partials
        home.html
        profile.html
        contacts.html
    |-- js
      |-- controllers
      |-- directives
      |-- services
      app.js
      ...
    index.html
  |-- test
    |-- unit
    |-- e2e
      |-- page-objects
          home-page.js
          profile-page.js
          contacts-page.js
      home-spec.js
      profile-spec.js
      contacts-spec.js