Skip to content
Permalink
Browse files

feat(core): add task tracking to Testability (#16863)

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.

If the optional parameters are not passed, whenStable() will work
as it did before, whether or not the task tracking zone spec is
available.

This change also migrates the Testability unit tests off the deprecated
AsyncTestCompleter.

PR Close #16863
  • Loading branch information...
heathkit authored and kara committed Apr 27, 2017
1 parent b1365d1 commit 37fedd001cb559ef307146958b38e38facf532db
@@ -57,6 +57,7 @@ filegroup(
"//:node_modules/zone.js/dist/async-test.js",
"//:node_modules/zone.js/dist/sync-test.js",
"//:node_modules/zone.js/dist/fake-async-test.js",
"//:node_modules/zone.js/dist/task-tracking.js",
"//:node_modules/zone.js/dist/proxy.js",
"//:node_modules/zone.js/dist/jasmine-patch.js",
],
@@ -30,6 +30,7 @@ module.exports = function(config) {
'node_modules/core-js/client/core.js',
'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',
@@ -10,14 +10,16 @@ PublicTestability.prototype.isStable = function() {};

/**
* @param {?} callback
* @param {?} timeout
* @param {?} updateCallback
* @return {?}
*/
PublicTestability.prototype.whenStable = function(callback) {};
PublicTestability.prototype.whenStable = function(callback, timeout, updateCallback) {};

/**
* @param {?} using
* @param {?} provider
* @param {?} exactMatch
* @return {?}
*/
PublicTestability.prototype.findProviders = function(using, provider, exactMatch) {};
PublicTestability.prototype.findProviders = function(using, provider, exactMatch) {};
@@ -18,10 +18,31 @@ import {NgZone} from '../zone/ng_zone';
*/
export declare interface PublicTestability {
isStable(): boolean;
whenStable(callback: Function): void;
whenStable(callback: Function, timeout?: number, updateCallback?: Function): void;
findProviders(using: any, provider: string, exactMatch: boolean): any[];
}

// Angular internal, not intended for public API.
export interface PendingMacrotask {
source: string;
isPeriodic: boolean;
delay?: number;
creationLocation: Error;
xhr?: XMLHttpRequest;
}

// Angular internal, not intended for public API.
export type DoneCallback = (didWork: boolean, tasks?: PendingMacrotask[]) => void;
export type UpdateCallback = (tasks: PendingMacrotask[]) => boolean;

interface WaitCallback {
// Needs to be 'any' - setTimeout returns a number according to ES6, but
// on NodeJS it returns a Timer.
timeoutId: any;
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
@@ -30,23 +51,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;
@@ -69,6 +92,7 @@ export class Testability implements PublicTestability {

/**
* Increases the number of pending request
* @deprecated pending requests are now tracked with zones.
*/
increasePendingRequestCount(): number {
this._pendingCount += 1;
@@ -78,6 +102,7 @@ export class Testability implements PublicTestability {

/**
* Decreases the number of pending request
* @deprecated pending requests are now tracked with zones
*/
decreasePendingRequestCount(): number {
this._pendingCount -= 1;
@@ -92,36 +117,93 @@ export class Testability implements PublicTestability {
* Whether an associated application is stable
*/
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() !;
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;
}
}

private getPendingTasks(): PendingMacrotask[] {
if (!this.taskTrackingZone) {
return [];
}

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: any = -1;
if (timeout && 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});
}

/**
* Run callback when the application is stable
* @param callback function to be called after the application is stable
* Wait for the application to be stable with a timeout. If the timeout is reached before that
* happens, 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
* and no further updates will be issued.
*/
whenStable(callback: Function): void {
this._callbacks.push(callback);
whenStable(doneCb: Function, timeout?: number, updateCb?: Function): void {
if (updateCb && !this.taskTrackingZone) {
throw new Error(
'Task tracking zone is required when passing an update callback to ' +
'whenStable(). Is "zone.js/dist/task-tracking.js" loaded?');
}
// These arguments are 'Function' above to keep the public API simple.
this.addCallback(doneCb as DoneCallback, timeout, updateCb as UpdateCallback);
this._runCallbacksIfReady();
}

/**
* Get the number of pending requests
* @deprecated pending requests are now tracked with zones
*/
getPendingRequestCount(): number { return this._pendingCount; }

@@ -132,6 +132,10 @@ export class NgZone {
self._inner = self._inner.fork((Zone as any)['wtfZoneSpec']);
}

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

if (enableLongStackTrace && (Zone as any)['longStackTraceZoneSpec']) {
self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']);
}

0 comments on commit 37fedd0

Please sign in to comment.
You can’t perform that action at this time.