Things I wish I knew about Protractor before I started

I’ve been writing end-to-end tests for the place I work at, which has its main product written in Google’s AngularJS. The dedicated E2E testing framework for AngularJS is the Protractor framework. While it’s a nice project, the documentation is a bit terse at times, and a lot of my understanding came, unsurprisingly, from Stack Overflow. Here are some of my key learnings, also for future reference. Some of it may seem a little obvious, but my experience with JavaScript doesn’t go much further than <script>alert('Hello World!')</script> so bear with me here.


Absorb best practices

Probably a good idea for any new framework you learn. I found this style guide by Carmen Popoviciu to be a great list, though I haven’t managed to implement all of it yet, and not all of it in there is relevant to my use case.

onPrepare

When I started out, my first spec, regardless of what it was, would always fail, saying that the page didn’t have Angular enabled. Most Protractor people will probably be familiar with the error, starts like “Error while waiting for Protractor to sync with the page:”. Any subsequent specs would run wihtout issue though.

The only way to fix this was by loading up the login page of our SPA before any of the specs started. You can do this using the onPrepare directive. This is vaguely referred to in Protractor’s documentation, but I only found that page researching onPrepare just now. I still think I’m doing something wrong in my setup that’s forcing me to resort to this hack, but I can’t figure out what.

pageObject elements as function expressions

I found myself testing entire user flows, meaning the user/test case navigates from one page to the next. I’m not sure if that is a correct testing pattern (I suspect maybe not because of what’s to follow), but let’s take it as a given for now.

Testing multiple pages within a spec meant that I had to load multiple pageObjects, one for each page tested. The Protractor way of defining elements of a pageObject looks like this:

var AngularHomepage = function() {
  var nameInput = element(by.model(yourName));
  this.setName = function(name) {
    nameInput.sendKeys(name);
  };
};
module.exports = new AngularHomepage();

However, loading multiple, different pageObjects at spec initiation inevitably resulted in errors. This shouldn’t be surprising: if the test is currently on Page1, loading the Page2 pageObject will lead it to try to instantiate elements that don’t exist currently, given you’re on the wrong page.

This led me to trying something new: I defined the variables as functions in the pageObject. This way, the variables would only execute when I called them:

var AngularHomepage = function() {
  var nameInput = (function() { return element(by.model(yourName)) });
  this.setName = function(name) {
    nameInput.sendKeys(name);
  };
};
module.exports = new AngularHomepage();

Another way to fix the above is by not initiating all your pageObjects at the top of the spec, instead only initiating as you need them. This goes against the best practices linked above though (Rule 15), hence this solution.

Referring to other variables in a pageObject

For JavaScript ninjas, this is an obvious one, but I’m a mere layman when it comes to JS, so here’s a trick for all you other laymen out there. Sometimes, in a pageObject, you’ll want to refer to an earlier defined variable. For example:

this.buttonList = (function () { return element(by.css(‘div.btn-list’)); }) ();

However, in JavaScript, this is a reserved name. That means that in another part of the pageObject, I can not do the following:

this.saveButton = (function () { return this.buttonList.element(by.css(‘button.btn-primary[id=”save”]’)); }) ();

As far as my understanding goes, this fails because of JavaScript’s function scope. At any rate, I’m not the first one to have run into this. Hence, at the top of my pageObject, I declare:

var that = this;

Later on, I can then simply do:

this.saveButton = (function () { return that.buttonList.element(by.css(‘button.btn-primary[id=”save”]’)); }) ();

It’s left as an exercise to the reader to figure out what the hell just happened, but it works!

Testing toasts means you’ll have to turn synchronization off

First hit on Google Images for ‘toast notification’. Source: https://www.patternfly.org/pattern-library/communication/toast-notifications/
First hit on Google Images for ‘toast notification’. Source: https://www.patternfly.org/pattern-library/communication/toast-notifications/

A common UX pattern is to confirm a user’s action by showing a so-called “toast” notification. In my case, I had to write a test that confirms a toast appears after a user’s saving action. Here’s my first attempt at that:

builderPage.saveButton.click()
// Toast appears
expect(builderPage.saveConfirmationToast.isDisplayed()).toBe(true);

This approach failed me. For some reason, Protractor would wait for the toast to fade in and fade out after the user’s click. Only once the toast was faded out, Protractor would fire the expect, which of course failed, given the toast had already faded out.

The solution, as so often, came in the form of a StackOverflow answer. The reason I was seeing this behaviour, is because Protractor is very patient: the moment the click() is issued, the callback is initiated which fades the toast in and out. Protractor politely waits for this promise to be resolved, before moving on to the next execution in the stack (the expect()).

Having established the reason, the SO answer pointed me in the right direction: ignoring the synchronisation by setting the browser.ignoreSynchronization parameter to true, and wrapping the test nicely in a callback once the toast is visible:

browser.ignoreSynchronization = true;
builderPage.saveButton.click().then(function() {
  browser.wait(EC.presenceOf(builderPage.saveConfirmationToast, 2000)).then(function() {        expect(builderPage.saveConfirmationToast.isDisplayed()).toBe(true);
    browser.ignoreSynchronization = false; // Important!
  });
});

Protractor is not “too fast”

For a while, my tests would run poorly, and Protractor would systematically not wait long enough for an element/page to finish loading, before running its tests. Put in other words, for some reason, Protractor wouldn’t wait for Angular to finish loading the page, no matter how many browser.waitForAngular’s I put in there. Now repeat after me, “Protractor does not run too fast”. If it looks like it’s the case, you’ve probably turned browser.ignoreSynchronization on somewhere. At least, that was my case. As far as I can tell, running quite a few complex testing suites, Protractor is perfectly tuned to Angular and waits nicely if you don’t have that setting set to true.

XPath locators aren’t all bad

I know the best practices state that you shouldn’t use XPath locators because they’re very brittle. This is true, they should be used judiciously. But honestly, finding parent elements with the API Protractor provides is just frustrating as hell (as of the time of this writing anyways). I only use XPath locators when I really need to, but damn if I am not thankful when I can.