Skip to content

Commit

Permalink
feat(core): Improvements to the Testability API.
Browse files Browse the repository at this point in the history
Allow passing an optional timeout to Testability's whenStable(). If
specified, if Angular is not stable before the timeout is hit, the
done callback will be invoked with a list of pending macrotasks.

Also, allows an optional update callback, which will be invoked whenever
the set of pending macrotasks changes. If this callback returns true,
the timeout will be cancelled and the done callback will not be invoked.
  • Loading branch information
heathkit committed May 18, 2017
1 parent af99cf2 commit a28fdfb
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 151 deletions.
1 change: 1 addition & 0 deletions karma-js.conf.js
Expand Up @@ -28,6 +28,7 @@ module.exports = function(config) {

'node_modules/zone.js/dist/zone.js',
'node_modules/zone.js/dist/long-stack-trace-zone.js',
'node_modules/zone.js/dist/task-tracking.js',
'node_modules/zone.js/dist/proxy.js',
'node_modules/zone.js/dist/sync-test.js',
'node_modules/zone.js/dist/jasmine-patch.js',
Expand Down
109 changes: 92 additions & 17 deletions packages/core/src/testability/testability.ts
Expand Up @@ -22,6 +22,23 @@ export declare interface PublicTestability {
findProviders(using: any, provider: string, exactMatch: boolean): any[];
}

export interface PendingMacrotask {
source: string;
isPeriodic: boolean;
delay?: number;
creationLocation: Error;
xhr?: XMLHttpRequest;
}

export type DoneCallback = (didWork: boolean, tasks?: PendingMacrotask[]) => void;
export type UpdateCallback = (tasks: PendingMacrotask[]) => boolean;

interface WaitCallback {
timeoutId: number;
doneCb: DoneCallback;
updateCb?: UpdateCallback;
}

/**
* The Testability service provides testing hooks that can be accessed from
* the browser and by services such as Protractor. Each bootstrapped Angular
Expand All @@ -30,23 +47,25 @@ export declare interface PublicTestability {
*/
@Injectable()
export class Testability implements PublicTestability {
/** @internal */
_pendingCount: number = 0;
/** @internal */
_isZoneStable: boolean = true;
private _pendingCount: number = 0;
private _isZoneStable: boolean = true;
/**
* Whether any work was done since the last 'whenStable' callback. This is
* useful to detect if this could have potentially destabilized another
* component while it is stabilizing.
* @internal
*/
_didWork: boolean = false;
/** @internal */
_callbacks: Function[] = [];
constructor(private _ngZone: NgZone) { this._watchAngularEvents(); }
private _didWork: boolean = false;
private _callbacks: WaitCallback[] = [];

/** @internal */
_watchAngularEvents(): void {
private taskTrackingZone: any;

constructor(private _ngZone: NgZone) {
this._watchAngularEvents();
_ngZone.run(() => { this.taskTrackingZone = Zone.current.get('TaskTrackingZone'); });
}

private _watchAngularEvents(): void {
this._ngZone.onUnstable.subscribe({
next: () => {
this._didWork = true;
Expand All @@ -67,12 +86,14 @@ export class Testability implements PublicTestability {
});
}

/** @deprecated pending requests are now tracked with zones */
increasePendingRequestCount(): number {
this._pendingCount += 1;
this._didWork = true;
return this._pendingCount;
}

/** @deprecated pending requests are now tracked with zones */
decreasePendingRequestCount(): number {
this._pendingCount -= 1;
if (this._pendingCount < 0) {
Expand All @@ -83,27 +104,81 @@ export class Testability implements PublicTestability {
}

isStable(): boolean {
return this._isZoneStable && this._pendingCount == 0 && !this._ngZone.hasPendingMacrotasks;
return this._isZoneStable && this._pendingCount === 0 && !this._ngZone.hasPendingMacrotasks;
}

/** @internal */
_runCallbacksIfReady(): void {
private _runCallbacksIfReady(): void {
if (this.isStable()) {
// Schedules the call backs in a new frame so that it is always async.
scheduleMicroTask(() => {
while (this._callbacks.length !== 0) {
(this._callbacks.pop() !)(this._didWork);
let cb = (this._callbacks.pop() as WaitCallback);
clearTimeout(cb.timeoutId);
cb.doneCb(this._didWork);
}
this._didWork = false;
});
} else {
// Not Ready
// Still not stable, send updates.
let pending = this.getPendingTasks();
this._callbacks = this._callbacks.filter((cb) => {
if (cb.updateCb && cb.updateCb(pending)) {
clearTimeout(cb.timeoutId);
return false;
}

return true;
});

this._didWork = true;
}
}

whenStable(callback: Function): void {
this._callbacks.push(callback);
private getPendingTasks(): PendingMacrotask[] {
if (!this.taskTrackingZone) {
throw new Error('Task tracking zone required when using whenStable() with a timeout!');
}

return this.taskTrackingZone.macroTasks.map((t: Task) => {
return {
source: t.source,
isPeriodic: t.data.isPeriodic,
delay: t.data.delay,
// From TaskTrackingZone:
// https://github.com/angular/zone.js/blob/master/lib/zone-spec/task-tracking.ts#L40
creationLocation: (t as any).creationLocation as Error,
// Added by Zones for XHRs
// https://github.com/angular/zone.js/blob/master/lib/browser/browser.ts#L133
xhr: (t.data as any).target
};
});
}

private addCallback(cb: DoneCallback, timeout?: number, updateCb?: UpdateCallback) {
let timeoutId = -1;
if (timeout > 0) {
timeoutId = setTimeout(() => {
this._callbacks = this._callbacks.filter((cb) => cb.timeoutId !== timeoutId);
cb(this._didWork, this.getPendingTasks());
}, timeout);
}
this._callbacks.push(<WaitCallback>{doneCb: cb, timeoutId: timeoutId, updateCb: updateCb});
}

/**
* Wait for angular to be stable with a timeout. If the timeout is hit before Angular becomes
* stable, the callback receives a list of the macro tasks that were pending, otherwise null.
*
* @param doneCb The callback to invoke when Angular is stable or the timeout expires
* whichever comes first.
* @param timeout Optional. The maximum time to wait for Angular to become stable. If not
* specified, whenStable() will wait forever.
* @param updateCb Optional. If specified, this callback will be invoked whenever the set of
* pending macrotasks changes. If this callback returns true doneCb will not be invoked.
*
*/
whenStable(doneCb: DoneCallback, timeout?: number, updateCb?: UpdateCallback): void {
this.addCallback(doneCb, timeout, updateCb);
this._runCallbacksIfReady();
}

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/zone/ng_zone.ts
Expand Up @@ -107,6 +107,10 @@ export class NgZone {
this.inner = this.inner.fork((Zone as any)['wtfZoneSpec']);
}

if ((Zone as any)['TaskTrackingZoneSpec']) {
this.inner = this.inner.fork(new ((Zone as any)['TaskTrackingZoneSpec'] as any));
}

if (enableLongStackTrace && (Zone as any)['longStackTraceZoneSpec']) {
this.inner = this.inner.fork((Zone as any)['longStackTraceZoneSpec']);
}
Expand Down

0 comments on commit a28fdfb

Please sign in to comment.