The orignal post is here
http://stackoverflow.com/questions/36358405/how-to-implement-intervals-polling-in-angular2-to-work-with-protractor
I have an angular2 app I want to test with protractor.
In this app I have a page with a graph that is being updated in regular intervals with autogenerated data.
Apparently one feature of protractor is waiting for scripts and http calls to finish before executing test code. However, if there is a constantly polling script that never finishes, protractor will wait forever and time out after a certain time.
In angular1 this could be solved by implementing the polling with $interval, which protractor does not wait for. Unfortunately in angular2 there is no $interval and the correct way to implement polling seems to be Observable.interval, so this is what my code looks like:
Observable.interval(500)
.map(x => this.getRandomData())
.subscribe(data => this.updateGraph(data));
When testing a page where this code is running, protractor will time out. It waits for the page to finish loading and thinks this script will exit sometime (when in fact it runs forever).
EDIT: To clarify, the timeout problem already existed in protractor with angular1, but could be fixed by using $interval, see:
This doesn't work in angular2 because there is no $interval.
I'll post a longer form later, but the short answer is you can tell Protractor not to wait on something by running it outside of the Angular zone. See my talk at https://www.youtube.com/watch?v=DltUEDy7ItY and example at https://github.com/juliemr/ngconf-2016-zones/blob/master/src/app/main.ts#L38
great talk, this resolved the problem. thanks juliemr! :+1:
this.ngZone.runOutsideAngular(() => {
this.timer = Observable.interval(1000)
}
Hi, @juliemr. I'm trying to run a poller service outside of ngZone, but can't get it to work and my E2E tests time out. The idea is that I need to update some user data every X seconds by fetching it from back end and I can have multiple consumers of this data which can subscribe or unsubscribe at any point in time. Here's the code:
import { Injectable, NgZone } from "@angular/core";
import { Http } from "@angular/http";
import { Observable } from "rxjs";
@Injectable()
export class SomeService {
private someEmitter: Observable<SomeData>;
constructor(private http: Http, private ngZone: NgZone) {
this.ngZone.runOutsideAngular(() => {
this.someEmitter = Observable
.timer(0, 10000) // Run every 10 seconds
.flatMap(() => http.get(`http://localhost/userData`))
.map(response => {
this.ngZone.run(() => {
console.log('Done!');
});
return response.json().result;
})
.share();
});
}
get data(): Observable<SomeData> {
return this.someEmitter;
}
}
Any suggestions on how to fix that?
@Auxx I had similar problem and I solved it by writing my custom Observable that imitates Observable.delay(1000) :
Observable.create((observer) => {
let timeout = null;
this._zone.runOutsideAngular(() => {
timeout = setTimeout(() => {
observer.next(null);
observer.complete();
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
},1000);
});
});
In your case I think the problem is that timer will be created when you call subscribe for the first time and you probably doing it inside angular zone.
TODO(me): in general, we need better docs on Angular2 tests. Find a section where this should go.
Here's some feedback for the Protractor team...
We were facing the same issue. We have an Angular 2 based ecommerce store with a little notification that shows in the corner of the screen when an item is added to basket, and then disappears after 3 seconds (via setTimeout). It should not stop the user from being able to add items to the basket.
One of our E2E tests adds multiple items to the basket then checks out. When we added the notification code, we noticed the E2E test took significantly longer because it was waiting for the notification to disappear between adding each item.
I came here and saw this issue and was thinking to myself the following:
"it's terrible that I have to write zone.runOutsideAngular in production code just so my E2E test will reflect actual user behaviour and continue adding items with the notification still displayed. If it was Angular 1, I would use setTimeout or $timeout as desired"
I soon realised this thought was wrong because differentiating between setTimeout and $timeout is the same as differentiating between a bare setTimeout and a setTimeout within zone.runOutsideAngular. In either case, my production code is different because of my tests.
Then it struck me that Protractor has all this waitForAngular and Angular integration shenanigans to help us by obeying timeouts, HTTP requests etc. So I guess it works exactly as designed. It's just something that for our use case is more of a hindrance.
So for us, the solution was that in our beforeEach we do this:
browser.waitForAngularEnabled(false);
The reason is that we're quite happy for Protractor to back off and we can manage the waiting ourselves. We have several helpers defined here that do various cool waiting stuff for us: https://github.com/SMH110/Pizza-website/blob/d342755a728e5afe7632a2c0e9b48f3253882a62/specs/e2e/utils.ts. Wherever we want Protractor to wait for Angular we can explicitly enable it then disable it again after.
@massimocode that only works for things which happen once, not for timers running all the time.
In general, I've found that it's better to have Protractor wait less. End users can always add more waiting later, but it's hard to deal with overly zealous waitForAngular functions. The only option for end users is to disable waiting entirely and implement their own thing. One of my hopes in the future is to make the waitForAngular 'shenanigans' more flexible, so that users can add waiting as needed, but use a reasonable API that gives them control over the kinds of events they wait for.
I poll an http service for status updates throughout my app. Initially when I got the Protractor timeouts I tried the runOutsideAngular() option. This lets the test run but I need the change detection to work.
I've resorted to setting browser.ignoreSynchronization = true combined with browser.driver.sleep(1000) after any page changes but it's clunky and unreliable.
Any solution on the Protractor side of things would be most welcome.
The following service works for me:
import {Injectable, NgZone} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {Subscription} from 'rxjs/Subscription';
import {Observer} from 'rxjs/Observer';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/multicast';
@Injectable()
export class PoolingService {
constructor(private zone: NgZone) {}
// NOTE: Running the interval outside Angular ensures that e2e tests will not hang.
execute<T>(operation: () => Observable<T>, frequency: number = 1000): Observable<T> {
const subject = new Subject();
const source = Observable.create((observer: Observer<T>) => {
let sub: Subscription;
this.zone.runOutsideAngular(() => {
const zone = this.zone;
sub = Observable.interval(frequency)
.mergeMap(operation)
.subscribe({
next(result) {
zone.run(() => {
observer.next(result);
});
},
error(err) {
zone.run(() => {
observer.error(err);
});
}
});
});
return () => {
if (sub) {
sub.unsubscribe();
}
};
});
return source.multicast(subject)
.refCount();
}
}
And you use it as such:
const obs = this.pooling.execute(() => ... return an observable here, 1500);
obs.subscribe(() => {
... do something here
});
Though, if you actually need to wait for things to be updated, I'm unsure what the solution would be.
@mgiambalvo - I second that. Definitely having more flexibility built in the Testability API and having Protractor follow suit would be nice. Something like individual flags for waiting for different async operations, e.g. wait for no running tasks/no scheduled tasks/no pending XHR.
@juliemr Is there a way to determine what async tasks are running when getting a timeout? We are having trouble tracking down the reason we are getting ScriptTimeoutError: asynchronous script timeout: result was not received in 11 seconds messages at times
We've updated the documentation for this, and are working on getting better timeout errors in modern Angular (ie, not AngularJS) apps.
This works great if you just need to delay a function call.
@Injectable()
export class DelayService {
constructor (
private _zone: NgZone
) {}
public delay (fn: Function, delay: number): void {
if (!fn || delay < 0) {
throw new FrendError(`No function or invalid delay provided.`);
}
this._zone.runOutsideAngular(() => {
Observable
.of(null)
.delay(delay)
.subscribe(() => this._zone.run(() => fn()));
});
}
}
Having to introduce complexity to the implementation, in order to satisfy some end-to-end tests is far from ideal, yet it appears to be recommended to mess around with zones and manually control what runs inside or outside the zone.
I'm wondering if there is any sort of monkey patching we can do during the setup phase of our end-to-end tests, so that we don't have to change application code. Still looking...
@Merott tl;dr; No. There's nothing you can do during setup phase.
The complexity is not just "to satisfy some end-to-end tests" (actually it seems that you undersestimate the value of e2e tests) rather a matter of proper application structure.
This is how protractor works - it needs to know when the initial tasks of loading the page are completed. If you have background jobs, you need to run them in their own zones, not just in order to make protractor work, but for many other reasons (like per-zone error-handling, for instance).
Just my 2c.
@AlexandrosD
I don't think I underestimate the value of e2e tests. However, perhaps I don't quite understand why the proper or better way of writing the following method would be to have it run outside Angular's zone:
public poll(endpoint: string, pollingInterval: number) {
return this.http
.get(endpoint)
.repeatWhen(() => Observable.interval(pollingInterval))
.map(response => response.json());
}
I realise this might be taking the topic of this thread slightly off the rails, but I'm just trying to understand if this Github issue is about something to be _fixed_, or something to be better _explained_.
I came across a more elegant solution, to use a scheduler to run certain operations outside of the angular zone. That makes it possible to not fiddle around with run and runOutsideAngular at the time you subscribe to an observable but rather to define the things to be run outside the angular zone at the position of the Observable sequence.
I just wrote an article on how this could also be solved through the async pipe (or a fork of it): https://medium.com/@magnattic/preventing-the-infamous-protractor-timeouts-the-smart-way-66b8cc517b04
This issue effectively blocks issuing Angular's async pipe with timer-based observable - there is no way to use runOutsideAngular, as subscription happens internally, in async pipe code.
The workaround is not to use async pipe with doing all subscribe/unsubscribe/trigger changes manually
In terms of code ammount:
template: '<span *ngIf="time" [innerHTML]="label$ | async"></span>',
ngOnInit() {
this.label$ = timer(0, UPDATE_INTERVAL).pipe(map(() => this.formatLabel()));
}
VS:
template: '<span *ngIf="time" [innerHTML]="label"></span>',
ngOnInit() {
this.setupLabelUpdate();
}
ngOnDestroy() {
// eslint-disable-next-line chai-friendly/no-unused-expressions
this.labelSubscription?.unsubscribe();
}
private setupLabelUpdate() {
if (!this.time) {
return;
}
this.ngZone.runOutsideAngular(() => {
this.labelSubscription = timer(0, UPDATE_INTERVAL)
.pipe(map(() => this.formatLabel()))
.subscribe((label) => {
this.label = label;
this.changeDetector.detectChanges();
});
});
}
It would be nice to have some more robust solution for cases like this.
Most helpful comment
great talk, this resolved the problem. thanks juliemr! :+1: