Simplify Protractor Web Tests with Async and Await

by
Tags: , , ,
Category:

Protractor is a testing API written by Google in support of AngularJS. It is a wrapper around the Selenium WebDriver API for
JavaScript. In its earlier versions, Protractor is heavily promise-driven, and can be a bear to wrestle with using the traditional APIs.

Consider this typical promise-driven Protractor test:

it('should get something done, eventually', (done) => {
  element(by.model('customer.name').getText())
  .then(t => {
     expect(t).then(a => toBe('Charles');
     done();
  });
});

… girded by this page object:

class MySearchPage {
  var search = element(by.model('vm.search'));
  this.getSearchText = function() {
   // a resting input's value is not in getText, it's in the attribute 'value'
   return search.getAttribute('value');
  };
  this.enterSearchText = function(searchText) {
    return search.clear()
      .then(function() {
        return search.sendKeys(searchText);
      });
  };
}

To paraphrase a movie actor, the code's body is "writing checks it can't cash"… Not easily anyway. We have a number of promises
being resolved one by one:

Page Object

  • A promise in the search page, implicitly, with element(by.model('vm.search')) – this generates a promise when used.
  • A promise by search.clear() which resolves once the clear operation completes on the designated input field. We then use that
    promise to chain a call to send a set of keys to the input field, and return that promise back to the test.

Test

  • this.taskListPage.enterSearchText('boo!').then(...) – call the page object to send text to the search input field. This all
    results in a promise call so we chain a then and…
  • Since the test itself resolves via a promise, we pass in a done method and call it when the above call is done.

This is enough promising to drive even the most hardened Zen Master crazy.

There has to be another way!

Slay the Promise Dragon with async and await

Yes, there is another way. In fact, it's made easier in the most recent versions of NodeJS and Protractor.

Consider this nice replacement test. Comments within!

// the key to this test is to mark the whole arrow function as `async`
it('entered data in a field and checked it with async/await', async () => {
  // to wait for a promise before moving on, just await the completion
  await this.taskListPage.enterSearchText('boo!');
  // to get an answer back - await the result of the promise and assign
  let searchText = await this.taskListPage.getSearchText();
  // now synchronously check the final result
  expect(searchText).toBe('boo!');
});

Easy, right? And now you can see just how stupid this test is. If someone was reviewing my code, they'd probably ask "why are you
testing whether Angular's form input works when you set text?" Ugh!

Disabling the Promise Flow Control Feature in Jasmine

Quick tip – when you're using async and await, disable the now-deprecated Selenium promise manager in your protractor configuration file. With this setting still enabled you'll run into stability problems:

exports.config = {
  ...
  SELENIUM_PROMISE_MANAGER: false,
  ...
};

Debugging Protractor tests in Node 8.x and higher

Now the really good news (preceded by a recap of the bad news of old-timey Protractor). Recently the NodeJS team deprecated an
ancient debugging engine. The Selenium team responded by doing that deprecation of the promise manager in favor of tests written
with async and await.

The problem? Both of these things broke two Protractor APIs: browser.enterRepl() and browser.debug(). The first
provided a rather inscrutable REPL to play with (which I could never really get to work) and the second paused your browser.

Good news, however. If you have Node 8.x or higher (I'm running on 9.5 as I write this) and you run Protractor from any recent
vintage, you should be able to debug your tests in real time!

First, set a breakpoint using debugger; on a line in your spec file. If you're thinking 'nah, I'll set it once I start', think
again, because Protractor won't load your script file until after you begin testing, so the source tab of Chrome's developer tools
won't have your tests in it.

Now, start Protractor like this:

node --inspect-brk ./node_modules/.bin/protractor ...args here...

This will fire up Protractor but pause waiting for a debugging connection.

Launch Chrome, and browse to: chrome://inspect/#devices which should let you click on your Protractor session to debug it.

The debugger pane will launch, immediately pause before running your tests suite. YOu can click play in the debugger to begin
running protractor, and it will stop on your debug line. Now you can actually step through
your code!

Setting a longer timeout during debug sessions

If you start debugging but it takes a long time for you to move from step to step, Protractor will cancel your test and run another
one. To fix this, you can pass a longer debug time into Protractor. The fastest way is to set up a copy of your Protractor config
file and add a long timeout to it:

exports.config = {
  ...
    getPageTimeout: 100000,
  ...
};

Now you can run your tests, and as long as you're done with debugging steps in 100 seconds you won't get timed out.
If you’re a Cucumber tester, you’ll be able to use this same set of techniques (async/await, debugging steps, etc) there
as well, simplifying your life too.

Wrap-up

I hope you've enjoyed this article and it has made you a bit more efficient and DRYed up your Protractor tests a bit.

References:

 

Learn with us

If you’re a company needing training for your team of developers in AngularJS, Angular (4+) or React, we can help.

We aren’t just a training outfit, in fact we’re mostly a consulting firm, with projects all over the full stack
including mobile, SPA, Scala, Spring, Spark and much more. Contact us today for training or help with your projects.