Protractor: Using the Page Object Model

What is Protractor?

Protractor is an end-to-end (e2e) test automation framework for AngularJS application. It is an open source Node.js program built on top of WebDriverJS originally developed by a team at Google. Test cases written in Protractor run in the browser simulating the actions of a real user. An e2e test written in Protractor makes sure your application behaves as expected.

Challenge: Code Duplication

There is always duplication in test cases. For instance login, find, and logout are clearly duplicated in the following two test cases:

Test case 1: login to the website, find an item, add it to my wish list and logout.

Test case 2: login to the website, find an item, add it to cart, purchase and logout.

Duplicate test cases result in code duplication. An e2e test suite with code duplication is difficult to maintain and requires costly modifications. In this tutorial, we will implement a page object design best practice for Protractor to minimize code duplication, make tests more readable, reduce the cost of modification, and improve maintainability.  

The most important concept here is to separate the abstraction of the test object (the page) and the test script (the spec). Hence, a single test object can be used multiple times by test scripts without rewriting it.

Using the PhoneCat application

We will use the popular AngularJS PhoneCat application to demonstrate how Protractor tests could make use of the page object design pattern to create simple and maintainable e2e test automation.

A concise instruction set, on how to setup the PhoneCat application in your local machine, is at the end of this post.

Abstraction: Separation of Test Object from Test Script

The PhoneCat app has the ‘phones list view’ page where all available phones are listed. A user can search or change the order of the listed phones on the page. When selecting a phone from the list, a user navigates to the ‘phone details view’ page, where more details about the selected phone are included.

In line with the page object design pattern best practice: the PhoneCat application has two test objects, the phones list view page and the phone details view page. Each of the pages should be self-contained, meaning they should provide all the locators and functions required to interact with each page. For example, the phones list view page should have a locator for the search input box and a function to search.

The image below shows the separation of the test object (page object files) from the test script (spec files). The spec files under the spec folder contain only test scripts. The page object files under the page object folder contain page specific locators and functions.

Figure 1: Separation of page object from test specification

Test Object (Page Object)

The PhoneCat application have the phones list page and the phone details page. The following two page object files provide the locators and functions required to interact with these pages. 

Phones = {

    elements: {

        _search: function () {

            return element(by.model('query'));

        },

 

        _sort: function(){

            return element(by.model('orderProp'));

        },

 

        _phoneList: function(){

            return element.all(by.repeater('phone in phones'));

        }

        _phoneNameColumn: function(){

            returnelement.all(by.repeater('phone in phones').column('phone.name'));

        }

    },

    _phonesCount: function(){

        return this.elements._phoneList().count();

    },

    searchFor: function(word){

        this.elements._search().sendKeys(word);

    },

    clearSearch: function(){

        this.elements._search().clear();

    },

    _getNames: function(){

        return this.elements._phoneNameColumn().map(function(elem){

            return elem.getText();

        });

    },

    sortItBy: function(type){

        this.elements._sort().element(by.css('option[value="' + type + '"]')).click();

    },

    selectFirstPhone : function(){

        element.all(by.css('.phones li a')).first().click();

        return require('./phone.details.page.js');

    }

};

module.exports = Phones;

Listing 1: phones.page.js

PhoneDetails = {

    elements:{

        _name: function(){

            return element(by.binding('phone.name'));

        },

        _image: function(){

            return element(by.css('img.phone.active'));

        },

        _thumbnail: function(index){

            return element(by.css('.phone-thumbs li:nth-child(' + index +') img'));

        }

    },

    _getName: function(){

        return this.elements._name().getText();

    },

    _getImage: function(){

        return this.elements._image().getAttribute('src');

    },

    clickThumbnail: function(index){

        this.elements._thumbnail(index).click();

    }

};

module.exports = PhoneDetails; 

Listing 2: phone.details.page

Test Script (Spec)

The test script can now make use of the page object files. All the functions required to interact with the page (the test object) are encapsulated in the page object and the test scripts are more readable and concise.

describe('Phone list view', function(){

    var phones = require('../page_objects/phones.page.js');

    beforeEach(function() {

        browser.get('app/index.html#/phones');

    })

    it('should filter the phone list as a user types into the search box', function() {

        expect(phones._phonesCount()).toBe(20);

        phones.searchFor('nexus');

        expect(phones._phonesCount()).toBe(1);

        phones.clearSearch();

        phones.searchFor('motorola');

        expect(phones._phonesCount()).toBe(8);

    });

    it('should be possible to control phone order via the drop down select box', function() {

        phones.clearSearch();

        phones.searchFor('tablet'); //let's narrow the dataset to make the test assertions shorter

        expect(phones._getNames()).toEqual([

            "Motorola XOOM\u2122 with Wi-Fi",

            "MOTOROLA XOOM\u2122"

        ]);

        phones.sortItBy('name');

        expect(phones._getNames()).toEqual([

            "MOTOROLA XOOM\u2122",

            "Motorola XOOM\u2122 with Wi-Fi"

        ]);

    });

    it('should render phone specific links', function() {

        phones.clearSearch();

        phones.searchFor('nexus');

        phones.selectFirstPhone();

        browser.getLocationAbsUrl().then(function(url) {

            expect(url.split('#')[1]).toBe('/phones/nexus-s');

        });

    });

});

Listing 3: phones.spec.js

describe('Phone detail view', function(){

    var phones = require('../page_objects/phones.page.js'),

        phoneDetails;

    beforeEach(function() {

        browser.get('app/index.html#/phnes');

        phones.searchFor('nexus');

        phoneDetails = phones.selectFirstPhone();

    });

    it('should display nexus-s page', function() {

        expect(phoneDetails._getName()).toBe('Nexus S');

    });

    it('should display the first phone image as the main phone image', function() {

        expect(phoneDetails._getImage()).toMatch(/img\/phones\/nexus-s.0.jpg/);

    });

    it('should swap main image if a thumbnail image is clicked on', function() {

        phoneDetails.clickThumbnail(3);

        expect(phoneDetails._getImage()).toMatch(/img\/phones\/nexus-s.2.jpg/);

        phoneDetails.clickThumbnail(1);

        expect(phoneDetails._getImage()).toMatch(/img\/phones\/nexus-s.0.jpg/);

    });

});

Listing 4: phone.details.spec.js

In conclusion, when a page object design pattern is properly used in Protractor test automation, it will make an e2e test easy to maintain and reduce code duplication.

Appendix

GitHub Repo: For This Tutorial

The following gitHub link for the PhoneCat tutorial and adopt it to this tutorial. It is basically a sample Protractor test (scenarios.js) of the PhoneCat app rewritten in a page object model.

https://github.com/xgirma/angular-phonecat.git

This could be a good starting point for discussion on the application of the page object model to improve the maintainability of Protractor tests.

Comparison

The following table shows the main use of the page object model, which is minimizing code duplication. The table compares a Protractor test included in the PhoneCat (scenario.js) and a Protractor test (phones.page.js, phone.details.page.js, phones.spec.js and phone.details.spec.js) which implements the same test case with the page object model. As shown in the table, even in this simple test, code duplication is enormous when implemented without the page object model. In contrast, code duplication when implemented with the page object model is very minimal.

 

Table 1: Comparison of code duplication with and with out page object model

 

 

PhoneCat app: the Setup

1.     Install Git and Node.js.

2.     Clone the angular-phonecat repository. ($ git clone --depth=14 https://github.com/angular/angular-phonecat.git)

3.     Change your current directory to angular-phonecat ($ cd angular-phonecat). Download the tool dependencies by running ($ npm install).

4.     Use npm helper scripts to start a local development web-server($ npm start). This will create a local webserver in your machine, listening to port 8000. Browse the application at http://localhost:8000/app/index.html

5.     To install the drivers needed by Protractor ($ npm run update-webdriver) and to run the Protractor end to end tests ($ npm run protractor).

Refer to the AngularJS site for complete instructions.

https://docs.angularjs.org/tutorial/step_00

Final note: If you want to try the code samples given in this tutorial,  besides creating folders, the page object files, and the spec files, you need to change the path to the the new spec files in protractor-cof.js file. Simply change spec: [‘e2e/*.js’] to spec:[‘e2e/spec/*.spec.js’] or to a path where you put the spec files.

Related Works

1.     Using Page Objects to Organize Tests https://github.com/angular/protractor/blob/master/docs/page-objects.md

2.     Using Page Objects to Overcome Protractor's Shortcomings http://www.thoughtworks.com/insights/blog/using-page-objects-overcome-protractors-shortcomings

3.     Getting Started with Protractor and Page Objects for AngularJS E2E Testing https://teamgaslight.com/blog/getting-started-with-protractor-and-page-objects-for-angularjs-e2e-testing