Skip to content

Commit 37fedd0

Browse files
heathkitkara
authored andcommitted
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
1 parent b1365d1 commit 37fedd0

File tree

15 files changed

+396
-163
lines changed

15 files changed

+396
-163
lines changed

BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ filegroup(
5757
"//:node_modules/zone.js/dist/async-test.js",
5858
"//:node_modules/zone.js/dist/sync-test.js",
5959
"//:node_modules/zone.js/dist/fake-async-test.js",
60+
"//:node_modules/zone.js/dist/task-tracking.js",
6061
"//:node_modules/zone.js/dist/proxy.js",
6162
"//:node_modules/zone.js/dist/jasmine-patch.js",
6263
],

karma-js.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module.exports = function(config) {
3030
'node_modules/core-js/client/core.js',
3131
'node_modules/zone.js/dist/zone.js',
3232
'node_modules/zone.js/dist/long-stack-trace-zone.js',
33+
'node_modules/zone.js/dist/task-tracking.js',
3334
'node_modules/zone.js/dist/proxy.js',
3435
'node_modules/zone.js/dist/sync-test.js',
3536
'node_modules/zone.js/dist/jasmine-patch.js',

packages/core/src/testability/testability.externs.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ PublicTestability.prototype.isStable = function() {};
1010

1111
/**
1212
* @param {?} callback
13+
* @param {?} timeout
14+
* @param {?} updateCallback
1315
* @return {?}
1416
*/
15-
PublicTestability.prototype.whenStable = function(callback) {};
17+
PublicTestability.prototype.whenStable = function(callback, timeout, updateCallback) {};
1618

1719
/**
1820
* @param {?} using
1921
* @param {?} provider
2022
* @param {?} exactMatch
2123
* @return {?}
2224
*/
23-
PublicTestability.prototype.findProviders = function(using, provider, exactMatch) {};
25+
PublicTestability.prototype.findProviders = function(using, provider, exactMatch) {};

packages/core/src/testability/testability.ts

+102-20
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,31 @@ import {NgZone} from '../zone/ng_zone';
1818
*/
1919
export declare interface PublicTestability {
2020
isStable(): boolean;
21-
whenStable(callback: Function): void;
21+
whenStable(callback: Function, timeout?: number, updateCallback?: Function): void;
2222
findProviders(using: any, provider: string, exactMatch: boolean): any[];
2323
}
2424

25+
// Angular internal, not intended for public API.
26+
export interface PendingMacrotask {
27+
source: string;
28+
isPeriodic: boolean;
29+
delay?: number;
30+
creationLocation: Error;
31+
xhr?: XMLHttpRequest;
32+
}
33+
34+
// Angular internal, not intended for public API.
35+
export type DoneCallback = (didWork: boolean, tasks?: PendingMacrotask[]) => void;
36+
export type UpdateCallback = (tasks: PendingMacrotask[]) => boolean;
37+
38+
interface WaitCallback {
39+
// Needs to be 'any' - setTimeout returns a number according to ES6, but
40+
// on NodeJS it returns a Timer.
41+
timeoutId: any;
42+
doneCb: DoneCallback;
43+
updateCb?: UpdateCallback;
44+
}
45+
2546
/**
2647
* The Testability service provides testing hooks that can be accessed from
2748
* the browser and by services such as Protractor. Each bootstrapped Angular
@@ -30,23 +51,25 @@ export declare interface PublicTestability {
3051
*/
3152
@Injectable()
3253
export class Testability implements PublicTestability {
33-
/** @internal */
34-
_pendingCount: number = 0;
35-
/** @internal */
36-
_isZoneStable: boolean = true;
54+
private _pendingCount: number = 0;
55+
private _isZoneStable: boolean = true;
3756
/**
3857
* Whether any work was done since the last 'whenStable' callback. This is
3958
* useful to detect if this could have potentially destabilized another
4059
* component while it is stabilizing.
4160
* @internal
4261
*/
43-
_didWork: boolean = false;
44-
/** @internal */
45-
_callbacks: Function[] = [];
46-
constructor(private _ngZone: NgZone) { this._watchAngularEvents(); }
62+
private _didWork: boolean = false;
63+
private _callbacks: WaitCallback[] = [];
4764

48-
/** @internal */
49-
_watchAngularEvents(): void {
65+
private taskTrackingZone: any;
66+
67+
constructor(private _ngZone: NgZone) {
68+
this._watchAngularEvents();
69+
_ngZone.run(() => { this.taskTrackingZone = Zone.current.get('TaskTrackingZone'); });
70+
}
71+
72+
private _watchAngularEvents(): void {
5073
this._ngZone.onUnstable.subscribe({
5174
next: () => {
5275
this._didWork = true;
@@ -69,6 +92,7 @@ export class Testability implements PublicTestability {
6992

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

79103
/**
80104
* Decreases the number of pending request
105+
* @deprecated pending requests are now tracked with zones
81106
*/
82107
decreasePendingRequestCount(): number {
83108
this._pendingCount -= 1;
@@ -92,36 +117,93 @@ export class Testability implements PublicTestability {
92117
* Whether an associated application is stable
93118
*/
94119
isStable(): boolean {
95-
return this._isZoneStable && this._pendingCount == 0 && !this._ngZone.hasPendingMacrotasks;
120+
return this._isZoneStable && this._pendingCount === 0 && !this._ngZone.hasPendingMacrotasks;
96121
}
97122

98-
/** @internal */
99-
_runCallbacksIfReady(): void {
123+
private _runCallbacksIfReady(): void {
100124
if (this.isStable()) {
101125
// Schedules the call backs in a new frame so that it is always async.
102126
scheduleMicroTask(() => {
103127
while (this._callbacks.length !== 0) {
104-
(this._callbacks.pop() !)(this._didWork);
128+
let cb = this._callbacks.pop() !;
129+
clearTimeout(cb.timeoutId);
130+
cb.doneCb(this._didWork);
105131
}
106132
this._didWork = false;
107133
});
108134
} else {
109-
// Not Ready
135+
// Still not stable, send updates.
136+
let pending = this.getPendingTasks();
137+
this._callbacks = this._callbacks.filter((cb) => {
138+
if (cb.updateCb && cb.updateCb(pending)) {
139+
clearTimeout(cb.timeoutId);
140+
return false;
141+
}
142+
143+
return true;
144+
});
145+
110146
this._didWork = true;
111147
}
112148
}
113149

150+
private getPendingTasks(): PendingMacrotask[] {
151+
if (!this.taskTrackingZone) {
152+
return [];
153+
}
154+
155+
return this.taskTrackingZone.macroTasks.map((t: Task) => {
156+
return {
157+
source: t.source,
158+
isPeriodic: t.data.isPeriodic,
159+
delay: t.data.delay,
160+
// From TaskTrackingZone:
161+
// https://github.com/angular/zone.js/blob/master/lib/zone-spec/task-tracking.ts#L40
162+
creationLocation: (t as any).creationLocation as Error,
163+
// Added by Zones for XHRs
164+
// https://github.com/angular/zone.js/blob/master/lib/browser/browser.ts#L133
165+
xhr: (t.data as any).target
166+
};
167+
});
168+
}
169+
170+
private addCallback(cb: DoneCallback, timeout?: number, updateCb?: UpdateCallback) {
171+
let timeoutId: any = -1;
172+
if (timeout && timeout > 0) {
173+
timeoutId = setTimeout(() => {
174+
this._callbacks = this._callbacks.filter((cb) => cb.timeoutId !== timeoutId);
175+
cb(this._didWork, this.getPendingTasks());
176+
}, timeout);
177+
}
178+
this._callbacks.push(<WaitCallback>{doneCb: cb, timeoutId: timeoutId, updateCb: updateCb});
179+
}
180+
114181
/**
115-
* Run callback when the application is stable
116-
* @param callback function to be called after the application is stable
182+
* Wait for the application to be stable with a timeout. If the timeout is reached before that
183+
* happens, the callback receives a list of the macro tasks that were pending, otherwise null.
184+
*
185+
* @param doneCb The callback to invoke when Angular is stable or the timeout expires
186+
* whichever comes first.
187+
* @param timeout Optional. The maximum time to wait for Angular to become stable. If not
188+
* specified, whenStable() will wait forever.
189+
* @param updateCb Optional. If specified, this callback will be invoked whenever the set of
190+
* pending macrotasks changes. If this callback returns true doneCb will not be invoked
191+
* and no further updates will be issued.
117192
*/
118-
whenStable(callback: Function): void {
119-
this._callbacks.push(callback);
193+
whenStable(doneCb: Function, timeout?: number, updateCb?: Function): void {
194+
if (updateCb && !this.taskTrackingZone) {
195+
throw new Error(
196+
'Task tracking zone is required when passing an update callback to ' +
197+
'whenStable(). Is "zone.js/dist/task-tracking.js" loaded?');
198+
}
199+
// These arguments are 'Function' above to keep the public API simple.
200+
this.addCallback(doneCb as DoneCallback, timeout, updateCb as UpdateCallback);
120201
this._runCallbacksIfReady();
121202
}
122203

123204
/**
124205
* Get the number of pending requests
206+
* @deprecated pending requests are now tracked with zones
125207
*/
126208
getPendingRequestCount(): number { return this._pendingCount; }
127209

packages/core/src/zone/ng_zone.ts

+4
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ export class NgZone {
132132
self._inner = self._inner.fork((Zone as any)['wtfZoneSpec']);
133133
}
134134

135+
if ((Zone as any)['TaskTrackingZoneSpec']) {
136+
self._inner = self._inner.fork(new ((Zone as any)['TaskTrackingZoneSpec'] as any));
137+
}
138+
135139
if (enableLongStackTrace && (Zone as any)['longStackTraceZoneSpec']) {
136140
self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']);
137141
}

0 commit comments

Comments
 (0)