More AngularJS testing: end-to-end tests with protractor
End-to-end tests exist to have a method of programatically testing an actual user's experience in your app. The user never cares how much the pieces of your app are decoupled. The user cares that he or she loaded the page and did all of the things he or she expected to be able to do, without unexpected breaking. We can do this in AngularJS with protractor. Protractor will launch a browser and act just as we expect a user to behave (if we give it the right instructions). There are a few lesser documented tricks in the api that are vital to doing this properly, and I couldn't have truly tested ngLazy without them.
FYI: Most of what I know about testing with protractor, I learned here in the ng-newsletter. You will find very detailed instructions on setting it up, and learn a little about the technology protractor is built upon (Selenium's Webdriver).
Using grunt to build my project, and after installing via npm install grunt-protractor-runner
, running my protractor e2e tests is as simple as running my karma unit tests. One command, grunt test
, and tests do the rest of the hard work.
Protractor and Webdriver give you access to a browser
object, through which you define the user's experience and behavior inside of your test.
Space & Time
I experienced a very confounding problem when writing my e2e tests, and it brought an interesting protractor feature to light. Because my module is dependent on various uses of angular's $timeout, I seemed unable to coordinate protractor's behavior without experiencing it timing out, or blowing through the tests at the improper times. I found the issues documented here and this lead to the eventual solution to my problem:
browser.ignoreSynchronization = true;
When using ignoreSynchonization
before running my tests, I'm able to instruct the browser to observe sleep timers at various times, and can verify that the correct items are on screen, or hidden, at the appropriate times.
Because there are several phases during the use of ngLazy in which we can expect different things from our view, I needed a way to allow for Protractor to run different tests during each phase of the cycle. This was fairly easy once I made use of the method:
browser.sleep()
Probably the primary feature of ngLazy is reliant on the user scrolling to the bottom of the list, in order to trigger ngLazy to begin appending more items to the dom. This behavior was also pretty trivial to recreate in protractor! The executeScript
method provides an api that takes DOM API arguments and executes them, while returning a promise. When the promise resolves, proceed with more instructions:
browser.executeScript('window.scrollTo(0,' + 800 + ');').then(function(){
browser.sleep(2000);
// range is defined previously
expect(element.all(by.css('.ng-binding')).count()).toEqual(range * 2);
})
Assert yourself
I'll include the current family of tests below, to demonstrate the kind of integrated tests I'm running. The assertions include:
* it should immediately display a spinner
* it should have a spinner-color that matches the configuration
* it should have as many ng-repeated items as the scope indicates
* it should have an element in the DOM that represents the bottom of the list
* it should add elements to the DOM when it scrolls to the bottom of the list
describe("ngLazy-demo", function(){
browser.driver.manage().window().maximize();
browser.get('/#');
browser.ignoreSynchronization = true;
describe("index", function () {
it("should display the correct title", function(){
expect(browser.getTitle()).toBe('ngLazy-Demo');
});
it("should have a lazy-load element", function(){
expect(element(by.css('lazy-load')).isPresent()).toBe(true);
});
});
var range;
describe("lazy-load directive", function(){
it ("should immediately display a spinner", function(){
expect(element(by.css('.ng-hide')).isPresent()).toBe(false);
});
it("should have a spinner-color that matches the configuration", function(){
var color;
var loadingWidget = browser.findElement(by.css('.loading-widget'));
loadingWidget.getAttribute('style').then(function(color){
// ugly way of getting the color string from the directive
color = (((color
.split('border-color:')[1])
.split(';')[0])
.split('transparent ')[1]).trim()
.split(' rgb')[0];
var spinnerElement = browser.findElement(by.model('spinnerColor'));
spinnerElement.getAttribute('value').then(function(val){
expect(color).toEqual(val);
})
})
})
it("should have as many ng-repeated items as the scope indicates", function(){
var rangeElement = browser.findElement(by.model('range'));
rangeElement.getAttribute('value').then(function(val){
browser.sleep(4000);
range = parseInt(val);
var repeats = element.all(by.css('.ng-binding')).count();
expect(repeats).toEqual(range);
});
});
it("should have an element in the DOM that represents the bottom of the list", function(){
expect(element(by.css('#lazy-bottom')).isPresent()).toBe(true);
});
it("should add elements to the DOM when it scrolls to the bottom of the list", function(){
browser.sleep(2000);
browser.executeScript('window.scrollTo(0,' + 800 + ');').then(function(){
browser.sleep(2000);
expect(element.all(by.css('.ng-binding')).count()).toEqual(range * 2);
})
})
});
})
I am looking to fill out this suite with even more useful tests, so I welcome any suggestions! Please feel free to fork and pull request from the demo app's repo.