Friday, May 9, 2014

E2E Test for AngularJS without Protractor

Alternative way to perform E2E test for AngularJS application without Protractor.


Setup

Back to TOC
1. Install global npm modules. This is to ensure our $PATH can find them.
npm install --save-dev -g selenium-standalone mocha

Module Description
selenium-standalone Community-contributed which easily download and install Selenium standalone server.
mocha Test runner framework.

2. Install webdriver.io’s webdriverjs module. This is not the same as selenium-webdriver although its rather confusing as both vendor use the same term “webdriverjs” to desribe their driver. Checkout the webdriver.io website for API documentation.
npm install --save-dev webdriverjs


Selenium Standalone Server

Back to TOC
Run the command to start the server in background.

start-selenium &

Test script

Back to TOC
Simple test script to use the default chrome driver and check the title and take a screenshot of homepage.

Test site is AngularJS official site.

We will test the title of the page.
1. Make a test folder. mkdir test
2. Create a test file index.js in test folder. Use the code below.

//test/index.js
var
webdriverjs = require('webdriverjs'), //Refere to http://webdriver.io for more info
    assert = require('assert');

describe('E2E testing AngularJS without Protractor - ', function() {
    this.timeout(99999999);
    var client = {};
    var siteUrl = 'http://www.angularjs.org';

    before(function() {
        client = webdriverjs.remote({
            desiredCapabilities: {
                browserName: 'chrome'
            }
        });
        client.init();
    });

    after(function(done) {
        client.end(done);
    });

    it('Takes Screenshot', function(done) {
        client
            .url(siteUrl)
            .getTitle(function(err, title) {
                assert(err === null, "getTitle Error");
                assert(title === 'AngularJS รข€” Superheroic JavaScript MVW Framework');
            })
            .saveScreenshot(__dirname + '/screenshots/home.png', function(err, image) {
                assert(err === null);
            })
            .call(done);
    });
});
  1. From the project root folder, run mocha.
  2. Open test/screenshots/home.png file and examine the content.

Find by [ng-model]

Back to TOC
Protractor comes with handy locator which allows us to find element(s) with specific ng-model or binding.

Here’s how to find element by model manually.

    it('Gets element(s) by ng-model', function(done) {
        //Check against [ng-model] should be enough, but
        //as some sites may require HTML compliant certification
        //[data-*] or [x-*] are used.
        var selector = '[data-ng-model="yourName"],[x-ng-model="yourName"],[ng-model="yourName"]';
        var testVal = 'Foo Bar';
        client
            .url(siteUrl)
            .windowHandleMaximize()   //Make sure input element is visible.
            .setValue(selector, testVal, function(err) {
                assert(err === null, "setValue error: " + err);
            })
            .getValue(selector, function(err, val) {
                assert(err === null, "getValue error: " + err);
                assert(val === testVal);
            })
            .call(done);
    });

This is the same as how we use css selector to find elements by attribute eg. '[attribue="foo"]'.


Find by {{ .ng-binding }}

Back to TOC
Now for the ng-binding. This needs some work to achive but not that hard too once you know how client.execute works.

Under the hood, the client-side angular.element object is basically a jqlite/jQuery object. Binding works similiar to



jQuery.data method.

We can retrive the binding name used by the DOM element in browser console by running this command:

//This has only been tested on AngularJS 1.3.0-beta.7.
//Might work with 1.2.x.
//Not work with 1.1.x and below.

//Return an array of binding name string. (Without curlies)

jQuery('.ng-binding').data('$binding')[0].expressions

Find by binding example:

    it('Gets element(s) by ng-binding', function(done) {
        //Binding can be '{{branch.version}}'' or 'branch.version'
        //Remove '{{}}' and extract only the binding name.
        var binding = '{{branch.version}}'.replace(/(\s*{{)(.*)(}}\s*)/g, function(match, p1, p2, p3) {            
            return p2;
        });
        var selector = '.ng-binding';  //Css Selector to find all ng-bind or {{Binding}}
        var matchList = [];            //Keeps a list of element(s) containing binding

        client
            .url(siteUrl)

            .elements(selector, function(err, res) {
                //Find elements by css selector
                assert(err === null, "elements error");
                assert(res, "res error");

                //Foreach element, check binding match       
                res.value.forEach(function(elemId) {

                    //Make a javascript execute(client-script, element, callback)
                    //and get the binding value 
                    client.execute(
                        function() {
                            return this.angular.element(arguments[0]).data('$binding');
                        },
                        elemId,  //The arguments[0]
                        function(err, res) {
                            assert(err === null, "res.value.forEach error");

                            //Check binding with curly brackets ({{}});
                            if (Array.isArray(res.value) && res.value[0].expressions && res.value[0].expressions.indexOf(binding) >= 0) {                              
                                matchList.push(elemId);
                            } else
                                //Check binding with ng-bind attribute  
                                if (res.value === binding) {                                
                                matchList.push(elemId);
                            }
                    });
                });
            })

            //Using pause command here to make sure matchList is populated before hand 
            .pause(0, function() {
                assert(matchList.length === 2, "List not match. Current matchList.length: " + matchList.length);
            })

            .call(done);
    });

Find by ng-repeater and row and column

Back to TOC
There are few types of result return depends on how to write.

Just by selecting the repeaters will return all the row(s). If furthur request:

Request Result
By row index: return a single row
By row index follow by column: return the column element of that row
By column: return list of column element from every rows
By column follow by row index: return the column element of that row

Two examples:

    it('Get element(s) by ng-repeat (Return row(s))', function(done) {
        var rowIdx = 0;
        var selector = '[ng-repeat^="todo in todos"]';
        var matchList = [];

        client
            .url(siteUrl)
            .elements(selector, function(err, res) {
                assert(err === null, "elements error");
                assert(res, "res error");

                if (rowIdx != null) {
                    matchList.push(res.value[rowIdx]);
                } else {
                    matchList = res.value;
                }
            })
            .pause(0, function() {
                assert(matchList.length === 1, "List not match. Current matchList.length: " + matchList.length);
            })
            .call(done);
    });

    it('Get element(s) by ng-repeat (Return specific column row(s))', function(done) {
        var rowIdx = 1;
        var column = 'todo.text';
        var selector = '[ng-repeat^="todo in todos"]';
        var matchList = [];

        client
            .url(siteUrl)
            .elements(selector, function(err, res) {
                assert(err === null, "elements error");
                assert(res, "res error");

                res.value.forEach(function(elemId, idx) {

                    if (rowIdx == null || rowIdx === idx) {
                        client.execute(
                            function() {
                                return this.angular.element(arguments[0]).children();
                            },
                            elemId, //The arguments[0]
                            function(err, res) {
                                if (err) {
                                    return;
                                }

                                //Loop through children
                                res.value.forEach(function(childId) {

                                    client.execute(
                                        function() {
                                            return this.angular.element(arguments[0]).data('$binding');
                                        },
                                        childId, //The arguments[0]
                                        function(err, res) {
                                            if (err) {
                                                return;
                                            }

                                            //Check binding with curly brackets ({{}});
                                            if (Array.isArray(res.value) && res.value[0].expressions && res.value[0].expressions.indexOf(column) >= 0) {
                                                matchList.push(childId);
                                            } else
                                            //Check binding with ng-bind attribute  
                                            if (res.value === column) {
                                                matchList.push(childId);
                                            }
                                        });
                                });
                            }

                        );
                    }

                })
            })
            .pause(0, function() {
                assert(matchList.length === 1, "List not match. Current matchList.length: " + matchList.length);
            })
            .call(done);
    });

Shutdown Selenium

Back to TOC
Manually stop selenium server after you have done testing.

kill $(ps aux | awk '/[s]tart-selenium/ {print $2}')

About HEADLESS testing

Back to TOC

So far i don’t have any success with phantomjs(ghostdriver).


Download

Back to TOC

You can visit go here for the test script.
https://github.com/Zev23/e2etest


Well the test scripts may look tedious but that shouldn’t be a problem once we make some helper libraries for it.

The main purpose of this article is to demonstrate how to test AngularJS specific type such as binding without the use of Protractor.

This is useful if we have problem running Proctractor (eg. Segmentation Fault) or just want to use totally different framework to test.






Copyright © Zev23.com 2014 All Rights Reserved. No part of this website may be reproduced without Zev23.com’s express consent.

No comments: