Testability is an important discipline in application development. Angular was always built with testability in mind. Protractor is Angular’s end-to-end testing framework. It was originally created for AngularJS, the first edition of the popular SPA framework, but works perfectly with Angular 2 by simply setting useAllAngular2AppRoots
to true
in Protractor’s config.js. In addition, you can optionally specify an element name for the rootElement
property to exclusively test against this element.
exports.config = { framework: 'jasmine', seleniumAddress: 'http://localhost:4444/wd/hub', specs: ['spec.js'], useAllAngular2AppRoots: true, // rootElement: 'root-element' };
However, if you are using Angular 2 and Protractor in combination, you might stumble upon one of the following error messages:
Failed: Timed out waiting for Protractor to synchronize with the page after 11 seconds. Please see https://github.com/angular/protractor/blob/master/docs/faq.md.
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
Here’s how to fix those errors:
The error messages noted above are related to long-running or repeatedly executed asynchronous operations, usually in the form of setTimeout
, setInterval
, observables or promises which are started during the app’s launch. This could be ping/heartbeat services or setTimeout
s waiting a couple of seconds or longer. The usages could look like the following:
setInterval(() => { // async operation }, 2500);
Or, maybe a more elegant solution, using RxJS’s IntervalObservable:
Observable.interval(2500).subscribe(() => { // async operation });
Before Protractor will perform any test specification, it tries to wait for the Angular 2 app to “synchronize”, i.e. to complete all outstanding operations. As per default, Protractor defines an 11 seconds timeout for this to happen.
What Zones Got to Do with It
Zone.js is an inherent part of Angular 2. This library keeps track of any asynchronous operations. Thus, Zone.js monkey-patches setTimeout
, setInterval
, intercepts a whole bunch of events and almost anything that could happen asynchronously in the browser. The execution context in which asynchronous operations are happening, is referred to as a zone. As such, Zone.js is a crucial part of Angular’s change detection.
Here in this case, this behavior prevents Angular to finally synchronize with Protractor: Angular is running in an own zone, which is a fork of the global zone. Long-running or repeating asynchronous operations started with the application and running in this zone will prevent synchronization. Luckily, you can opt-out from running inside Angular’s zone: You can get a reference to the current zone by injecting NgZone
into your Angular service or component. By calling runOutsideAngular
on the zone reference, you can pass a function invoking the asynchronous operation which will run outside of the framework. This allows the Angular 2 application to synchronize—as a result, Protractor will be able to run your test specifications.
ngZone.runOutsideAngular(() => Observable.interval(2500).subscribe(() => { // async operation });
Keep Change Detection Working
However, this change might break your application, as you now also opted-out from Angular’s change detection. If you expect the UI to update on changes triggered by your asynchronous operation, this won’t work any longer. Fortunately, you can opt-in to run inside Angular again, by calling the run
method on the zone reference and passing a function. Any changes performed there will now be handled by Angular’s change detection mechanism again and the UI will magically update.
ngZone.runOutsideAngular(() => { setInterval(() => { ngZone.run(() => { // async operation }); }, 2500);
In conclusion, your application will be able to synchronize with Protractor without breaking Angular’s change detection. You’re all set.