Thursday, November 19, 2015

Enough to be Dangerous: Open a different browser during a Protractor test

Those of you looking to test AngularJS apps may have particular use cases where multiple instances of the page need to be opened to simulate multiple instances of an application running.  Say you have a chat client, and you wish to simulate multiple users on different instances of the application.  Or, perhaps you want to run two separate windows so that one represents a user interacting with a service and the other represents an admin panel watching over the user.  No matter what your use case is, Protractor makes this easy.  Protractor is an end-to-end testing framework for Angular applications that integrates with the Selenium WebDriver for powerful browser automation and ties in tightly with Angular internals for very powerful testing possibilities.


A Simple Case: More of the Same


Current versions of Protractor as of this writing easily support the ability to add more browser instances of the type you defined in your configuration file's capabilities section.  Recall that your capabilities section might look like this (it's OK if you choose to use multiCapabilities instead):


// protractor-conf.js

exports.config = {
    ...
    capabilities: {
        'browserName': 'chrome'
    }
}

The interesting part exists in your spec (test code) file:

var browserCount = NUM_OF_BROWSERS_YOU_WANT_TO_SPAWN;
var browsers = [];
describe("a suite of tests", function() {
    it("should open multiple browser instances", function() {
        for (var i = 0; i < browserCount; i++) {
            // Make the new browser instance<
            browsers[i] = browser.forkNewDriverInstance();
            var newBrowser = browsers[i];
            newBrowser.get('http://www.example.com');
            // Make sure it is separate from the original browser
            expect(newBrowser).not.toEqual(browser);
            expect(newBrowser.driver).not.toEqual(browser.driver);
            // Go on manipulating the controls on your new instance
            newBrowser.element(by.id('firstThingToClick')).click();
            ...
        }
    })
})

The important part here is the use of the forkNewDriverInstance() function on browser.  This spawns the new browser instance.  Now, in case you want to properly close all your spawned browser instances, run this either at the end of your it() test case, or in your afterAll() or afterEach() function:


var closedCount;
var done = function() {
    // some kind of done routine, if you need it
};


for (b in browsers) {
    if (browsers[b]) {
        // if browsers[b] is actually a browser instance, close it
        console.log("Attempting to close browser #" + b);
        browsers[b].quit().then(function() {
            closedCount += 1;
            if (closedCount == browserCount)
                done();
        });
    } else {
        // in the event that you already wiped out your browsers after the 1st test case and haven't quite written subsequent test cases to reinitialize them, this'll save you from getting ugly errors
        // Just increment the closedCount so the test can end if we've reached the total of browserCount
        closedCount += 1;
        if (closedCount == browserCount)
            done();
    }
}

Pretty darn simple and elegant, right?


More Browsers, More Fun!


Of course, running on just one browser is less interesting.  Your users and perhaps your admins are going to run your application on whatever their favorite browser is, and you'd better be prepared.  However, the Protractor documentation doesn't seem to detail this anywhere.  To handle all possible interactions between browsers, you can actually initialize another Runner object in the middle of your test.  This Runner object will initialize a new driverProvider for the browser type you specify in the capabilities of the configuration you give to Runner.  I did quite a bit of poring over the Protractor code in order to figure out how to do this, but the solution ends up being even shorter:


it("Should open both Chrome & Firefox", function() {
    // browser is defined by the framework, so go ahead and use it
    browser.get('http://www.foo.com');
    // Find where runner.js is relative to your test case
    var Runner = require('./path/to/node_modules/protractor/lib/runner');
    var myConfig = {
        allScriptsTimeout: 30000,
        getPageTimeout: 30000,
        capabilities: { browserName: 'firefox', count: 1 } }
    var ffRunner = new Runner(myConfig);
    var ffBrowser = ffRunner.createBrowser();
    ffBrowser.get("http://www.bar.com"); 
    // Do various operations on your page to run the test case you want
    // Pretend like this is the last operation in your test; the important part is the .then()
 
   ffBrowser.sleep(SLEEP_TIME).then(function() {
        ffBrowser.quit().then(function() {
            ffRunner.shutdown_();
        });
    });
});

A couple things to point out about this code:

  1. 1. The contents of myConfig are the minimum contents I found in order to get the test case working and passing.  Now you could remove getPageTimeout, but I wouldn't recommend it in case the page under test goes down.
  2. Without putting ffRunner.shutdown_() inside all those Promises (.then()), it will run out of sync with the browser operations and actually shut down the browser before anything in your test happens.  If you put ffBrowser.quit() and ffRunner.shutdown_() in series, the framework will freak out that the browser was not uninitialized properly.  Might as well keep your output clean.
  3. The shutdown_() routine seems to me to be private, based on the underscore.  I feel bad calling it, but it's not exposed anywhere else, and it seems to work, so whatever.  Have a piece of cake or a bag of junk food if you feel so bad about using it. :-P

Now, of course, you could probably put new Runner(myConfig) in beforeAll() and ffRunner.shutdown_() in afterAll(), but for convenience sake, I put everything in the it() test case.

Enjoy running multiple browser instances in your Protractor tests!