-
Notifications
You must be signed in to change notification settings - Fork 26.2k
/
Copy pathtestability.ts
330 lines (297 loc) Β· 9.82 KB
/
testability.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {Inject, Injectable, InjectionToken} from '../di';
import {NgZone} from '../zone/ng_zone';
/**
* Testability API.
* `declare` keyword causes tsickle to generate externs, so these methods are
* not renamed by Closure Compiler.
* @publicApi
*/
export declare interface PublicTestability {
isStable(): boolean;
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;
creationLocation: Error;
runCount?: number;
data?: TaskData;
}
export interface TaskData {
target?: XMLHttpRequest;
delay?: number;
isPeriodic?: boolean;
}
interface WaitCallback {
// Needs to be 'any' - setTimeout returns a number according to ES6, but
// on NodeJS it returns a Timer.
timeoutId: any;
doneCb: Function;
updateCb?: Function;
}
/**
* Internal injection token that can used to access an instance of a Testability class.
*
* This token acts as a bridge between the core bootstrap code and the `Testability` class. This is
* needed to ensure that there are no direct references to the `Testability` class, so it can be
* tree-shaken away (if not referenced). For the environments/setups when the `Testability` class
* should be available, this token is used to add a provider that references the `Testability`
* class. Otherwise, only this token is retained in a bundle, but the `Testability` class is not.
*/
export const TESTABILITY = new InjectionToken<Testability>('');
/**
* Internal injection token to retrieve Testability getter class instance.
*/
export const TESTABILITY_GETTER = new InjectionToken<GetTestability>('');
/**
* The Testability service provides testing hooks that can be accessed from
* the browser.
*
* Angular applications bootstrapped using an NgModule (via `@NgModule.bootstrap` field) will also
* instantiate Testability by default (in both development and production modes).
*
* For applications bootstrapped using the `bootstrapApplication` function, Testability is not
* included by default. You can include it into your applications by getting the list of necessary
* providers using the `provideProtractorTestingSupport()` function and adding them into the
* `options.providers` array. Example:
*
* ```ts
* import {provideProtractorTestingSupport} from '@angular/platform-browser';
*
* await bootstrapApplication(RootComponent, providers: [provideProtractorTestingSupport()]);
* ```
*
* @publicApi
*/
@Injectable()
export class Testability implements PublicTestability {
private _isZoneStable: boolean = true;
private _callbacks: WaitCallback[] = [];
private taskTrackingZone: {macroTasks: Task[]} | null = null;
constructor(
private _ngZone: NgZone,
private registry: TestabilityRegistry,
@Inject(TESTABILITY_GETTER) testabilityGetter: GetTestability,
) {
// If there was no Testability logic registered in the global scope
// before, register the current testability getter as a global one.
if (!_testabilityGetter) {
setTestabilityGetter(testabilityGetter);
testabilityGetter.addToWindow(registry);
}
this._watchAngularEvents();
_ngZone.run(() => {
this.taskTrackingZone =
typeof Zone == 'undefined' ? null : Zone.current.get('TaskTrackingZone');
});
}
private _watchAngularEvents(): void {
this._ngZone.onUnstable.subscribe({
next: () => {
this._isZoneStable = false;
},
});
this._ngZone.runOutsideAngular(() => {
this._ngZone.onStable.subscribe({
next: () => {
NgZone.assertNotInAngularZone();
queueMicrotask(() => {
this._isZoneStable = true;
this._runCallbacksIfReady();
});
},
});
});
}
/**
* Whether an associated application is stable
*/
isStable(): boolean {
return this._isZoneStable && !this._ngZone.hasPendingMacrotasks;
}
private _runCallbacksIfReady(): void {
if (this.isStable()) {
// Schedules the call backs in a new frame so that it is always async.
queueMicrotask(() => {
while (this._callbacks.length !== 0) {
let cb = this._callbacks.pop()!;
clearTimeout(cb.timeoutId);
cb.doneCb();
}
});
} else {
// 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;
});
}
}
private getPendingTasks(): PendingMacrotask[] {
if (!this.taskTrackingZone) {
return [];
}
// Copy the tasks data so that we don't leak tasks.
return this.taskTrackingZone.macroTasks.map((t: Task) => {
return {
source: t.source,
// From TaskTrackingZone:
// https://github.com/angular/zone.js/blob/master/lib/zone-spec/task-tracking.ts#L40
creationLocation: (t as any).creationLocation as Error,
data: t.data,
};
});
}
private addCallback(cb: Function, timeout?: number, updateCb?: Function) {
let timeoutId: any = -1;
if (timeout && timeout > 0) {
timeoutId = setTimeout(() => {
this._callbacks = this._callbacks.filter((cb) => cb.timeoutId !== timeoutId);
cb();
}, timeout);
}
this._callbacks.push(<WaitCallback>{doneCb: cb, timeoutId: timeoutId, updateCb: updateCb});
}
/**
* 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(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/plugins/task-tracking" loaded?',
);
}
this.addCallback(doneCb, timeout, updateCb);
this._runCallbacksIfReady();
}
/**
* Registers an application with a testability hook so that it can be tracked.
* @param token token of application, root element
*
* @internal
*/
registerApplication(token: any) {
this.registry.registerApplication(token, this);
}
/**
* Unregisters an application.
* @param token token of application, root element
*
* @internal
*/
unregisterApplication(token: any) {
this.registry.unregisterApplication(token);
}
/**
* Find providers by name
* @param using The root element to search from
* @param provider The name of binding variable
* @param exactMatch Whether using exactMatch
*/
findProviders(using: any, provider: string, exactMatch: boolean): any[] {
// TODO(juliemr): implement.
return [];
}
}
/**
* A global registry of {@link Testability} instances for specific elements.
* @publicApi
*/
@Injectable({providedIn: 'platform'})
export class TestabilityRegistry {
/** @internal */
_applications = new Map<any, Testability>();
/**
* Registers an application with a testability hook so that it can be tracked
* @param token token of application, root element
* @param testability Testability hook
*/
registerApplication(token: any, testability: Testability) {
this._applications.set(token, testability);
}
/**
* Unregisters an application.
* @param token token of application, root element
*/
unregisterApplication(token: any) {
this._applications.delete(token);
}
/**
* Unregisters all applications
*/
unregisterAllApplications() {
this._applications.clear();
}
/**
* Get a testability hook associated with the application
* @param elem root element
*/
getTestability(elem: any): Testability | null {
return this._applications.get(elem) || null;
}
/**
* Get all registered testabilities
*/
getAllTestabilities(): Testability[] {
return Array.from(this._applications.values());
}
/**
* Get all registered applications(root elements)
*/
getAllRootElements(): any[] {
return Array.from(this._applications.keys());
}
/**
* Find testability of a node in the Tree
* @param elem node
* @param findInAncestors whether finding testability in ancestors if testability was not found in
* current node
*/
findTestabilityInTree(elem: Node, findInAncestors: boolean = true): Testability | null {
return _testabilityGetter?.findTestabilityInTree(this, elem, findInAncestors) ?? null;
}
}
/**
* Adapter interface for retrieving the `Testability` service associated for a
* particular context.
*
* @publicApi
*/
export interface GetTestability {
addToWindow(registry: TestabilityRegistry): void;
findTestabilityInTree(
registry: TestabilityRegistry,
elem: any,
findInAncestors: boolean,
): Testability | null;
}
/**
* Set the {@link GetTestability} implementation used by the Angular testing framework.
* @publicApi
*/
export function setTestabilityGetter(getter: GetTestability): void {
_testabilityGetter = getter;
}
let _testabilityGetter: GetTestability | undefined;