In the world of scripted automation tests, Selenium has long reigned supreme. With its powerful Webdriver and easy inclusion in continuous integration and delivery processes, Selenium serves as the backbone for many of today's web testing suites. However, with the rising popularity of Angular, another tool is needed to handle the fluidity introduced by Angular's dynamic web views.

Enter Protractor. Built off Selenium's WebDriverJS, Protractor is a Node.js program which allows you to script and run automated tests in a real browser, interacting with it just like a real user would. Because it's designed specifically for Angular, Protractor integrates with Angular's page state and allows testers to script robust tests which execute successfully despite the unpredictability of an element's appearance on a page. Central to Protractor's design is the promise, and following its design pattern is critical in succeeding with Protractor and deploying well-tested Angular sites.

What is a Promise?

Citing the MDN documentation, a promise "represents the eventual completion or failure of an asynchronous operation, and its resulting value." As a quick analogy, imagine you have promised a coworker you are going to buy them a soda. This promise doesn't mean you actually have a cool drink for them at that exact moment. Your upcoming beverage run could succeed, and you return with soda in hand, or it could fail and the coworker is left thirsty. Non knowing either the outcome or when the outcome will occur is typical of asynchronous process, and this behavior can be hard to code for without the proper tools.

Since promises play an important role in Angular's ability to display dynamic on-screen content, they are also used heavily within Protractor. Let's examine how this paradigm differs from the behavior commonly seen in Selenium, starting off with the command for getting the text from a div, using the Selenium webdriver and the Chai assertion library.

var text = driver.findElement(by.id('myDiv')).getText();
expect(text).to.equal('Hello');

On execution, the webdriver goes and finds the div with id of myDiv, grabs its text, and returns it into the variable. The variable is then immediately usable by other code. This is great for static webpages, but will fail if the element is still being rendered by an Angular page as is common in more modern sites.

Here is the Protractor code to handle Angular:

element(by.id('myDiv')).getText().then(function(divText){
 expect(divText).to.equal('Hello');
});

The major difference here is that the Protractor function getText() does not return a string. It instead returns a promise of providing a string. This is because Protractor may wait for several moments for a page to finish rendering. Once the page has been rendered, the promise resolves, executing the attached function and passing the element text into a variable. Any code that relies on this variable must be executed after the promise resolves.

Almost all Protractor functions work in this manner - returning a promise that resolves after an indeterminate amount of time, and upon resolving, triggering a function to handle the result. Understanding this asynchronous behavior is critical to designing robust, consistent tests.

Handling Multiple Promises

Once we understand that a promise executes asynchronously, the natural question is how to design Protractor tests which run steps in the right order and at the right time. Let's explore a common workflow.

  1. User clicks a button
  2. Modal appears with specific text

When the user clicks the button, it is unclear how long it will take for the modal to appear. In Selenium, we might have used browser.driver.sleep() to wait for a determined period of time, and hope that the modal shows up by then. If we were a bit more sophisticated, browser.driver.wait() could be leveraged, but in Protractor both of these functions are almost always unnecessary and can be avoided.

Here's how we would code for this workflow using Protractor.

element(by.id('myButton')).click();
element(by.id('myModalHeader')).getText().then(function(text){
 expect(text).to.equal('Hello');
});

Because Protractor is integrated in the Angular page state, it knows how to wait for the modal to appear before attempting to grab its text. No sleep or wait functions are needed.

Let's try something a bit more complicated, say, aggregating text from several elements on the page. We can accomplish this using the Promise.all() function.

Promise.all([
 element(by.id('oneDiv')).getText(),
 element(by.id('anotherDiv')).getText()
]).then(function(values){
 expect(values[0]).to.equal('Hello');
 expect(values[1]).to.equal('Nice to meet you');
});

This function kicks off several promises at once, then waits until they all resolve and aggregates the results into a single array. This array can then be used by the attached function.

As an additional example, let's explore what to do if you have promises that absolutely must be run in sequence. Going back to our soda run example, our coworker shouldn't attempt to open the drink until he has it in his hands, so we need to wait until the first promise resolves before initiating the next. In this case, we can use the first promises' then() function.

element(by.id('myInput')).sendKeys('Goodbye').then(function(){
 element(by.id('myInput')).getAttribute('value').then(function(text){
 expect(text).to.equal('Goodbye');
 });
});

You can string together promises in this manner as long as you wish, though the use cases for that are not as common.

Module support for Promises

As a Protractor testing suite takes shape, you may find the need to integrate additional modules to support things like assertions or data creations. Thankfully, there are a whole ecosystem of modules designed especially for Protractor, which follow a promise-based design pattern and integrate seamlessly into the testing flow. Here are a couple particularly useful ones.

Chai-As-Promised

This module is an extension of the popular Chai module, which was being used in the examples above. Chai-As-Promised allows for assertions on promises, which leads to cleaner and more readable code.

var chai = require('chai');
chai.use(require('chai-as-promised'));
var expect = chai.expect;

expect(element(by.id('myDiv')).getText()).to.eventually.equal('Hello');

Request-Promise

Need to make a call to a REST API, perhaps to create test data or validate information from the UI? Since any REST call will be naturally asynchronous, it must be wrapped in a promise.

var requestPromise = require('request-promise');

//configure an options object
var options = {
 uri : 'http://yourAPI/user/1234',
 method : 'GET'
};

//once the call is made, the function below will be fired, passing in the response
requestPromise(options).then(function (response){
 //parse the json response
 var status = JSON.parse(response).status;
 expect(status).to.equal('Active');
});

Pg-Promise

Similar to a REST call, it might become necessary to retrieve or create data from a database. Pg-Promise allows for database calls wrapped in a promise, to work within Protractor.

var pgp = require('pg-promise')();
var db = pgp('postgresql://username:password@host:port/db');

//call the database with a parameter, expecting to receive one row back
db.one('SELECT status FROM users WHERE user_id=1234').then(function(row){
 expect(row.status).to.equal('Active');
});

Writing your own Promises

If you can't find a module to suite your needs, it may be necessary to write a custom promise to handle the asynchronous behavior you are trying to test. Thankfully, the code is relatively straightforward, and easy to wrap inside a function for repeated use.

Here's a very simple promise, returned in a function.

var isItGreen = function(color){
 return new Promise(function(resolve){
 if(color === 'Green'){resolve(true)}
 else{resolve(false)}
 });
};

This function would resolved in the following manner.

expect(isItGreen('Green')).to.eventually.be.true;
expect(isItGreen('Red')).to.eventually.be.false;

There is no need for a promise just to compare strings, so for our final exploration let's consider a more useful scenario. In the Angular world, sometimes it's necessary to have two separate applications, and test the interaction between the two. Though Protractor does an excellent job of tracking page state within a single application, it does not always handle jumping to a different application. Tests that attempt to navigate between applications will often result in a confusing "document unloaded while waiting for result" error.

Here's a custom function which will click a provided hyperlink, wait for the expected url to load, and only then resolve the promise. It will utilize Selenium calls to handle browser redirects and url interactions.

var selectHyperlink = function(hyperlinkId, expectedUrl){

 //we want to click the hyperlink, and then do something afterwards
 return element(by.id(hyperlinkId)).click().then(function(){

 //wrap browser wait functionality in a custom promise
 return new Promise(function(resolve) {

 //wait on the url to change to the expected value
 browser.driver.wait(function(){

 //only return true if url contains expected url
 return browser.driver.getCurrentUrl().then(function(url){
 return url == expectedUrl;
 });
 }, 10000).then(function(){
 //resolve the promise only once the browser has confrimed the url changed
 resolve();
 });
 });
 });
};

This function can be then used in this manner.

selectHyperlink('goToPage', 'https://mysite/home').then(function(){
 expect(element(by.id('newPageHeader')).getText()).to.eventually.equal('My New Page');
});

Once you understand how a custom promise resolves or rejects, you can integrate just about any asynchronous process into your testing suite without relying on outside modules.

In Closing

Testing using Protractor is not without its challenges, and requires focused attention on Angular design patterns. Nevertheless, understanding promises and how to leverage them properly will vault you beyond many of the common pitfalls, leading to robust and effective test suites.