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
This commit is contained in:
parent
b1365d1fa8
commit
37fedd001c
|
@ -57,6 +57,7 @@ filegroup(
|
||||||
"//:node_modules/zone.js/dist/async-test.js",
|
"//:node_modules/zone.js/dist/async-test.js",
|
||||||
"//:node_modules/zone.js/dist/sync-test.js",
|
"//:node_modules/zone.js/dist/sync-test.js",
|
||||||
"//:node_modules/zone.js/dist/fake-async-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/proxy.js",
|
||||||
"//:node_modules/zone.js/dist/jasmine-patch.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/core-js/client/core.js',
|
||||||
'node_modules/zone.js/dist/zone.js',
|
'node_modules/zone.js/dist/zone.js',
|
||||||
'node_modules/zone.js/dist/long-stack-trace-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/proxy.js',
|
||||||
'node_modules/zone.js/dist/sync-test.js',
|
'node_modules/zone.js/dist/sync-test.js',
|
||||||
'node_modules/zone.js/dist/jasmine-patch.js',
|
'node_modules/zone.js/dist/jasmine-patch.js',
|
||||||
|
|
|
@ -10,9 +10,11 @@ PublicTestability.prototype.isStable = function() {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?} callback
|
* @param {?} callback
|
||||||
|
* @param {?} timeout
|
||||||
|
* @param {?} updateCallback
|
||||||
* @return {?}
|
* @return {?}
|
||||||
*/
|
*/
|
||||||
PublicTestability.prototype.whenStable = function(callback) {};
|
PublicTestability.prototype.whenStable = function(callback, timeout, updateCallback) {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?} using
|
* @param {?} using
|
||||||
|
@ -20,4 +22,4 @@ PublicTestability.prototype.whenStable = function(callback) {};
|
||||||
* @param {?} exactMatch
|
* @param {?} exactMatch
|
||||||
* @return {?}
|
* @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 {
|
export declare interface PublicTestability {
|
||||||
isStable(): boolean;
|
isStable(): boolean;
|
||||||
whenStable(callback: Function): void;
|
whenStable(callback: Function, timeout?: number, updateCallback?: Function): void;
|
||||||
findProviders(using: any, provider: string, exactMatch: boolean): any[];
|
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 Testability service provides testing hooks that can be accessed from
|
||||||
* the browser and by services such as Protractor. Each bootstrapped Angular
|
* the browser and by services such as Protractor. Each bootstrapped Angular
|
||||||
|
@ -30,23 +51,25 @@ export declare interface PublicTestability {
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Testability implements PublicTestability {
|
export class Testability implements PublicTestability {
|
||||||
/** @internal */
|
private _pendingCount: number = 0;
|
||||||
_pendingCount: number = 0;
|
private _isZoneStable: boolean = true;
|
||||||
/** @internal */
|
|
||||||
_isZoneStable: boolean = true;
|
|
||||||
/**
|
/**
|
||||||
* Whether any work was done since the last 'whenStable' callback. This is
|
* Whether any work was done since the last 'whenStable' callback. This is
|
||||||
* useful to detect if this could have potentially destabilized another
|
* useful to detect if this could have potentially destabilized another
|
||||||
* component while it is stabilizing.
|
* component while it is stabilizing.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
_didWork: boolean = false;
|
private _didWork: boolean = false;
|
||||||
/** @internal */
|
private _callbacks: WaitCallback[] = [];
|
||||||
_callbacks: Function[] = [];
|
|
||||||
constructor(private _ngZone: NgZone) { this._watchAngularEvents(); }
|
|
||||||
|
|
||||||
/** @internal */
|
private taskTrackingZone: any;
|
||||||
_watchAngularEvents(): void {
|
|
||||||
|
constructor(private _ngZone: NgZone) {
|
||||||
|
this._watchAngularEvents();
|
||||||
|
_ngZone.run(() => { this.taskTrackingZone = Zone.current.get('TaskTrackingZone'); });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _watchAngularEvents(): void {
|
||||||
this._ngZone.onUnstable.subscribe({
|
this._ngZone.onUnstable.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this._didWork = true;
|
this._didWork = true;
|
||||||
|
@ -69,6 +92,7 @@ export class Testability implements PublicTestability {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increases the number of pending request
|
* Increases the number of pending request
|
||||||
|
* @deprecated pending requests are now tracked with zones.
|
||||||
*/
|
*/
|
||||||
increasePendingRequestCount(): number {
|
increasePendingRequestCount(): number {
|
||||||
this._pendingCount += 1;
|
this._pendingCount += 1;
|
||||||
|
@ -78,6 +102,7 @@ export class Testability implements PublicTestability {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decreases the number of pending request
|
* Decreases the number of pending request
|
||||||
|
* @deprecated pending requests are now tracked with zones
|
||||||
*/
|
*/
|
||||||
decreasePendingRequestCount(): number {
|
decreasePendingRequestCount(): number {
|
||||||
this._pendingCount -= 1;
|
this._pendingCount -= 1;
|
||||||
|
@ -92,36 +117,93 @@ export class Testability implements PublicTestability {
|
||||||
* Whether an associated application is stable
|
* Whether an associated application is stable
|
||||||
*/
|
*/
|
||||||
isStable(): boolean {
|
isStable(): boolean {
|
||||||
return this._isZoneStable && this._pendingCount == 0 && !this._ngZone.hasPendingMacrotasks;
|
return this._isZoneStable && this._pendingCount === 0 && !this._ngZone.hasPendingMacrotasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
private _runCallbacksIfReady(): void {
|
||||||
_runCallbacksIfReady(): void {
|
|
||||||
if (this.isStable()) {
|
if (this.isStable()) {
|
||||||
// Schedules the call backs in a new frame so that it is always async.
|
// Schedules the call backs in a new frame so that it is always async.
|
||||||
scheduleMicroTask(() => {
|
scheduleMicroTask(() => {
|
||||||
while (this._callbacks.length !== 0) {
|
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;
|
this._didWork = false;
|
||||||
});
|
});
|
||||||
} else {
|
} 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;
|
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
|
* Wait for the application to be stable with a timeout. If the timeout is reached before that
|
||||||
* @param callback function to be called after the application is stable
|
* 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 {
|
whenStable(doneCb: Function, timeout?: number, updateCb?: Function): void {
|
||||||
this._callbacks.push(callback);
|
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();
|
this._runCallbacksIfReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the number of pending requests
|
* Get the number of pending requests
|
||||||
|
* @deprecated pending requests are now tracked with zones
|
||||||
*/
|
*/
|
||||||
getPendingRequestCount(): number { return this._pendingCount; }
|
getPendingRequestCount(): number { return this._pendingCount; }
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,10 @@ export class NgZone {
|
||||||
self._inner = self._inner.fork((Zone as any)['wtfZoneSpec']);
|
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']) {
|
if (enableLongStackTrace && (Zone as any)['longStackTraceZoneSpec']) {
|
||||||
self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']);
|
self._inner = self._inner.fork((Zone as any)['longStackTraceZoneSpec']);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,13 @@
|
||||||
|
|
||||||
import {EventEmitter} from '@angular/core';
|
import {EventEmitter} from '@angular/core';
|
||||||
import {Injectable} from '@angular/core/src/di';
|
import {Injectable} from '@angular/core/src/di';
|
||||||
import {Testability, TestabilityRegistry} from '@angular/core/src/testability/testability';
|
import {PendingMacrotask, Testability, TestabilityRegistry} from '@angular/core/src/testability/testability';
|
||||||
import {NgZone} from '@angular/core/src/zone/ng_zone';
|
import {NgZone} from '@angular/core/src/zone/ng_zone';
|
||||||
import {AsyncTestCompleter, SpyObject, beforeEach, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal';
|
import {async, fakeAsync, flush, tick} from '@angular/core/testing';
|
||||||
|
import {SpyObject, beforeEach, describe, expect, it} from '@angular/core/testing/src/testing_internal';
|
||||||
|
|
||||||
import {scheduleMicroTask} from '../../src/util';
|
import {scheduleMicroTask} from '../../src/util';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Schedules a microtasks (using a resolved promise .then())
|
// Schedules a microtasks (using a resolved promise .then())
|
||||||
function microTask(fn: Function): void {
|
function microTask(fn: Function): void {
|
||||||
scheduleMicroTask(() => {
|
scheduleMicroTask(() => {
|
||||||
|
@ -49,26 +48,25 @@ class MockNgZone extends NgZone {
|
||||||
let testability: Testability;
|
let testability: Testability;
|
||||||
let execute: any;
|
let execute: any;
|
||||||
let execute2: any;
|
let execute2: any;
|
||||||
|
let updateCallback: any;
|
||||||
let ngZone: MockNgZone;
|
let ngZone: MockNgZone;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async(() => {
|
||||||
ngZone = new MockNgZone();
|
ngZone = new MockNgZone();
|
||||||
testability = new Testability(ngZone);
|
testability = new Testability(ngZone);
|
||||||
execute = new SpyObject().spy('execute');
|
execute = new SpyObject().spy('execute');
|
||||||
execute2 = new SpyObject().spy('execute');
|
execute2 = new SpyObject().spy('execute');
|
||||||
});
|
updateCallback = new SpyObject().spy('execute');
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Pending count logic', () => {
|
describe('Pending count logic', () => {
|
||||||
it('should start with a pending count of 0',
|
it('should start with a pending count of 0',
|
||||||
() => { expect(testability.getPendingRequestCount()).toEqual(0); });
|
() => { expect(testability.getPendingRequestCount()).toEqual(0); });
|
||||||
|
|
||||||
it('should fire whenstable callbacks if pending count is 0',
|
it('should fire whenstable callbacks if pending count is 0', async(() => {
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
microTask(() => {
|
|
||||||
expect(execute).toHaveBeenCalled();
|
microTask(() => { expect(execute).toHaveBeenCalled(); });
|
||||||
async.done();
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should not fire whenstable callbacks synchronously if pending count is 0', () => {
|
it('should not fire whenstable callbacks synchronously if pending count is 0', () => {
|
||||||
|
@ -76,8 +74,7 @@ class MockNgZone extends NgZone {
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call whenstable callbacks when there are pending counts',
|
it('should not call whenstable callbacks when there are pending counts', async(() => {
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
||||||
testability.increasePendingRequestCount();
|
testability.increasePendingRequestCount();
|
||||||
testability.increasePendingRequestCount();
|
testability.increasePendingRequestCount();
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
|
@ -86,15 +83,11 @@ class MockNgZone extends NgZone {
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
testability.decreasePendingRequestCount();
|
testability.decreasePendingRequestCount();
|
||||||
|
|
||||||
microTask(() => {
|
microTask(() => { expect(execute).not.toHaveBeenCalled(); });
|
||||||
expect(execute).not.toHaveBeenCalled();
|
|
||||||
async.done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should fire whenstable callbacks when pending drops to 0',
|
it('should fire whenstable callbacks when pending drops to 0', async(() => {
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
||||||
testability.increasePendingRequestCount();
|
testability.increasePendingRequestCount();
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
|
|
||||||
|
@ -102,62 +95,151 @@ class MockNgZone extends NgZone {
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
testability.decreasePendingRequestCount();
|
testability.decreasePendingRequestCount();
|
||||||
|
|
||||||
microTask(() => {
|
microTask(() => { expect(execute).toHaveBeenCalled(); });
|
||||||
expect(execute).toHaveBeenCalled();
|
|
||||||
async.done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should not fire whenstable callbacks synchronously when pending drops to 0', () => {
|
it('should not fire whenstable callbacks synchronously when pending drops to 0', async(() => {
|
||||||
testability.increasePendingRequestCount();
|
testability.increasePendingRequestCount();
|
||||||
testability.whenStable(execute);
|
|
||||||
testability.decreasePendingRequestCount();
|
|
||||||
|
|
||||||
expect(execute).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fire whenstable callbacks with didWork if pending count is 0',
|
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
|
testability.decreasePendingRequestCount();
|
||||||
|
|
||||||
|
expect(execute).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should fire whenstable callbacks with didWork if pending count is 0', async(() => {
|
||||||
microTask(() => {
|
microTask(() => {
|
||||||
expect(execute).toHaveBeenCalledWith(false);
|
testability.whenStable(execute);
|
||||||
async.done();
|
|
||||||
|
microTask(() => { expect(execute).toHaveBeenCalledWith(false); });
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should fire whenstable callbacks with didWork when pending drops to 0',
|
it('should fire whenstable callbacks with didWork when pending drops to 0', async(() => {
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
||||||
testability.increasePendingRequestCount();
|
testability.increasePendingRequestCount();
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
|
|
||||||
|
testability.decreasePendingRequestCount();
|
||||||
|
|
||||||
microTask(() => {
|
microTask(() => {
|
||||||
testability.decreasePendingRequestCount();
|
expect(execute).toHaveBeenCalledWith(true);
|
||||||
|
testability.whenStable(execute2);
|
||||||
|
|
||||||
microTask(() => {
|
microTask(() => { expect(execute2).toHaveBeenCalledWith(false); });
|
||||||
expect(execute).toHaveBeenCalledWith(true);
|
|
||||||
testability.whenStable(execute2);
|
|
||||||
|
|
||||||
microTask(() => {
|
|
||||||
expect(execute2).toHaveBeenCalledWith(false);
|
|
||||||
async.done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('NgZone callback logic', () => {
|
describe('NgZone callback logic', () => {
|
||||||
it('should fire whenstable callback if event is already finished',
|
describe('whenStable with timeout', () => {
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
it('should list pending tasks when the timeout is hit', fakeAsync(() => {
|
||||||
|
const id = ngZone.run(() => setTimeout(() => {}, 1000));
|
||||||
|
testability.whenStable(execute, 200);
|
||||||
|
|
||||||
|
expect(execute).not.toHaveBeenCalled();
|
||||||
|
tick(200);
|
||||||
|
expect(execute).toHaveBeenCalled();
|
||||||
|
const tasks = execute.calls.mostRecent().args[1] as PendingMacrotask[];
|
||||||
|
|
||||||
|
expect(tasks.length).toEqual(1);
|
||||||
|
expect(tasks[0].delay).toEqual(1000);
|
||||||
|
expect(tasks[0].source).toEqual('setTimeout');
|
||||||
|
expect(tasks[0].isPeriodic).toEqual(false);
|
||||||
|
|
||||||
|
clearTimeout(id);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should fire if Angular is already stable', async(() => {
|
||||||
|
testability.whenStable(execute, 200);
|
||||||
|
|
||||||
|
microTask(() => { expect(execute).toHaveBeenCalled(); });
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should fire when macroTasks are cancelled', fakeAsync(() => {
|
||||||
|
const id = ngZone.run(() => setTimeout(() => {}, 1000));
|
||||||
|
testability.whenStable(execute, 500);
|
||||||
|
|
||||||
|
tick(200);
|
||||||
|
ngZone.run(() => clearTimeout(id));
|
||||||
|
// fakeAsync doesn't trigger NgZones whenStable
|
||||||
|
ngZone.stable();
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
expect(execute).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('calls the done callback when angular is stable', fakeAsync(() => {
|
||||||
|
let timeout1Done = false;
|
||||||
|
ngZone.run(() => setTimeout(() => timeout1Done = true, 500));
|
||||||
|
testability.whenStable(execute, 1000);
|
||||||
|
|
||||||
|
tick(600);
|
||||||
|
ngZone.stable();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(timeout1Done).toEqual(true);
|
||||||
|
expect(execute).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Should cancel the done timeout.
|
||||||
|
tick(500);
|
||||||
|
ngZone.stable();
|
||||||
|
tick();
|
||||||
|
expect(execute.calls.count()).toEqual(1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
it('calls update when macro tasks change', fakeAsync(() => {
|
||||||
|
let timeout1Done = false;
|
||||||
|
let timeout2Done = false;
|
||||||
|
ngZone.run(() => setTimeout(() => timeout1Done = true, 500));
|
||||||
|
tick();
|
||||||
|
testability.whenStable(execute, 1000, updateCallback);
|
||||||
|
|
||||||
|
tick(100);
|
||||||
|
ngZone.run(() => setTimeout(() => timeout2Done = true, 300));
|
||||||
|
expect(updateCallback.calls.count()).toEqual(1);
|
||||||
|
tick(600);
|
||||||
|
|
||||||
|
expect(timeout1Done).toEqual(true);
|
||||||
|
expect(timeout2Done).toEqual(true);
|
||||||
|
expect(updateCallback.calls.count()).toEqual(3);
|
||||||
|
expect(execute).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const update1 = updateCallback.calls.all()[0].args[0] as PendingMacrotask[];
|
||||||
|
expect(update1[0].delay).toEqual(500);
|
||||||
|
|
||||||
|
const update2 = updateCallback.calls.all()[1].args[0] as PendingMacrotask[];
|
||||||
|
expect(update2[0].delay).toEqual(500);
|
||||||
|
expect(update2[1].delay).toEqual(300);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('cancels the done callback if the update callback returns true', fakeAsync(() => {
|
||||||
|
let timeoutDone = false;
|
||||||
|
ngZone.unstable();
|
||||||
|
execute2.and.returnValue(true);
|
||||||
|
testability.whenStable(execute, 1000, execute2);
|
||||||
|
|
||||||
|
tick(100);
|
||||||
|
ngZone.run(() => setTimeout(() => timeoutDone = true, 500));
|
||||||
|
ngZone.stable();
|
||||||
|
expect(execute2).toHaveBeenCalled();
|
||||||
|
|
||||||
|
tick(500);
|
||||||
|
ngZone.stable();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(execute).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fire whenstable callback if event is already finished', fakeAsync(() => {
|
||||||
ngZone.unstable();
|
ngZone.unstable();
|
||||||
ngZone.stable();
|
ngZone.stable();
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).toHaveBeenCalled();
|
expect(execute).toHaveBeenCalled();
|
||||||
async.done();
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should not fire whenstable callbacks synchronously if event is already finished', () => {
|
it('should not fire whenstable callbacks synchronously if event is already finished', () => {
|
||||||
|
@ -168,20 +250,16 @@ class MockNgZone extends NgZone {
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fire whenstable callback when event finishes',
|
it('should fire whenstable callback when event finishes', fakeAsync(() => {
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
||||||
ngZone.unstable();
|
ngZone.unstable();
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
ngZone.stable();
|
ngZone.stable();
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).toHaveBeenCalled();
|
expect(execute).toHaveBeenCalled();
|
||||||
async.done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should not fire whenstable callbacks synchronously when event finishes', () => {
|
it('should not fire whenstable callbacks synchronously when event finishes', () => {
|
||||||
|
@ -192,91 +270,72 @@ class MockNgZone extends NgZone {
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not fire whenstable callback when event did not finish',
|
it('should not fire whenstable callback when event did not finish', fakeAsync(() => {
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
||||||
ngZone.unstable();
|
ngZone.unstable();
|
||||||
testability.increasePendingRequestCount();
|
testability.increasePendingRequestCount();
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
testability.decreasePendingRequestCount();
|
testability.decreasePendingRequestCount();
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
ngZone.stable();
|
ngZone.stable();
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).toHaveBeenCalled();
|
expect(execute).toHaveBeenCalled();
|
||||||
async.done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should not fire whenstable callback when there are pending counts',
|
it('should not fire whenstable callback when there are pending counts', fakeAsync(() => {
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
||||||
ngZone.unstable();
|
ngZone.unstable();
|
||||||
testability.increasePendingRequestCount();
|
testability.increasePendingRequestCount();
|
||||||
testability.increasePendingRequestCount();
|
testability.increasePendingRequestCount();
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
ngZone.stable();
|
ngZone.stable();
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
testability.decreasePendingRequestCount();
|
testability.decreasePendingRequestCount();
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).not.toHaveBeenCalled();
|
expect(execute).not.toHaveBeenCalled();
|
||||||
testability.decreasePendingRequestCount();
|
testability.decreasePendingRequestCount();
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).toHaveBeenCalled();
|
expect(execute).toHaveBeenCalled();
|
||||||
async.done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should fire whenstable callback with didWork if event is already finished',
|
it('should fire whenstable callback with didWork if event is already finished',
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
fakeAsync(() => {
|
||||||
ngZone.unstable();
|
ngZone.unstable();
|
||||||
ngZone.stable();
|
ngZone.stable();
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).toHaveBeenCalledWith(true);
|
expect(execute).toHaveBeenCalledWith(true);
|
||||||
testability.whenStable(execute2);
|
testability.whenStable(execute2);
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute2).toHaveBeenCalledWith(false);
|
expect(execute2).toHaveBeenCalledWith(false);
|
||||||
async.done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should fire whenstable callback with didwork when event finishes',
|
it('should fire whenstable callback with didwork when event finishes', fakeAsync(() => {
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
||||||
ngZone.unstable();
|
ngZone.unstable();
|
||||||
testability.whenStable(execute);
|
testability.whenStable(execute);
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
ngZone.stable();
|
ngZone.stable();
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute).toHaveBeenCalledWith(true);
|
expect(execute).toHaveBeenCalledWith(true);
|
||||||
testability.whenStable(execute2);
|
testability.whenStable(execute2);
|
||||||
|
|
||||||
microTask(() => {
|
tick();
|
||||||
expect(execute2).toHaveBeenCalledWith(false);
|
expect(execute2).toHaveBeenCalledWith(false);
|
||||||
async.done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -284,39 +343,39 @@ class MockNgZone extends NgZone {
|
||||||
describe('TestabilityRegistry', () => {
|
describe('TestabilityRegistry', () => {
|
||||||
let testability1: Testability;
|
let testability1: Testability;
|
||||||
let testability2: Testability;
|
let testability2: Testability;
|
||||||
let resgitry: TestabilityRegistry;
|
let registry: TestabilityRegistry;
|
||||||
let ngZone: MockNgZone;
|
let ngZone: MockNgZone;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async(() => {
|
||||||
ngZone = new MockNgZone();
|
ngZone = new MockNgZone();
|
||||||
testability1 = new Testability(ngZone);
|
testability1 = new Testability(ngZone);
|
||||||
testability2 = new Testability(ngZone);
|
testability2 = new Testability(ngZone);
|
||||||
resgitry = new TestabilityRegistry();
|
registry = new TestabilityRegistry();
|
||||||
});
|
}));
|
||||||
describe('unregister testability', () => {
|
describe('unregister testability', () => {
|
||||||
it('should remove the testability when unregistering an existing testability', () => {
|
it('should remove the testability when unregistering an existing testability', () => {
|
||||||
resgitry.registerApplication('testability1', testability1);
|
registry.registerApplication('testability1', testability1);
|
||||||
resgitry.registerApplication('testability2', testability2);
|
registry.registerApplication('testability2', testability2);
|
||||||
resgitry.unregisterApplication('testability2');
|
registry.unregisterApplication('testability2');
|
||||||
expect(resgitry.getAllTestabilities().length).toEqual(1);
|
expect(registry.getAllTestabilities().length).toEqual(1);
|
||||||
expect(resgitry.getTestability('testability1')).toEqual(testability1);
|
expect(registry.getTestability('testability1')).toEqual(testability1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remain the same when unregistering a non-existing testability', () => {
|
it('should remain the same when unregistering a non-existing testability', () => {
|
||||||
expect(resgitry.getAllTestabilities().length).toEqual(0);
|
expect(registry.getAllTestabilities().length).toEqual(0);
|
||||||
resgitry.registerApplication('testability1', testability1);
|
registry.registerApplication('testability1', testability1);
|
||||||
resgitry.registerApplication('testability2', testability2);
|
registry.registerApplication('testability2', testability2);
|
||||||
resgitry.unregisterApplication('testability3');
|
registry.unregisterApplication('testability3');
|
||||||
expect(resgitry.getAllTestabilities().length).toEqual(2);
|
expect(registry.getAllTestabilities().length).toEqual(2);
|
||||||
expect(resgitry.getTestability('testability1')).toEqual(testability1);
|
expect(registry.getTestability('testability1')).toEqual(testability1);
|
||||||
expect(resgitry.getTestability('testability2')).toEqual(testability2);
|
expect(registry.getTestability('testability2')).toEqual(testability2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove all the testability when unregistering all testabilities', () => {
|
it('should remove all the testability when unregistering all testabilities', () => {
|
||||||
resgitry.registerApplication('testability1', testability1);
|
registry.registerApplication('testability1', testability1);
|
||||||
resgitry.registerApplication('testability2', testability2);
|
registry.registerApplication('testability2', testability2);
|
||||||
resgitry.unregisterAllApplications();
|
registry.unregisterAllApplications();
|
||||||
expect(resgitry.getAllTestabilities().length).toEqual(0);
|
expect(registry.getAllTestabilities().length).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
(function(global: any) {
|
(function(global: any) {
|
||||||
writeScriptTag('/vendor/zone.js');
|
writeScriptTag('/vendor/zone.js');
|
||||||
|
writeScriptTag('/vendor/task-tracking.js');
|
||||||
writeScriptTag('/vendor/system.js');
|
writeScriptTag('/vendor/system.js');
|
||||||
writeScriptTag('/vendor/Reflect.js');
|
writeScriptTag('/vendor/Reflect.js');
|
||||||
writeScriptTag('/_common/system-config.js');
|
writeScriptTag('/_common/system-config.js');
|
||||||
|
|
|
@ -22,6 +22,7 @@ ln -s ../../../dist/packages-dist/ $DIST/vendor/@angular
|
||||||
for FILE in \
|
for FILE in \
|
||||||
../../../node_modules/angular/angular.js \
|
../../../node_modules/angular/angular.js \
|
||||||
../../../node_modules/zone.js/dist/zone.js \
|
../../../node_modules/zone.js/dist/zone.js \
|
||||||
|
../../../node_modules/zone.js/dist/task-tracking.js \
|
||||||
../../../node_modules/systemjs/dist/system.js \
|
../../../node_modules/systemjs/dist/system.js \
|
||||||
../../../node_modules/reflect-metadata/Reflect.js \
|
../../../node_modules/reflect-metadata/Reflect.js \
|
||||||
../../../node_modules/rxjs
|
../../../node_modules/rxjs
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {browser, by, element} from 'protractor';
|
||||||
|
import {verifyNoBrowserErrors} from '../../../../../_common/e2e_util';
|
||||||
|
|
||||||
|
describe('testability example', () => {
|
||||||
|
afterEach(verifyNoBrowserErrors);
|
||||||
|
|
||||||
|
describe('using task tracking', () => {
|
||||||
|
const URL = '/core/testability/ts/whenStable/';
|
||||||
|
|
||||||
|
it('times out with a list of tasks', (done) => {
|
||||||
|
browser.get(URL);
|
||||||
|
browser.ignoreSynchronization = true;
|
||||||
|
|
||||||
|
// Script that runs in the browser and calls whenStable with a timeout.
|
||||||
|
let waitWithResultScript = function(done: any) {
|
||||||
|
let rootEl = document.querySelector('example-app');
|
||||||
|
let testability = (window as any).getAngularTestability(rootEl);
|
||||||
|
testability.whenStable((didWork: boolean, tasks: any) => { done(tasks); }, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
element(by.css('.start-button')).click();
|
||||||
|
|
||||||
|
browser.driver.executeAsyncScript(waitWithResultScript).then((result: any[]) => {
|
||||||
|
let pendingTask = result[0];
|
||||||
|
expect(pendingTask.delay).toEqual(5000);
|
||||||
|
expect(pendingTask.source).toEqual('setTimeout');
|
||||||
|
expect(element(by.css('.status')).getText()).not.toContain('done');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => { browser.ignoreSynchronization = false; });
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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.io/license
|
||||||
|
*/
|
||||||
|
export {AppModule} from './testability_example';
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'example-app',
|
||||||
|
template: `
|
||||||
|
<button class="start-button" (click)="start()">Start long-running task</button>
|
||||||
|
<div class="status">Status: {{status}}</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class StableTestCmp {
|
||||||
|
status = 'none';
|
||||||
|
start() {
|
||||||
|
this.status = 'running';
|
||||||
|
setTimeout(() => { this.status = 'done'; }, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({imports: [BrowserModule], declarations: [StableTestCmp], bootstrap: [StableTestCmp]})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ require('zone.js/dist/zone-node.js');
|
||||||
var JasmineRunner = require('jasmine');
|
var JasmineRunner = require('jasmine');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
require('zone.js/dist/long-stack-trace-zone.js');
|
require('zone.js/dist/long-stack-trace-zone.js');
|
||||||
|
require('zone.js/dist/task-tracking.js');
|
||||||
require('zone.js/dist/proxy.js');
|
require('zone.js/dist/proxy.js');
|
||||||
require('zone.js/dist/sync-test.js');
|
require('zone.js/dist/sync-test.js');
|
||||||
require('zone.js/dist/async-test.js');
|
require('zone.js/dist/async-test.js');
|
||||||
|
|
|
@ -16,6 +16,7 @@ var JasmineRunner = require('jasmine');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
require('source-map-support').install();
|
require('source-map-support').install();
|
||||||
require('zone.js/dist/long-stack-trace-zone.js');
|
require('zone.js/dist/long-stack-trace-zone.js');
|
||||||
|
require('zone.js/dist/task-tracking.js');
|
||||||
require('zone.js/dist/proxy.js');
|
require('zone.js/dist/proxy.js');
|
||||||
require('zone.js/dist/sync-test.js');
|
require('zone.js/dist/sync-test.js');
|
||||||
require('zone.js/dist/async-test.js');
|
require('zone.js/dist/async-test.js');
|
||||||
|
|
|
@ -998,12 +998,12 @@ export declare abstract class TemplateRef<C> {
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare class Testability implements PublicTestability {
|
export declare class Testability implements PublicTestability {
|
||||||
constructor(_ngZone: NgZone);
|
constructor(_ngZone: NgZone);
|
||||||
decreasePendingRequestCount(): number;
|
/** @deprecated */ decreasePendingRequestCount(): number;
|
||||||
findProviders(using: any, provider: string, exactMatch: boolean): any[];
|
findProviders(using: any, provider: string, exactMatch: boolean): any[];
|
||||||
getPendingRequestCount(): number;
|
/** @deprecated */ getPendingRequestCount(): number;
|
||||||
increasePendingRequestCount(): number;
|
/** @deprecated */ increasePendingRequestCount(): number;
|
||||||
isStable(): boolean;
|
isStable(): boolean;
|
||||||
whenStable(callback: Function): void;
|
whenStable(doneCb: Function, timeout?: number, updateCb?: Function): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
|
|
|
@ -12,6 +12,7 @@ import 'zone.js/dist/proxy.js';
|
||||||
import 'zone.js/dist/sync-test.js';
|
import 'zone.js/dist/sync-test.js';
|
||||||
import 'zone.js/dist/async-test.js';
|
import 'zone.js/dist/async-test.js';
|
||||||
import 'zone.js/dist/fake-async-test.js';
|
import 'zone.js/dist/fake-async-test.js';
|
||||||
|
import 'zone.js/dist/task-tracking.js';
|
||||||
import 'reflect-metadata/Reflect';
|
import 'reflect-metadata/Reflect';
|
||||||
|
|
||||||
// This hack is needed to get jasmine, node and zone working inside bazel.
|
// This hack is needed to get jasmine, node and zone working inside bazel.
|
||||||
|
|
Loading…
Reference in New Issue