diff --git a/BUILD.bazel b/BUILD.bazel index c982d1b877..12719d12e8 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -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", ], diff --git a/karma-js.conf.js b/karma-js.conf.js index 0138efeb36..f31417eda1 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.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', diff --git a/packages/core/src/testability/testability.externs.js b/packages/core/src/testability/testability.externs.js index 5db059634d..1954a3fcbb 100644 --- a/packages/core/src/testability/testability.externs.js +++ b/packages/core/src/testability/testability.externs.js @@ -10,9 +10,11 @@ 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 @@ -20,4 +22,4 @@ PublicTestability.prototype.whenStable = function(callback) {}; * @param {?} exactMatch * @return {?} */ -PublicTestability.prototype.findProviders = function(using, provider, exactMatch) {}; \ No newline at end of file +PublicTestability.prototype.findProviders = function(using, provider, exactMatch) {}; diff --git a/packages/core/src/testability/testability.ts b/packages/core/src/testability/testability.ts index f39e0bb823..664ffd11a5 100644 --- a/packages/core/src/testability/testability.ts +++ b/packages/core/src/testability/testability.ts @@ -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({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; } diff --git a/packages/core/src/zone/ng_zone.ts b/packages/core/src/zone/ng_zone.ts index b845e48998..bd410c78a0 100644 --- a/packages/core/src/zone/ng_zone.ts +++ b/packages/core/src/zone/ng_zone.ts @@ -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']); } diff --git a/packages/core/test/testability/testability_spec.ts b/packages/core/test/testability/testability_spec.ts index ee5e7560c5..7004eeabce 100644 --- a/packages/core/test/testability/testability_spec.ts +++ b/packages/core/test/testability/testability_spec.ts @@ -8,14 +8,13 @@ import {EventEmitter} from '@angular/core'; 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 {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'; - - // Schedules a microtasks (using a resolved promise .then()) function microTask(fn: Function): void { scheduleMicroTask(() => { @@ -49,26 +48,25 @@ class MockNgZone extends NgZone { let testability: Testability; let execute: any; let execute2: any; + let updateCallback: any; let ngZone: MockNgZone; - beforeEach(() => { + beforeEach(async(() => { ngZone = new MockNgZone(); testability = new Testability(ngZone); execute = new SpyObject().spy('execute'); execute2 = new SpyObject().spy('execute'); - }); + updateCallback = new SpyObject().spy('execute'); + })); describe('Pending count logic', () => { it('should start with a pending count of 0', () => { expect(testability.getPendingRequestCount()).toEqual(0); }); - it('should fire whenstable callbacks if pending count is 0', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + it('should fire whenstable callbacks if pending count is 0', async(() => { testability.whenStable(execute); - microTask(() => { - expect(execute).toHaveBeenCalled(); - async.done(); - }); + + microTask(() => { expect(execute).toHaveBeenCalled(); }); })); it('should not fire whenstable callbacks synchronously if pending count is 0', () => { @@ -76,8 +74,7 @@ class MockNgZone extends NgZone { expect(execute).not.toHaveBeenCalled(); }); - it('should not call whenstable callbacks when there are pending counts', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + it('should not call whenstable callbacks when there are pending counts', async(() => { testability.increasePendingRequestCount(); testability.increasePendingRequestCount(); testability.whenStable(execute); @@ -86,15 +83,11 @@ class MockNgZone extends NgZone { expect(execute).not.toHaveBeenCalled(); testability.decreasePendingRequestCount(); - microTask(() => { - expect(execute).not.toHaveBeenCalled(); - async.done(); - }); + microTask(() => { expect(execute).not.toHaveBeenCalled(); }); }); })); - it('should fire whenstable callbacks when pending drops to 0', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + it('should fire whenstable callbacks when pending drops to 0', async(() => { testability.increasePendingRequestCount(); testability.whenStable(execute); @@ -102,62 +95,151 @@ class MockNgZone extends NgZone { expect(execute).not.toHaveBeenCalled(); testability.decreasePendingRequestCount(); - microTask(() => { - expect(execute).toHaveBeenCalled(); - async.done(); - }); + microTask(() => { expect(execute).toHaveBeenCalled(); }); }); })); - it('should not fire whenstable callbacks synchronously when pending drops to 0', () => { - 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) => { + it('should not fire whenstable callbacks synchronously when pending drops to 0', async(() => { + testability.increasePendingRequestCount(); testability.whenStable(execute); + testability.decreasePendingRequestCount(); + + expect(execute).not.toHaveBeenCalled(); + })); + + it('should fire whenstable callbacks with didWork if pending count is 0', async(() => { microTask(() => { - expect(execute).toHaveBeenCalledWith(false); - async.done(); + testability.whenStable(execute); + + microTask(() => { expect(execute).toHaveBeenCalledWith(false); }); }); })); - it('should fire whenstable callbacks with didWork when pending drops to 0', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + it('should fire whenstable callbacks with didWork when pending drops to 0', async(() => { testability.increasePendingRequestCount(); testability.whenStable(execute); + testability.decreasePendingRequestCount(); + microTask(() => { - testability.decreasePendingRequestCount(); + expect(execute).toHaveBeenCalledWith(true); + testability.whenStable(execute2); - microTask(() => { - expect(execute).toHaveBeenCalledWith(true); - testability.whenStable(execute2); - - microTask(() => { - expect(execute2).toHaveBeenCalledWith(false); - async.done(); - }); - }); + microTask(() => { expect(execute2).toHaveBeenCalledWith(false); }); }); })); + }); describe('NgZone callback logic', () => { - it('should fire whenstable callback if event is already finished', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + describe('whenStable with timeout', () => { + 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.stable(); testability.whenStable(execute); - microTask(() => { - expect(execute).toHaveBeenCalled(); - async.done(); - }); + tick(); + expect(execute).toHaveBeenCalled(); })); it('should not fire whenstable callbacks synchronously if event is already finished', () => { @@ -168,20 +250,16 @@ class MockNgZone extends NgZone { expect(execute).not.toHaveBeenCalled(); }); - it('should fire whenstable callback when event finishes', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + it('should fire whenstable callback when event finishes', fakeAsync(() => { ngZone.unstable(); testability.whenStable(execute); - microTask(() => { - expect(execute).not.toHaveBeenCalled(); - ngZone.stable(); + tick(); + expect(execute).not.toHaveBeenCalled(); + ngZone.stable(); - microTask(() => { - expect(execute).toHaveBeenCalled(); - async.done(); - }); - }); + tick(); + expect(execute).toHaveBeenCalled(); })); it('should not fire whenstable callbacks synchronously when event finishes', () => { @@ -192,91 +270,72 @@ class MockNgZone extends NgZone { expect(execute).not.toHaveBeenCalled(); }); - it('should not fire whenstable callback when event did not finish', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + it('should not fire whenstable callback when event did not finish', fakeAsync(() => { ngZone.unstable(); testability.increasePendingRequestCount(); testability.whenStable(execute); - microTask(() => { - expect(execute).not.toHaveBeenCalled(); - testability.decreasePendingRequestCount(); + tick(); + expect(execute).not.toHaveBeenCalled(); + testability.decreasePendingRequestCount(); - microTask(() => { - expect(execute).not.toHaveBeenCalled(); - ngZone.stable(); + tick(); + expect(execute).not.toHaveBeenCalled(); + ngZone.stable(); - microTask(() => { - expect(execute).toHaveBeenCalled(); - async.done(); - }); - }); - }); + tick(); + expect(execute).toHaveBeenCalled(); })); - it('should not fire whenstable callback when there are pending counts', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + it('should not fire whenstable callback when there are pending counts', fakeAsync(() => { ngZone.unstable(); testability.increasePendingRequestCount(); testability.increasePendingRequestCount(); testability.whenStable(execute); - microTask(() => { - expect(execute).not.toHaveBeenCalled(); - ngZone.stable(); + tick(); + expect(execute).not.toHaveBeenCalled(); + ngZone.stable(); - microTask(() => { - expect(execute).not.toHaveBeenCalled(); - testability.decreasePendingRequestCount(); + tick(); + expect(execute).not.toHaveBeenCalled(); + testability.decreasePendingRequestCount(); - microTask(() => { - expect(execute).not.toHaveBeenCalled(); - testability.decreasePendingRequestCount(); + tick(); + expect(execute).not.toHaveBeenCalled(); + testability.decreasePendingRequestCount(); - microTask(() => { - expect(execute).toHaveBeenCalled(); - async.done(); - }); - }); - }); - }); + tick(); + expect(execute).toHaveBeenCalled(); })); it('should fire whenstable callback with didWork if event is already finished', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + fakeAsync(() => { ngZone.unstable(); ngZone.stable(); testability.whenStable(execute); - microTask(() => { - expect(execute).toHaveBeenCalledWith(true); - testability.whenStable(execute2); + tick(); + expect(execute).toHaveBeenCalledWith(true); + testability.whenStable(execute2); - microTask(() => { - expect(execute2).toHaveBeenCalledWith(false); - async.done(); - }); - }); + tick(); + expect(execute2).toHaveBeenCalledWith(false); })); - it('should fire whenstable callback with didwork when event finishes', - inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { + it('should fire whenstable callback with didwork when event finishes', fakeAsync(() => { ngZone.unstable(); testability.whenStable(execute); - microTask(() => { - ngZone.stable(); + tick(); + ngZone.stable(); - microTask(() => { - expect(execute).toHaveBeenCalledWith(true); - testability.whenStable(execute2); + tick(); + expect(execute).toHaveBeenCalledWith(true); + testability.whenStable(execute2); - microTask(() => { - expect(execute2).toHaveBeenCalledWith(false); - async.done(); - }); - }); - }); + tick(); + expect(execute2).toHaveBeenCalledWith(false); })); }); }); @@ -284,39 +343,39 @@ class MockNgZone extends NgZone { describe('TestabilityRegistry', () => { let testability1: Testability; let testability2: Testability; - let resgitry: TestabilityRegistry; + let registry: TestabilityRegistry; let ngZone: MockNgZone; - beforeEach(() => { + beforeEach(async(() => { ngZone = new MockNgZone(); testability1 = new Testability(ngZone); testability2 = new Testability(ngZone); - resgitry = new TestabilityRegistry(); - }); + registry = new TestabilityRegistry(); + })); describe('unregister testability', () => { it('should remove the testability when unregistering an existing testability', () => { - resgitry.registerApplication('testability1', testability1); - resgitry.registerApplication('testability2', testability2); - resgitry.unregisterApplication('testability2'); - expect(resgitry.getAllTestabilities().length).toEqual(1); - expect(resgitry.getTestability('testability1')).toEqual(testability1); + registry.registerApplication('testability1', testability1); + registry.registerApplication('testability2', testability2); + registry.unregisterApplication('testability2'); + expect(registry.getAllTestabilities().length).toEqual(1); + expect(registry.getTestability('testability1')).toEqual(testability1); }); it('should remain the same when unregistering a non-existing testability', () => { - expect(resgitry.getAllTestabilities().length).toEqual(0); - resgitry.registerApplication('testability1', testability1); - resgitry.registerApplication('testability2', testability2); - resgitry.unregisterApplication('testability3'); - expect(resgitry.getAllTestabilities().length).toEqual(2); - expect(resgitry.getTestability('testability1')).toEqual(testability1); - expect(resgitry.getTestability('testability2')).toEqual(testability2); + expect(registry.getAllTestabilities().length).toEqual(0); + registry.registerApplication('testability1', testability1); + registry.registerApplication('testability2', testability2); + registry.unregisterApplication('testability3'); + expect(registry.getAllTestabilities().length).toEqual(2); + expect(registry.getTestability('testability1')).toEqual(testability1); + expect(registry.getTestability('testability2')).toEqual(testability2); }); it('should remove all the testability when unregistering all testabilities', () => { - resgitry.registerApplication('testability1', testability1); - resgitry.registerApplication('testability2', testability2); - resgitry.unregisterAllApplications(); - expect(resgitry.getAllTestabilities().length).toEqual(0); + registry.registerApplication('testability1', testability1); + registry.registerApplication('testability2', testability2); + registry.unregisterAllApplications(); + expect(registry.getAllTestabilities().length).toEqual(0); }); }); }); diff --git a/packages/examples/_common/bootstrap.ts b/packages/examples/_common/bootstrap.ts index 71a2fc6d4b..24bad23979 100644 --- a/packages/examples/_common/bootstrap.ts +++ b/packages/examples/_common/bootstrap.ts @@ -7,6 +7,7 @@ */ (function(global: any) { writeScriptTag('/vendor/zone.js'); + writeScriptTag('/vendor/task-tracking.js'); writeScriptTag('/vendor/system.js'); writeScriptTag('/vendor/Reflect.js'); writeScriptTag('/_common/system-config.js'); diff --git a/packages/examples/build.sh b/packages/examples/build.sh index d0ef96a011..c9df72ea1a 100755 --- a/packages/examples/build.sh +++ b/packages/examples/build.sh @@ -22,6 +22,7 @@ ln -s ../../../dist/packages-dist/ $DIST/vendor/@angular for FILE in \ ../../../node_modules/angular/angular.js \ ../../../node_modules/zone.js/dist/zone.js \ + ../../../node_modules/zone.js/dist/task-tracking.js \ ../../../node_modules/systemjs/dist/system.js \ ../../../node_modules/reflect-metadata/Reflect.js \ ../../../node_modules/rxjs diff --git a/packages/examples/core/testability/ts/whenStable/e2e_test/testability_example_spec.ts b/packages/examples/core/testability/ts/whenStable/e2e_test/testability_example_spec.ts new file mode 100644 index 0000000000..94eaaca00f --- /dev/null +++ b/packages/examples/core/testability/ts/whenStable/e2e_test/testability_example_spec.ts @@ -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; }); + }); +}); diff --git a/packages/examples/core/testability/ts/whenStable/module.ts b/packages/examples/core/testability/ts/whenStable/module.ts new file mode 100644 index 0000000000..a8f49caec7 --- /dev/null +++ b/packages/examples/core/testability/ts/whenStable/module.ts @@ -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'; diff --git a/packages/examples/core/testability/ts/whenStable/testability_example.ts b/packages/examples/core/testability/ts/whenStable/testability_example.ts new file mode 100644 index 0000000000..d2df32fdca --- /dev/null +++ b/packages/examples/core/testability/ts/whenStable/testability_example.ts @@ -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: ` + +
Status: {{status}}
+ ` +}) +export class StableTestCmp { + status = 'none'; + start() { + this.status = 'running'; + setTimeout(() => { this.status = 'done'; }, 5000); + } +} + +@NgModule({imports: [BrowserModule], declarations: [StableTestCmp], bootstrap: [StableTestCmp]}) +export class AppModule { +} diff --git a/tools/cjs-jasmine/index-tools.ts b/tools/cjs-jasmine/index-tools.ts index 77c68d70b6..4efdab932b 100644 --- a/tools/cjs-jasmine/index-tools.ts +++ b/tools/cjs-jasmine/index-tools.ts @@ -15,6 +15,7 @@ require('zone.js/dist/zone-node.js'); var JasmineRunner = require('jasmine'); var path = require('path'); 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/sync-test.js'); require('zone.js/dist/async-test.js'); diff --git a/tools/cjs-jasmine/index.ts b/tools/cjs-jasmine/index.ts index 94662573c6..ec60745ae3 100644 --- a/tools/cjs-jasmine/index.ts +++ b/tools/cjs-jasmine/index.ts @@ -16,6 +16,7 @@ var JasmineRunner = require('jasmine'); var path = require('path'); require('source-map-support').install(); 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/sync-test.js'); require('zone.js/dist/async-test.js'); diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index e843b3c638..11ed54c2bb 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -998,12 +998,12 @@ export declare abstract class TemplateRef { /** @experimental */ export declare class Testability implements PublicTestability { constructor(_ngZone: NgZone); - decreasePendingRequestCount(): number; + /** @deprecated */ decreasePendingRequestCount(): number; findProviders(using: any, provider: string, exactMatch: boolean): any[]; - getPendingRequestCount(): number; - increasePendingRequestCount(): number; + /** @deprecated */ getPendingRequestCount(): number; + /** @deprecated */ increasePendingRequestCount(): number; isStable(): boolean; - whenStable(callback: Function): void; + whenStable(doneCb: Function, timeout?: number, updateCb?: Function): void; } /** @experimental */ diff --git a/tools/testing/init_node_spec.ts b/tools/testing/init_node_spec.ts index 8647db3b85..78b3600e6d 100644 --- a/tools/testing/init_node_spec.ts +++ b/tools/testing/init_node_spec.ts @@ -12,6 +12,7 @@ import 'zone.js/dist/proxy.js'; import 'zone.js/dist/sync-test.js'; import 'zone.js/dist/async-test.js'; import 'zone.js/dist/fake-async-test.js'; +import 'zone.js/dist/task-tracking.js'; import 'reflect-metadata/Reflect'; // This hack is needed to get jasmine, node and zone working inside bazel.