From 491e1fdd2cf10d17f0fe56246c9a34385d15cbc2 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Mon, 19 Oct 2015 14:41:15 -0700 Subject: [PATCH] feat: move NgZone to Stream/Observable-based callback API BREAKING CHANGES: - deprecates these methods in NgZone: overrideOnTurnStart, overrideOnTurnDone, overrideOnEventDone, overrideOnErrorHandler - introduces new API in NgZone that may shadow other API used by existing applications. --- modules/angular2/src/core/facade/async.dart | 13 +- modules/angular2/src/core/facade/async.ts | 20 +- modules/angular2/src/core/zone.ts | 2 +- modules/angular2/src/core/zone/ng_zone.dart | 116 +++- modules/angular2/src/core/zone/ng_zone.ts | 127 +++- .../angular2/test/core/facade/async_spec.ts | 30 + .../test/core/zone/ng_zone_DEPRECATED_spec.ts | 655 ++++++++++++++++++ .../angular2/test/core/zone/ng_zone_spec.ts | 255 ++++--- modules/angular2/test/public_api_spec.ts | 12 + 9 files changed, 1111 insertions(+), 119 deletions(-) create mode 100644 modules/angular2/test/core/zone/ng_zone_DEPRECATED_spec.ts diff --git a/modules/angular2/src/core/facade/async.dart b/modules/angular2/src/core/facade/async.dart index 09ca62ffb0..bb7d7fdab4 100644 --- a/modules/angular2/src/core/facade/async.dart +++ b/modules/angular2/src/core/facade/async.dart @@ -35,6 +35,13 @@ class ObservableWrapper { return obs is Stream; } + /** + * Returns whether `emitter` has any subscribers listening to events. + */ + static bool hasSubscribers(EventEmitter emitter) { + return emitter._controller.hasListener; + } + static void dispose(StreamSubscription s) { s.cancel(); } @@ -55,8 +62,10 @@ class ObservableWrapper { class EventEmitter extends Stream { StreamController _controller; - EventEmitter() { - _controller = new StreamController.broadcast(); + /// Creates an instance of [EventEmitter], which depending on [isAsync], + /// delivers events synchronously or asynchronously. + EventEmitter([bool isAsync = true]) { + _controller = new StreamController.broadcast(sync: !isAsync); } StreamSubscription listen(void onData(dynamic line), diff --git a/modules/angular2/src/core/facade/async.ts b/modules/angular2/src/core/facade/async.ts index 98bc5627cb..6107ee520a 100644 --- a/modules/angular2/src/core/facade/async.ts +++ b/modules/angular2/src/core/facade/async.ts @@ -32,6 +32,11 @@ export class ObservableWrapper { static isObservable(obs: any): boolean { return obs instanceof Observable; } + /** + * Returns whether `obs` has any subscribers listening to events. + */ + static hasSubscribers(obs: EventEmitter): boolean { return obs._subject.observers.length > 0; } + static dispose(subscription: any) { subscription.unsubscribe(); } static callNext(emitter: EventEmitter, value: any) { emitter.next(value); } @@ -88,9 +93,22 @@ export class Observable { export class EventEmitter extends Observable { /** @internal */ _subject = new Subject(); + /** @internal */ + _isAsync: boolean; + + /** + * Creates an instance of [EventEmitter], which depending on [isAsync], + * delivers events synchronously or asynchronously. + */ + constructor(isAsync: boolean = true) { + super(); + this._isAsync = isAsync; + } observer(generator: any): any { - return this._subject.subscribe((value) => { setTimeout(() => generator.next(value)); }, + var schedulerFn = this._isAsync ? (value) => { setTimeout(() => generator.next(value)); } : + (value) => { generator.next(value); }; + return this._subject.subscribe(schedulerFn, (error) => generator.throw ? generator.throw(error) : null, () => generator.return ? generator.return () : null); } diff --git a/modules/angular2/src/core/zone.ts b/modules/angular2/src/core/zone.ts index ef99496e1d..6f3b3f93e3 100644 --- a/modules/angular2/src/core/zone.ts +++ b/modules/angular2/src/core/zone.ts @@ -1,2 +1,2 @@ // Public API for Zone -export {NgZone} from './zone/ng_zone'; +export {NgZone, ZeroArgFunction, ErrorHandlingFn, NgZoneError} from './zone/ng_zone'; diff --git a/modules/angular2/src/core/zone/ng_zone.dart b/modules/angular2/src/core/zone/ng_zone.dart index 90b851ae0d..de4ba2c034 100644 --- a/modules/angular2/src/core/zone/ng_zone.dart +++ b/modules/angular2/src/core/zone/ng_zone.dart @@ -35,6 +35,17 @@ class WrappedTimer implements Timer { bool get isActive => _timer.isActive; } +/** + * Stores error information; delivered via [NgZone.onError] stream. + */ +class NgZoneError { + /// Error object thrown. + final error; + /// Either long or short chain of stack traces. + final List stackTrace; + NgZoneError(this.error, this.stackTrace); +} + /** * A `Zone` wrapper that lets you schedule tasks after its private microtask queue is exhausted but * before the next "VM turn", i.e. event loop iteration. @@ -56,6 +67,12 @@ class NgZone { ZeroArgFunction _onEventDone; ErrorHandlingFn _onErrorHandler; + final _onTurnStartCtrl = new StreamController.broadcast(sync: true); + final _onTurnDoneCtrl = new StreamController.broadcast(sync: true); + final _onEventDoneCtrl = new StreamController.broadcast(sync: true); + final _onErrorCtrl = + new StreamController.broadcast(sync: true); + // Code executed in _mountZone does not trigger the onTurnDone. Zone _mountZone; // _innerZone is the child of _mountZone. Any code executed in this zone will trigger the @@ -103,18 +120,43 @@ class NgZone { * Sets the zone hook that is called just before Angular event turn starts. * It is called once per browser event. */ + @Deprecated('Use onTurnStart Stream instead') void overrideOnTurnStart(ZeroArgFunction onTurnStartFn) { _onTurnStart = onTurnStartFn; } + void _notifyOnTurnStart() { + this._onTurnStartCtrl.add(null); + } + + /** + * Notifies subscribers just before Angular event turn starts. + * + * Emits an event once per browser task that is handled by Angular. + */ + Stream get onTurnStart => _onTurnStartCtrl.stream; + /** * Sets the zone hook that is called immediately after Angular processes * all pending microtasks. */ + @Deprecated('Use onTurnDone Stream instead') void overrideOnTurnDone(ZeroArgFunction onTurnDoneFn) { _onTurnDone = onTurnDoneFn; } + /** + * Notifies subscribers immediately after the Angular zone is done processing + * the current turn and any microtasks scheduled from that turn. + * + * Used by Angular as a signal to kick off change-detection. + */ + Stream get onTurnDone => _onTurnDoneCtrl.stream; + + void _notifyOnTurnDone() { + this._onTurnDoneCtrl.add(null); + } + /** * Sets the zone hook that is called immediately after the last turn in * an event completes. At this point Angular will no longer attempt to @@ -123,6 +165,7 @@ class NgZone { * * This hook is useful for validating application state (e.g. in a test). */ + @Deprecated('Use onEventDone Stream instead') void overrideOnEventDone(ZeroArgFunction onEventDoneFn, [bool waitForAsync = false]) { _onEventDone = onEventDoneFn; @@ -136,15 +179,55 @@ class NgZone { } } + /** + * Notifies subscribers immediately after the final `onTurnDone` callback + * before ending VM event. + * + * This event is useful for validating application state (e.g. in a test). + */ + Stream get onEventDone => _onEventDoneCtrl.stream; + + void _notifyOnEventDone() { + this._onEventDoneCtrl.add(null); + } + + /** + * Whether there are any outstanding microtasks. + */ + bool get hasPendingMicrotasks => _pendingMicrotasks > 0; + + /** + * Whether there are any outstanding timers. + */ + bool get hasPendingTimers => _pendingTimers.isNotEmpty; + + /** + * Whether there are any outstanding asychnronous tasks of any kind that are + * scheduled to run within Angular zone. + * + * Useful as a signal of UI stability. For example, when a test reaches a + * point when [hasPendingAsyncTasks] is `false` it might be a good time to run + * test expectations. + */ + bool get hasPendingAsyncTasks => hasPendingMicrotasks || hasPendingTimers; + /** * Sets the zone hook that is called when an error is uncaught in the * Angular zone. The first argument is the error. The second argument is * the stack trace. */ + @Deprecated('Use onError Stream instead') void overrideOnErrorHandler(ErrorHandlingFn errorHandlingFn) { _onErrorHandler = errorHandlingFn; } + /** + * Notifies subscribers whenever an error happens within the zone. + * + * Useful for logging. + */ + Stream get onError => _onErrorCtrl.stream; + /** * Runs `fn` in the inner zone and returns whatever it returns. * @@ -194,6 +277,7 @@ class NgZone { void _maybeStartVmTurn(ZoneDelegate parent) { if (!_hasExecutedCodeInInnerZone) { _hasExecutedCodeInInnerZone = true; + parent.run(_innerZone, _notifyOnTurnStart); if (_onTurnStart != null) { parent.run(_innerZone, _onTurnStart); } @@ -209,19 +293,25 @@ class NgZone { _nestedRun--; // If there are no more pending microtasks and we are not in a recursive call, this is the end of a turn if (_pendingMicrotasks == 0 && _nestedRun == 0 && !_inVmTurnDone) { - if (_onTurnDone != null && _hasExecutedCodeInInnerZone) { + if (_hasExecutedCodeInInnerZone) { // Trigger onTurnDone at the end of a turn if _innerZone has executed some code try { _inVmTurnDone = true; - parent.run(_innerZone, _onTurnDone); + _notifyOnTurnDone(); + if (_onTurnDone != null) { + parent.run(_innerZone, _onTurnDone); + } } finally { _inVmTurnDone = false; _hasExecutedCodeInInnerZone = false; } } - if (_pendingMicrotasks == 0 && _onEventDone != null) { - runOutsideAngular(_onEventDone); + if (_pendingMicrotasks == 0) { + _notifyOnEventDone(); + if (_onEventDone != null) { + runOutsideAngular(_onEventDone); + } } } } @@ -248,9 +338,14 @@ class NgZone { // Called by Chain.capture() on errors when long stack traces are enabled void _onErrorWithLongStackTrace(error, Chain chain) { - if (_onErrorHandler != null) { + if (_onErrorHandler != null || _onErrorCtrl.hasListener) { final traces = chain.terse.traces.map((t) => t.toString()).toList(); - _onErrorHandler(error, traces); + if (_onErrorCtrl.hasListener) { + _onErrorCtrl.add(new NgZoneError(error, traces)); + } + if (_onErrorHandler != null) { + _onErrorHandler(error, traces); + } } else { throw error; } @@ -258,8 +353,13 @@ class NgZone { // Outer zone handleUnchaughtError when long stack traces are not used void _onErrorWithoutLongStackTrace(error, StackTrace trace) { - if (_onErrorHandler != null) { - _onErrorHandler(error, [trace.toString()]); + if (_onErrorHandler != null || _onErrorCtrl.hasListener) { + if (_onErrorHandler != null) { + _onErrorHandler(error, [trace.toString()]); + } + if (_onErrorCtrl.hasListener) { + _onErrorCtrl.add(new NgZoneError(error, [trace.toString()])); + } } else { throw error; } diff --git a/modules/angular2/src/core/zone/ng_zone.ts b/modules/angular2/src/core/zone/ng_zone.ts index 6eca86b22a..372d7d8a43 100644 --- a/modules/angular2/src/core/zone/ng_zone.ts +++ b/modules/angular2/src/core/zone/ng_zone.ts @@ -1,19 +1,22 @@ import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection'; import {normalizeBlank, isPresent, global} from 'angular2/src/core/facade/lang'; +import {ObservableWrapper, EventEmitter} from 'angular2/src/core/facade/async'; import {wtfLeave, wtfCreateScope, WtfScopeFn} from '../profile/profile'; - export interface NgZoneZone extends Zone { /** @internal */ _innerZone: boolean; } -export interface ZeroArgFunction { - (): void; -} +export interface ZeroArgFunction { (): void; } -export interface ErrorHandlingFn { - (error: any, stackTrace: any): void; +export interface ErrorHandlingFn { (error: any, stackTrace: any): void; } + +/** + * Stores error information; delivered via [NgZone.onError] stream. + */ +export class NgZoneError { + constructor(public error: any, public stackTrace: any) {} } /** @@ -109,6 +112,15 @@ export class NgZone { /** @internal */ _onErrorHandler: ErrorHandlingFn; + /** @internal */ + _onTurnStartEvents: EventEmitter; + /** @internal */ + _onTurnDoneEvents: EventEmitter; + /** @internal */ + _onEventDoneEvents: EventEmitter; + /** @internal */ + _onErrorEvents: EventEmitter; + // Number of microtasks pending from _innerZone (& descendants) /** @internal */ _pendingMicrotasks: number = 0; @@ -146,6 +158,10 @@ export class NgZone { this._disabled = true; this._mountZone = null; } + this._onTurnStartEvents = new EventEmitter(false); + this._onTurnDoneEvents = new EventEmitter(false); + this._onEventDoneEvents = new EventEmitter(false); + this._onErrorEvents = new EventEmitter(false); } /** @@ -155,11 +171,24 @@ export class NgZone { * The hook is called once per browser task that is handled by Angular. * * Setting the hook overrides any previously set hook. + * + * @deprecated this API will be removed in the future. Use `onTurnStart` instead. */ overrideOnTurnStart(onTurnStartHook: ZeroArgFunction): void { this._onTurnStart = normalizeBlank(onTurnStartHook); } + /** + * Notifies subscribers just before Angular event turn starts. + * + * Emits an event once per browser task that is handled by Angular. + */ + get onTurnStart(): /* Subject */ any { return this._onTurnStartEvents; } + + _notifyOnTurnStart(parentRun): void { + parentRun.call(this._innerZone, () => { this._onTurnStartEvents.next(null); }); + } + /** * Sets the zone hook that is called immediately after Angular zone is done processing the current * task and any microtasks scheduled from that task. @@ -169,11 +198,25 @@ export class NgZone { * The hook is called once per browser task that is handled by Angular. * * Setting the hook overrides any previously set hook. + * + * @deprecated this API will be removed in the future. Use `onTurnDone` instead. */ overrideOnTurnDone(onTurnDoneHook: ZeroArgFunction): void { this._onTurnDone = normalizeBlank(onTurnDoneHook); } + /** + * Notifies subscribers immediately after Angular zone is done processing + * the current turn and any microtasks scheduled from that turn. + * + * Used by Angular as a signal to kick off change-detection. + */ + get onTurnDone() { return this._onTurnDoneEvents; } + + _notifyOnTurnDone(parentRun): void { + parentRun.call(this._innerZone, () => { this._onTurnDoneEvents.next(null); }); + } + /** * Sets the zone hook that is called immediately after the `onTurnDone` callback is called and any * microstasks scheduled from within that callback are drained. @@ -184,6 +227,8 @@ export class NgZone { * This hook is useful for validating application state (e.g. in a test). * * Setting the hook overrides any previously set hook. + * + * @deprecated this API will be removed in the future. Use `onEventDone` instead. */ overrideOnEventDone(onEventDoneFn: ZeroArgFunction, opt_waitForAsync: boolean = false): void { var normalizedOnEventDone = normalizeBlank(onEventDoneFn); @@ -198,15 +243,51 @@ export class NgZone { } } + /** + * Notifies subscribers immediately after the final `onTurnDone` callback + * before ending VM event. + * + * This event is useful for validating application state (e.g. in a test). + */ + get onEventDone() { return this._onEventDoneEvents; } + + _notifyOnEventDone(): void { + this.runOutsideAngular(() => { this._onEventDoneEvents.next(null); }); + } + + /** + * Whether there are any outstanding microtasks. + */ + get hasPendingMicrotasks(): boolean { return this._pendingMicrotasks > 0; } + + /** + * Whether there are any outstanding timers. + */ + get hasPendingTimers(): boolean { return this._pendingTimeouts.length > 0; } + + /** + * Whether there are any outstanding asychnronous tasks of any kind that are + * scheduled to run within Angular zone. + * + * Useful as a signal of UI stability. For example, when a test reaches a + * point when [hasPendingAsyncTasks] is `false` it might be a good time to run + * test expectations. + */ + get hasPendingAsyncTasks(): boolean { return this.hasPendingMicrotasks || this.hasPendingTimers; } + /** * Sets the zone hook that is called when an error is thrown in the Angular zone. * * Setting the hook overrides any previously set hook. + * + * @deprecated this API will be removed in the future. Use `onError` instead. */ overrideOnErrorHandler(errorHandler: ErrorHandlingFn) { this._onErrorHandler = normalizeBlank(errorHandler); } + get onError() { return this._onErrorEvents; } + /** * Executes the `fn` function synchronously within the Angular zone and returns value returned by * the function. @@ -257,10 +338,10 @@ export class NgZone { var errorHandling; if (enableLongStackTrace) { - errorHandling = StringMapWrapper.merge(Zone.longStackTraceZone, - {onError: function(e) { ngZone._onError(this, e); }}); + errorHandling = StringMapWrapper.merge( + Zone.longStackTraceZone, {onError: function(e) { ngZone._notifyOnError(this, e); }}); } else { - errorHandling = {onError: function(e) { ngZone._onError(this, e); }}; + errorHandling = {onError: function(e) { ngZone._notifyOnError(this, e); }}; } return zone.fork(errorHandling) @@ -271,6 +352,7 @@ export class NgZone { ngZone._nestedRun++; if (!ngZone._hasExecutedCodeInInnerZone) { ngZone._hasExecutedCodeInInnerZone = true; + ngZone._notifyOnTurnStart(parentRun); if (ngZone._onTurnStart) { parentRun.call(ngZone._innerZone, ngZone._onTurnStart); } @@ -285,18 +367,24 @@ export class NgZone { // to run()). if (ngZone._pendingMicrotasks == 0 && ngZone._nestedRun == 0 && !this._inVmTurnDone) { - if (ngZone._onTurnDone && ngZone._hasExecutedCodeInInnerZone) { + if (ngZone._hasExecutedCodeInInnerZone) { try { this._inVmTurnDone = true; - parentRun.call(ngZone._innerZone, ngZone._onTurnDone); + ngZone._notifyOnTurnDone(parentRun); + if (ngZone._onTurnDone) { + parentRun.call(ngZone._innerZone, ngZone._onTurnDone); + } } finally { this._inVmTurnDone = false; ngZone._hasExecutedCodeInInnerZone = false; } } - if (ngZone._pendingMicrotasks === 0 && isPresent(ngZone._onEventDone)) { - ngZone.runOutsideAngular(ngZone._onEventDone); + if (ngZone._pendingMicrotasks === 0) { + ngZone._notifyOnEventDone(); + if (isPresent(ngZone._onEventDone)) { + ngZone.runOutsideAngular(ngZone._onEventDone); + } } } } @@ -340,17 +428,22 @@ export class NgZone { } /** @internal */ - _onError(zone, e): void { - if (isPresent(this._onErrorHandler)) { + _notifyOnError(zone, e): void { + if (isPresent(this._onErrorHandler) || ObservableWrapper.hasSubscribers(this._onErrorEvents)) { var trace = [normalizeBlank(e.stack)]; while (zone && zone.constructedAtException) { trace.push(zone.constructedAtException.get()); zone = zone.parent; } - this._onErrorHandler(e, trace); + if (ObservableWrapper.hasSubscribers(this._onErrorEvents)) { + ObservableWrapper.callNext(this._onErrorEvents, new NgZoneError(e, trace)); + } + if (isPresent(this._onErrorHandler)) { + this._onErrorHandler(e, trace); + } } else { - console.log('## _onError ##'); + console.log('## _notifyOnError ##'); console.log(e.stack); throw e; } diff --git a/modules/angular2/test/core/facade/async_spec.ts b/modules/angular2/test/core/facade/async_spec.ts index 03b46bdfce..0415c26cba 100644 --- a/modules/angular2/test/core/facade/async_spec.ts +++ b/modules/angular2/test/core/facade/async_spec.ts @@ -56,6 +56,36 @@ export function main() { expect(called).toBe(false); }); + it('delivers events asynchronously', inject([AsyncTestCompleter], (async) => { + var e = new EventEmitter(); + var log = []; + ObservableWrapper.subscribe(e, (x) => { + log.push(x); + expect(log).toEqual([1, 3, 2]); + async.done(); + }); + log.push(1); + ObservableWrapper.callNext(e, 2); + log.push(3); + })); + + it('delivers events synchronously', () => { + var e = new EventEmitter(false); + var log = []; + ObservableWrapper.subscribe(e, (x) => { log.push(x); }); + log.push(1); + ObservableWrapper.callNext(e, 2); + log.push(3); + expect(log).toEqual([1, 2, 3]); + }); + + it('reports whether it has subscribers', () => { + var e = new EventEmitter(false); + expect(ObservableWrapper.hasSubscribers(e)).toBe(false); + ObservableWrapper.subscribe(e, (_) => {}); + expect(ObservableWrapper.hasSubscribers(e)).toBe(true); + }); + // TODO: vsavkin: add tests cases // should call dispose on the subscription if generator returns {done:true} // should call dispose on the subscription on throw diff --git a/modules/angular2/test/core/zone/ng_zone_DEPRECATED_spec.ts b/modules/angular2/test/core/zone/ng_zone_DEPRECATED_spec.ts new file mode 100644 index 0000000000..e1fe860305 --- /dev/null +++ b/modules/angular2/test/core/zone/ng_zone_DEPRECATED_spec.ts @@ -0,0 +1,655 @@ +// TODO(yjbanov): this file tests the deprecated NgZone API. Delete it when +// the old API is cleaned up. +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + expect, + iit, + inject, + it, + xdescribe, + xit, + Log, + isInInnerZone, + browserDetection +} from 'angular2/test_lib'; + +import {PromiseCompleter, PromiseWrapper, TimerWrapper} from 'angular2/src/core/facade/async'; +import {BaseException} from 'angular2/src/core/facade/exceptions'; + +import {NgZone} from 'angular2/src/core/zone/ng_zone'; + +var needsLongerTimers = browserDetection.isSlow || browserDetection.isEdge; +var resultTimer = 1000; +var testTimeout = browserDetection.isEdge ? 1200 : 100; +// Schedules a macrotask (using a timer) +function macroTask(fn: (...args: any[]) => void, timer = 1): void { + // adds longer timers for passing tests in IE and Edge + _zone.runOutsideAngular(() => TimerWrapper.setTimeout(fn, needsLongerTimers ? timer : 1)); +} + +// Schedules a microtasks (using a resolved promise .then()) +function microTask(fn: Function): void { + PromiseWrapper.resolve(null).then((_) => { fn(); }); +} + +var _log; +var _errors: any[]; +var _traces: any[]; +var _zone; + +function logError(error, stackTrace) { + _errors.push(error); + _traces.push(stackTrace); +} + +export function main() { + describe("NgZone", () => { + + function createZone(enableLongStackTrace) { + var zone = new NgZone({enableLongStackTrace: enableLongStackTrace}); + zone.overrideOnTurnStart(_log.fn('onTurnStart')); + zone.overrideOnTurnDone(_log.fn('onTurnDone')); + return zone; + } + + beforeEach(() => { + _log = new Log(); + _errors = []; + _traces = []; + }); + + describe('long stack trace', () => { + beforeEach(() => { _zone = createZone(true); }); + + commonTests(); + + it('should produce long stack traces', inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.overrideOnErrorHandler(logError); + var c: PromiseCompleter = PromiseWrapper.completer(); + + _zone.run(() => { + TimerWrapper.setTimeout(() => { + TimerWrapper.setTimeout(() => { + c.resolve(null); + throw new BaseException('ccc'); + }, 0); + }, 0); + }); + + c.promise.then((_) => { + expect(_traces.length).toBe(1); + expect(_traces[0].length).toBeGreaterThan(1); + async.done(); + }); + }); + }), testTimeout); + + it('should produce long stack traces (when using microtasks)', + inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.overrideOnErrorHandler(logError); + var c: PromiseCompleter = PromiseWrapper.completer(); + + _zone.run(() => { + microTask(() => { + microTask(() => { + c.resolve(null); + throw new BaseException("ddd"); + }); + }); + }); + + c.promise.then((_) => { + expect(_traces.length).toBe(1); + expect(_traces[0].length).toBeGreaterThan(1); + async.done(); + }); + }); + }), testTimeout); + }); + + describe('short stack trace', () => { + beforeEach(() => { _zone = createZone(false); }); + + commonTests(); + + it('should disable long stack traces', inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.overrideOnErrorHandler(logError); + var c: PromiseCompleter = PromiseWrapper.completer(); + + _zone.run(() => { + TimerWrapper.setTimeout(() => { + TimerWrapper.setTimeout(() => { + c.resolve(null); + throw new BaseException('ccc'); + }, 0); + }, 0); + }); + + c.promise.then((_) => { + expect(_traces.length).toBe(1); + expect(_traces[0].length).toEqual(1); + async.done(); + }); + }); + }), testTimeout); + }); + }); +} + +function commonTests() { + describe('isInInnerZone', + () => {it('should return whether the code executes in the inner zone', () => { + expect(isInInnerZone()).toEqual(false); + _zone.run(() => { expect(isInInnerZone()).toEqual(true); }); + }, testTimeout)}); + + describe('run', () => { + it('should return the body return value from run', inject([AsyncTestCompleter], (async) => { + macroTask(() => { expect(_zone.run(() => { return 6; })).toEqual(6); }); + + macroTask(() => { async.done(); }); + }), testTimeout); + + it('should call onTurnStart and onTurnDone', inject([AsyncTestCompleter], (async) => { + macroTask(() => { _zone.run(_log.fn('run')); }); + + macroTask(() => { + expect(_log.result()).toEqual('onTurnStart; run; onTurnDone'); + async.done(); + }); + }), testTimeout); + + it('should call onEventDone once at the end of event', inject([AsyncTestCompleter], (async) => { + // The test is set up in a way that causes the zone loop to run onTurnDone twice + // then verified that onEventDone is only called once at the end + _zone.overrideOnTurnStart(null); + _zone.overrideOnEventDone(() => { _log.add('onEventDone'); }); + + var times = 0; + _zone.overrideOnTurnDone(() => { + times++; + _log.add(`onTurnDone ${times}`); + if (times < 2) { + // Scheduling a microtask causes a second digest + microTask(() => {}); + } + }); + + macroTask(() => { _zone.run(_log.fn('run')); }); + + macroTask(() => { + expect(_log.result()).toEqual('run; onTurnDone 1; onTurnDone 2; onEventDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should call standalone onEventDone', inject([AsyncTestCompleter], (async) => { + _zone.overrideOnTurnStart(null); + _zone.overrideOnEventDone(() => { _log.add('onEventDone'); }); + + _zone.overrideOnTurnDone(null); + + macroTask(() => { _zone.run(_log.fn('run')); }); + + macroTask(() => { + expect(_log.result()).toEqual('run; onEventDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should not allow onEventDone to cause further digests', + inject([AsyncTestCompleter], (async) => { + _zone.overrideOnTurnStart(null); + + var eventDone = false; + _zone.overrideOnEventDone(() => { + if (eventDone) throw 'Should not call this more than once'; + _log.add('onEventDone'); + // If not implemented correctly, this microtask will cause another digest, + // which is not what we want. + microTask(() => {}); + eventDone = true; + }); + + _zone.overrideOnTurnDone(() => { _log.add('onTurnDone'); }); + + macroTask(() => { _zone.run(_log.fn('run')); }); + + macroTask(() => { + expect(_log.result()).toEqual('run; onTurnDone; onEventDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should run async tasks scheduled inside onEventDone outside Angular zone', + inject([AsyncTestCompleter], (async) => { + _zone.overrideOnTurnStart(null); + + _zone.overrideOnEventDone(() => { + _log.add('onEventDone'); + // If not implemented correctly, this time will cause another digest, + // which is not what we want. + TimerWrapper.setTimeout(() => { _log.add('asyncTask'); }, 5); + }); + + _zone.overrideOnTurnDone(() => { _log.add('onTurnDone'); }); + + macroTask(() => { _zone.run(_log.fn('run')); }); + + macroTask(() => { + TimerWrapper.setTimeout(() => { + expect(_log.result()).toEqual('run; onTurnDone; onEventDone; asyncTask'); + async.done(); + }, 50); + }); + }), testTimeout); + + it('should call onTurnStart once before a turn and onTurnDone once after the turn', + inject([AsyncTestCompleter], (async) => { + + macroTask(() => { + _zone.run(() => { + _log.add('run start'); + microTask(_log.fn('async')); + _log.add('run end'); + }); + }); + + macroTask(() => { + // The microtask (async) is executed after the macrotask (run) + expect(_log.result()).toEqual('onTurnStart; run start; run end; async; onTurnDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should not run onTurnStart and onTurnDone for nested Zone.run', + inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.run(() => { + _log.add('start run'); + _zone.run(() => { + _log.add('nested run'); + microTask(_log.fn('nested run microtask')); + }); + _log.add('end run'); + }); + }); + + macroTask(() => { + expect(_log.result()) + .toEqual( + 'onTurnStart; start run; nested run; end run; nested run microtask; onTurnDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should not run onTurnStart and onTurnDone for nested Zone.run invoked from onTurnDone', + inject([AsyncTestCompleter], (async) => { + _zone.overrideOnTurnStart(null); + _zone.overrideOnTurnDone(() => { + _log.add('onTurnDone:started'); + _zone.run(() => _log.add('nested run')); + _log.add('onTurnDone:finished'); + }); + + macroTask(() => { _zone.run(() => { _log.add('start run'); }); }); + + macroTask(() => { + expect(_log.result()) + .toEqual('start run; onTurnDone:started; nested run; onTurnDone:finished'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should call onTurnStart and onTurnDone before and after each top-level run', + inject([AsyncTestCompleter], (async) => { + macroTask(() => { _zone.run(_log.fn('run1')); }); + + macroTask(() => { _zone.run(_log.fn('run2')); }); + + macroTask(() => { + expect(_log.result()) + .toEqual('onTurnStart; run1; onTurnDone; onTurnStart; run2; onTurnDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should call onTurnStart and onTurnDone before and after each turn', + inject([AsyncTestCompleter], (async) => { + var a: PromiseCompleter; + var b: PromiseCompleter; + + macroTask(() => { + _zone.run(() => { + a = PromiseWrapper.completer(); + b = PromiseWrapper.completer(); + + _log.add('run start'); + a.promise.then(_log.fn('a then')); + b.promise.then(_log.fn('b then')); + }); + }); + + macroTask(() => { + _zone.run(() => { + a.resolve('a'); + b.resolve('b'); + }); + }); + + macroTask(() => { + expect(_log.result()) + .toEqual( + 'onTurnStart; run start; onTurnDone; onTurnStart; a then; b then; onTurnDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should run a function outside of the angular zone', + inject([AsyncTestCompleter], (async) => { + macroTask(() => { _zone.runOutsideAngular(_log.fn('run')); }); + + macroTask(() => { + expect(_log.result()).toEqual('run'); + async.done() + }); + }), testTimeout); + + it('should call onTurnStart and onTurnDone when an inner microtask is scheduled from outside angular', + inject([AsyncTestCompleter], (async) => { + var completer: PromiseCompleter; + + macroTask( + () => { _zone.runOutsideAngular(() => { completer = PromiseWrapper.completer(); }); }); + + macroTask( + () => { _zone.run(() => { completer.promise.then(_log.fn('executedMicrotask')); }); }); + + macroTask(() => { + _zone.runOutsideAngular(() => { + _log.add('scheduling a microtask'); + completer.resolve(null); + }); + }); + + macroTask(() => { + expect(_log.result()) + .toEqual( + // First VM turn => setup Promise then + 'onTurnStart; onTurnDone; ' + + // Second VM turn (outside of anguler) + 'scheduling a microtask; ' + + // Third VM Turn => execute the microtask (inside angular) + 'onTurnStart; executedMicrotask; onTurnDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should call onTurnStart before executing a microtask scheduled in onTurnDone as well as ' + + 'onTurnDone after executing the task', + inject([AsyncTestCompleter], (async) => { + var ran = false; + _zone.overrideOnTurnStart(_log.fn('onTurnStart')); + _zone.overrideOnTurnDone(() => { + _log.add('onTurnDone(begin)'); + if (!ran) { + microTask(() => { + ran = true; + _log.add('executedMicrotask'); + }); + } + + _log.add('onTurnDone(end)'); + }); + + macroTask(() => { _zone.run(_log.fn('run')); }); + + macroTask(() => { + expect(_log.result()) + .toEqual( + // First VM turn => 'run' macrotask + 'onTurnStart; run; onTurnDone(begin); onTurnDone(end); ' + + // Second VM Turn => microtask enqueued from onTurnDone + 'onTurnStart; executedMicrotask; onTurnDone(begin); onTurnDone(end)'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should call onTurnStart and onTurnDone for a scheduleMicrotask in onTurnDone triggered by ' + + 'a scheduleMicrotask in run', + inject([AsyncTestCompleter], (async) => { + var ran = false; + _zone.overrideOnTurnStart(_log.fn('onTurnStart')); + _zone.overrideOnTurnDone(() => { + _log.add('onTurnDone(begin)'); + if (!ran) { + _log.add('onTurnDone(scheduleMicrotask)'); + microTask(() => { + ran = true; + _log.add('onTurnDone(executeMicrotask)'); + }); + } + _log.add('onTurnDone(end)'); + }); + + macroTask(() => { + _zone.run(() => { + _log.add('scheduleMicrotask'); + microTask(_log.fn('run(executeMicrotask)')); + }); + }); + + macroTask(() => { + expect(_log.result()) + .toEqual( + // First VM Turn => a macrotask + the microtask it enqueues + 'onTurnStart; scheduleMicrotask; run(executeMicrotask); onTurnDone(begin); onTurnDone(scheduleMicrotask); onTurnDone(end); ' + + // Second VM Turn => the microtask enqueued from onTurnDone + 'onTurnStart; onTurnDone(executeMicrotask); onTurnDone(begin); onTurnDone(end)'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should execute promises scheduled in onTurnStart before promises scheduled in run', + inject([AsyncTestCompleter], (async) => { + var donePromiseRan = false; + var startPromiseRan = false; + + _zone.overrideOnTurnStart(() => { + _log.add('onTurnStart(begin)'); + if (!startPromiseRan) { + _log.add('onTurnStart(schedulePromise)'); + microTask(_log.fn('onTurnStart(executePromise)')); + startPromiseRan = true; + } + _log.add('onTurnStart(end)'); + }); + _zone.overrideOnTurnDone(() => { + _log.add('onTurnDone(begin)'); + if (!donePromiseRan) { + _log.add('onTurnDone(schedulePromise)'); + microTask(_log.fn('onTurnDone(executePromise)')); + donePromiseRan = true; + } + _log.add('onTurnDone(end)'); + }); + + macroTask(() => { + _zone.run(() => { + _log.add('run start'); + PromiseWrapper.resolve(null) + .then((_) => { + _log.add('promise then'); + PromiseWrapper.resolve(null).then(_log.fn('promise foo')); + return PromiseWrapper.resolve(null); + }) + .then(_log.fn('promise bar')); + _log.add('run end'); + }); + }); + + macroTask(() => { + expect(_log.result()) + .toEqual( + // First VM turn: enqueue a microtask in onTurnStart + 'onTurnStart(begin); onTurnStart(schedulePromise); onTurnStart(end); ' + + // First VM turn: execute the macrotask which enqueues microtasks + 'run start; run end; ' + + // First VM turn: execute enqueued microtasks + 'onTurnStart(executePromise); promise then; promise foo; promise bar; ' + + // First VM turn: onTurnEnd, enqueue a microtask + 'onTurnDone(begin); onTurnDone(schedulePromise); onTurnDone(end); ' + + // Second VM turn: execute the microtask from onTurnEnd + 'onTurnStart(begin); onTurnStart(end); onTurnDone(executePromise); onTurnDone(begin); onTurnDone(end)'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should call onTurnStart and onTurnDone before and after each turn, respectively', + inject([AsyncTestCompleter], (async) => { + var completerA: PromiseCompleter; + var completerB: PromiseCompleter; + + macroTask(() => { + _zone.run(() => { + completerA = PromiseWrapper.completer(); + completerB = PromiseWrapper.completer(); + completerA.promise.then(_log.fn('a then')); + completerB.promise.then(_log.fn('b then')); + _log.add('run start'); + }); + }); + + macroTask(() => { _zone.run(() => { completerA.resolve(null); }); }, 20); + + + macroTask(() => { _zone.run(() => { completerB.resolve(null); }); }, 500); + + macroTask(() => { + expect(_log.result()) + .toEqual( + // First VM turn + 'onTurnStart; run start; onTurnDone; ' + + // Second VM turn + 'onTurnStart; a then; onTurnDone; ' + + // Third VM turn + 'onTurnStart; b then; onTurnDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should call onTurnStart and onTurnDone before and after (respectively) all turns in a chain', + inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.run(() => { + _log.add('run start'); + microTask(() => { + _log.add('async1'); + microTask(_log.fn('async2')); + }); + _log.add('run end'); + }); + }); + + macroTask(() => { + expect(_log.result()) + .toEqual('onTurnStart; run start; run end; async1; async2; onTurnDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should call onTurnStart and onTurnDone for promises created outside of run body', + inject([AsyncTestCompleter], (async) => { + var promise; + + macroTask(() => { + _zone.runOutsideAngular(() => { + promise = PromiseWrapper.resolve(4).then((x) => PromiseWrapper.resolve(x)); + }); + + _zone.run(() => { + promise.then(_log.fn('promise then')); + _log.add('zone run'); + }); + }); + + macroTask(() => { + expect(_log.result()) + .toEqual('onTurnStart; zone run; onTurnDone; onTurnStart; promise then; onTurnDone'); + async.done(); + }, resultTimer); + }), testTimeout); + }); + + describe('exceptions', () => { + it('should call the on error callback when it is defined', + inject([AsyncTestCompleter], (async) => { + macroTask(() => { + _zone.overrideOnErrorHandler(logError); + + var exception = new BaseException('sync'); + + _zone.run(() => { throw exception; }); + + expect(_errors.length).toBe(1); + expect(_errors[0]).toBe(exception); + async.done(); + }); + }), testTimeout); + + it('should call onError for errors from microtasks', inject([AsyncTestCompleter], (async) => { + _zone.overrideOnErrorHandler(logError); + + var exception = new BaseException('async'); + + macroTask(() => { _zone.run(() => { microTask(() => { throw exception; }); }); }); + + macroTask(() => { + expect(_errors.length).toBe(1); + expect(_errors[0]).toEqual(exception); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should call onError when onTurnDone throws and the zone is sync', + inject([AsyncTestCompleter], (async) => { + var exception = new BaseException('fromOnTurnDone'); + + _zone.overrideOnErrorHandler(logError); + _zone.overrideOnTurnDone(() => { throw exception; }); + + macroTask(() => { _zone.run(() => {}); }); + + macroTask(() => { + expect(_errors.length).toBe(1); + expect(_errors[0]).toEqual(exception); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should call onError when onTurnDone throws and the zone is async', + inject([AsyncTestCompleter], (async) => { + var asyncRan = false; + + var exception = new BaseException('fromOnTurnDone'); + + _zone.overrideOnErrorHandler(logError); + _zone.overrideOnTurnDone(() => { throw exception; }); + + macroTask(() => { _zone.run(() => { microTask(() => { asyncRan = true; }); }); }); + + macroTask(() => { + expect(asyncRan).toBe(true); + expect(_errors.length).toBe(1); + expect(_errors[0]).toEqual(exception); + async.done(); + }, resultTimer); + }), testTimeout); + }); +} diff --git a/modules/angular2/test/core/zone/ng_zone_spec.ts b/modules/angular2/test/core/zone/ng_zone_spec.ts index 83309072e4..ab93bdc255 100644 --- a/modules/angular2/test/core/zone/ng_zone_spec.ts +++ b/modules/angular2/test/core/zone/ng_zone_spec.ts @@ -14,10 +14,16 @@ import { browserDetection } from 'angular2/testing_internal'; -import {PromiseCompleter, PromiseWrapper, TimerWrapper} from 'angular2/src/core/facade/async'; +import { + PromiseCompleter, + PromiseWrapper, + TimerWrapper, + ObservableWrapper, + EventEmitter +} from 'angular2/src/core/facade/async'; import {BaseException} from 'angular2/src/core/facade/exceptions'; -import {NgZone} from 'angular2/src/core/zone/ng_zone'; +import {NgZone, NgZoneError} from 'angular2/src/core/zone/ng_zone'; var needsLongerTimers = browserDetection.isSlow || browserDetection.isEdge; var resultTimer = 1000; @@ -38,19 +44,30 @@ var _errors: any[]; var _traces: any[]; var _zone; -function logError(error, stackTrace) { - _errors.push(error); - _traces.push(stackTrace); +function logOnError() { + ObservableWrapper.subscribe(_zone.onError, (ngErr: NgZoneError) => { + _errors.push(ngErr.error); + _traces.push(ngErr.stackTrace); + }); +} + +function logOnTurnStart() { + ObservableWrapper.subscribe(_zone.onTurnStart, _log.fn('onTurnStart')); +} + +function logOnTurnDone() { + ObservableWrapper.subscribe(_zone.onTurnDone, _log.fn('onTurnDone')); +} + +function logOnEventDone() { + ObservableWrapper.subscribe(_zone.onEventDone, _log.fn('onEventDone')); } export function main() { describe("NgZone", () => { function createZone(enableLongStackTrace) { - var zone = new NgZone({enableLongStackTrace: enableLongStackTrace}); - zone.overrideOnTurnStart(_log.fn('onTurnStart')); - zone.overrideOnTurnDone(_log.fn('onTurnDone')); - return zone; + return new NgZone({enableLongStackTrace: enableLongStackTrace}); } beforeEach(() => { @@ -66,7 +83,7 @@ export function main() { it('should produce long stack traces', inject([AsyncTestCompleter], (async) => { macroTask(() => { - _zone.overrideOnErrorHandler(logError); + logOnError(); var c: PromiseCompleter = PromiseWrapper.completer(); _zone.run(() => { @@ -89,7 +106,7 @@ export function main() { it('should produce long stack traces (when using microtasks)', inject([AsyncTestCompleter], (async) => { macroTask(() => { - _zone.overrideOnErrorHandler(logError); + logOnError(); var c: PromiseCompleter = PromiseWrapper.completer(); _zone.run(() => { @@ -117,7 +134,7 @@ export function main() { it('should disable long stack traces', inject([AsyncTestCompleter], (async) => { macroTask(() => { - _zone.overrideOnErrorHandler(logError); + logOnError(); var c: PromiseCompleter = PromiseWrapper.completer(); _zone.run(() => { @@ -141,11 +158,44 @@ export function main() { } function commonTests() { - describe('isInInnerZone', - () => {it('should return whether the code executes in the inner zone', () => { - expect(isInInnerZone()).toEqual(false); - _zone.run(() => { expect(isInInnerZone()).toEqual(true); }); - }, testTimeout)}); + describe('hasPendingMicrotasks', () => { + it('should be false', () => { expect(_zone.hasPendingMicrotasks).toBe(false); }); + + it('should be true', () => { + _zone.run(() => { microTask(() => {}); }); + expect(_zone.hasPendingMicrotasks).toBe(true); + }); + }); + + describe('hasPendingTimers', () => { + it('should be false', () => { expect(_zone.hasPendingTimers).toBe(false); }); + + it('should be true', () => { + _zone.run(() => { TimerWrapper.setTimeout(() => {}, 0); }); + expect(_zone.hasPendingTimers).toBe(true); + }); + }); + + describe('hasPendingAsyncTasks', () => { + it('should be false', () => { expect(_zone.hasPendingAsyncTasks).toBe(false); }); + + it('should be true when microtask is scheduled', () => { + _zone.run(() => { microTask(() => {}); }); + expect(_zone.hasPendingAsyncTasks).toBe(true); + }); + + it('should be true when timer is scheduled', () => { + _zone.run(() => { TimerWrapper.setTimeout(() => {}, 0); }); + expect(_zone.hasPendingAsyncTasks).toBe(true); + }); + }); + + describe('isInInnerZone', () => { + it('should return whether the code executes in the inner zone', () => { + expect(isInInnerZone()).toEqual(false); + _zone.run(() => { expect(isInInnerZone()).toEqual(true); }); + }, testTimeout); + }); describe('run', () => { it('should return the body return value from run', inject([AsyncTestCompleter], (async) => { @@ -155,6 +205,8 @@ function commonTests() { }), testTimeout); it('should call onTurnStart and onTurnDone', inject([AsyncTestCompleter], (async) => { + logOnTurnStart(); + logOnTurnDone(); macroTask(() => { _zone.run(_log.fn('run')); }); macroTask(() => { @@ -166,16 +218,15 @@ function commonTests() { it('should call onEventDone once at the end of event', inject([AsyncTestCompleter], (async) => { // The test is set up in a way that causes the zone loop to run onTurnDone twice // then verified that onEventDone is only called once at the end - _zone.overrideOnTurnStart(null); - _zone.overrideOnEventDone(() => { _log.add('onEventDone'); }); + logOnEventDone(); var times = 0; - _zone.overrideOnTurnDone(() => { + ObservableWrapper.subscribe(_zone.onTurnDone, (_) => { times++; _log.add(`onTurnDone ${times}`); if (times < 2) { // Scheduling a microtask causes a second digest - microTask(() => {}); + _zone.run(() => { microTask(() => {}); }); } }); @@ -188,10 +239,7 @@ function commonTests() { }), testTimeout); it('should call standalone onEventDone', inject([AsyncTestCompleter], (async) => { - _zone.overrideOnTurnStart(null); - _zone.overrideOnEventDone(() => { _log.add('onEventDone'); }); - - _zone.overrideOnTurnDone(null); + logOnEventDone(); macroTask(() => { _zone.run(_log.fn('run')); }); @@ -201,42 +249,73 @@ function commonTests() { }, resultTimer); }), testTimeout); - it('should not allow onEventDone to cause further digests', + it('should run subscriber listeners in the subscription zone (outside)', inject([AsyncTestCompleter], (async) => { - _zone.overrideOnTurnStart(null); + // Each subscriber fires a microtask outside the Angular zone. The test + // then verifies that those microtasks do not cause additional digests. + + var turnStart = false; + ObservableWrapper.subscribe(_zone.onTurnStart, (_) => { + if (turnStart) throw 'Should not call this more than once'; + _log.add('onTurnStart'); + microTask(() => {}); + turnStart = true; + }); + + var turnDone = false; + ObservableWrapper.subscribe(_zone.onTurnDone, (_) => { + if (turnDone) throw 'Should not call this more than once'; + _log.add('onTurnDone'); + microTask(() => {}); + turnDone = true; + }); var eventDone = false; - _zone.overrideOnEventDone(() => { + ObservableWrapper.subscribe(_zone.onEventDone, (_) => { if (eventDone) throw 'Should not call this more than once'; _log.add('onEventDone'); - // If not implemented correctly, this microtask will cause another digest, - // which is not what we want. microTask(() => {}); eventDone = true; }); - _zone.overrideOnTurnDone(() => { _log.add('onTurnDone'); }); + macroTask(() => { _zone.run(_log.fn('run')); }); + + macroTask(() => { + expect(_log.result()).toEqual('onTurnStart; run; onTurnDone; onEventDone'); + async.done(); + }, resultTimer); + }), testTimeout); + + it('should run subscriber listeners in the subscription zone (inside)', + inject([AsyncTestCompleter], (async) => { + // the only practical use-case to run a callback inside the zone is + // change detection after "onTurnDone". That's the only case tested. + var turnDone = false; + ObservableWrapper.subscribe(_zone.onTurnDone, (_) => { + _log.add('onTurnDone'); + if (turnDone) return; + _zone.run(() => { microTask(() => {}); }); + turnDone = true; + }); macroTask(() => { _zone.run(_log.fn('run')); }); macroTask(() => { - expect(_log.result()).toEqual('run; onTurnDone; onEventDone'); + expect(_log.result()).toEqual('run; onTurnDone; onTurnDone'); async.done(); }, resultTimer); }), testTimeout); it('should run async tasks scheduled inside onEventDone outside Angular zone', inject([AsyncTestCompleter], (async) => { - _zone.overrideOnTurnStart(null); - - _zone.overrideOnEventDone(() => { + ObservableWrapper.subscribe(_zone.onEventDone, (_) => { _log.add('onEventDone'); // If not implemented correctly, this time will cause another digest, // which is not what we want. TimerWrapper.setTimeout(() => { _log.add('asyncTask'); }, 5); }); - _zone.overrideOnTurnDone(() => { _log.add('onTurnDone'); }); + logOnTurnDone(); macroTask(() => { _zone.run(_log.fn('run')); }); @@ -250,6 +329,8 @@ function commonTests() { it('should call onTurnStart once before a turn and onTurnDone once after the turn', inject([AsyncTestCompleter], (async) => { + logOnTurnStart(); + logOnTurnDone(); macroTask(() => { _zone.run(() => { @@ -268,6 +349,8 @@ function commonTests() { it('should not run onTurnStart and onTurnDone for nested Zone.run', inject([AsyncTestCompleter], (async) => { + logOnTurnStart(); + logOnTurnDone(); macroTask(() => { _zone.run(() => { _log.add('start run'); @@ -289,8 +372,7 @@ function commonTests() { it('should not run onTurnStart and onTurnDone for nested Zone.run invoked from onTurnDone', inject([AsyncTestCompleter], (async) => { - _zone.overrideOnTurnStart(null); - _zone.overrideOnTurnDone(() => { + ObservableWrapper.subscribe(_zone.onTurnDone, (_) => { _log.add('onTurnDone:started'); _zone.run(() => _log.add('nested run')); _log.add('onTurnDone:finished'); @@ -307,6 +389,9 @@ function commonTests() { it('should call onTurnStart and onTurnDone before and after each top-level run', inject([AsyncTestCompleter], (async) => { + logOnTurnStart(); + logOnTurnDone(); + macroTask(() => { _zone.run(_log.fn('run1')); }); macroTask(() => { _zone.run(_log.fn('run2')); }); @@ -320,6 +405,9 @@ function commonTests() { it('should call onTurnStart and onTurnDone before and after each turn', inject([AsyncTestCompleter], (async) => { + logOnTurnStart(); + logOnTurnDone(); + var a: PromiseCompleter; var b: PromiseCompleter; @@ -351,6 +439,9 @@ function commonTests() { it('should run a function outside of the angular zone', inject([AsyncTestCompleter], (async) => { + logOnTurnStart(); + logOnTurnDone(); + macroTask(() => { _zone.runOutsideAngular(_log.fn('run')); }); macroTask(() => { @@ -361,6 +452,9 @@ function commonTests() { it('should call onTurnStart and onTurnDone when an inner microtask is scheduled from outside angular', inject([AsyncTestCompleter], (async) => { + logOnTurnStart(); + logOnTurnDone(); + var completer: PromiseCompleter; macroTask( @@ -393,13 +487,17 @@ function commonTests() { 'onTurnDone after executing the task', inject([AsyncTestCompleter], (async) => { var ran = false; - _zone.overrideOnTurnStart(_log.fn('onTurnStart')); - _zone.overrideOnTurnDone(() => { + logOnTurnStart(); + + ObservableWrapper.subscribe(_zone.onTurnDone, (_) => { _log.add('onTurnDone(begin)'); + if (!ran) { - microTask(() => { - ran = true; - _log.add('executedMicrotask'); + _zone.run(() => { + microTask(() => { + ran = true; + _log.add('executedMicrotask'); + }); }); } @@ -423,14 +521,17 @@ function commonTests() { 'a scheduleMicrotask in run', inject([AsyncTestCompleter], (async) => { var ran = false; - _zone.overrideOnTurnStart(_log.fn('onTurnStart')); - _zone.overrideOnTurnDone(() => { + logOnTurnStart(); + + ObservableWrapper.subscribe(_zone.onTurnDone, (_) => { _log.add('onTurnDone(begin)'); if (!ran) { _log.add('onTurnDone(scheduleMicrotask)'); - microTask(() => { - ran = true; - _log.add('onTurnDone(executeMicrotask)'); + _zone.run(() => { + microTask(() => { + ran = true; + _log.add('onTurnDone(executeMicrotask)'); + }); }); } _log.add('onTurnDone(end)'); @@ -459,20 +560,21 @@ function commonTests() { var donePromiseRan = false; var startPromiseRan = false; - _zone.overrideOnTurnStart(() => { + ObservableWrapper.subscribe(_zone.onTurnStart, (_) => { _log.add('onTurnStart(begin)'); if (!startPromiseRan) { _log.add('onTurnStart(schedulePromise)'); - microTask(_log.fn('onTurnStart(executePromise)')); + _zone.run(() => { microTask(_log.fn('onTurnStart(executePromise)')); }); startPromiseRan = true; } _log.add('onTurnStart(end)'); }); - _zone.overrideOnTurnDone(() => { + + ObservableWrapper.subscribe(_zone.onTurnDone, (_) => { _log.add('onTurnDone(begin)'); if (!donePromiseRan) { _log.add('onTurnDone(schedulePromise)'); - microTask(_log.fn('onTurnDone(executePromise)')); + _zone.run(() => { microTask(_log.fn('onTurnDone(executePromise)')); }); donePromiseRan = true; } _log.add('onTurnDone(end)'); @@ -514,6 +616,9 @@ function commonTests() { var completerA: PromiseCompleter; var completerB: PromiseCompleter; + logOnTurnStart(); + logOnTurnDone(); + macroTask(() => { _zone.run(() => { completerA = PromiseWrapper.completer(); @@ -526,7 +631,6 @@ function commonTests() { macroTask(() => { _zone.run(() => { completerA.resolve(null); }); }, 20); - macroTask(() => { _zone.run(() => { completerB.resolve(null); }); }, 500); macroTask(() => { @@ -544,6 +648,9 @@ function commonTests() { it('should call onTurnStart and onTurnDone before and after (respectively) all turns in a chain', inject([AsyncTestCompleter], (async) => { + logOnTurnStart(); + logOnTurnDone(); + macroTask(() => { _zone.run(() => { _log.add('run start'); @@ -564,6 +671,9 @@ function commonTests() { it('should call onTurnStart and onTurnDone for promises created outside of run body', inject([AsyncTestCompleter], (async) => { + logOnTurnStart(); + logOnTurnDone(); + var promise; macroTask(() => { @@ -589,7 +699,7 @@ function commonTests() { it('should call the on error callback when it is defined', inject([AsyncTestCompleter], (async) => { macroTask(() => { - _zone.overrideOnErrorHandler(logError); + logOnError(); var exception = new BaseException('sync'); @@ -602,7 +712,7 @@ function commonTests() { }), testTimeout); it('should call onError for errors from microtasks', inject([AsyncTestCompleter], (async) => { - _zone.overrideOnErrorHandler(logError); + logOnError(); var exception = new BaseException('async'); @@ -614,40 +724,5 @@ function commonTests() { async.done(); }, resultTimer); }), testTimeout); - - it('should call onError when onTurnDone throws and the zone is sync', - inject([AsyncTestCompleter], (async) => { - var exception = new BaseException('fromOnTurnDone'); - - _zone.overrideOnErrorHandler(logError); - _zone.overrideOnTurnDone(() => { throw exception; }); - - macroTask(() => { _zone.run(() => {}); }); - - macroTask(() => { - expect(_errors.length).toBe(1); - expect(_errors[0]).toEqual(exception); - async.done(); - }, resultTimer); - }), testTimeout); - - it('should call onError when onTurnDone throws and the zone is async', - inject([AsyncTestCompleter], (async) => { - var asyncRan = false; - - var exception = new BaseException('fromOnTurnDone'); - - _zone.overrideOnErrorHandler(logError); - _zone.overrideOnTurnDone(() => { throw exception; }); - - macroTask(() => { _zone.run(() => { microTask(() => { asyncRan = true; }); }); }); - - macroTask(() => { - expect(asyncRan).toBe(true); - expect(_errors.length).toBe(1); - expect(_errors[0]).toEqual(exception); - async.done(); - }, resultTimer); - }), testTimeout); }); } diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 1f2051b59c..dd39ee7a1d 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -467,6 +467,7 @@ var NG_API = [ 'ElementRef.parentView', 'ElementRef.parentView=', 'ElementRef.renderView', + 'ErrorHandlingFn:dart', 'Output', 'Output.bindingPropertyName', 'EventEmitter', @@ -795,12 +796,22 @@ var NG_API = [ 'NgSwitchWhen', 'NgSwitchWhen.ngSwitchWhen=', 'NgZone', + 'NgZone.hasPendingAsyncTasks', + 'NgZone.hasPendingMicrotasks', + 'NgZone.hasPendingTimers', + 'NgZone.onError', + 'NgZone.onEventDone', + 'NgZone.onTurnDone', + 'NgZone.onTurnStart', 'NgZone.overrideOnErrorHandler()', 'NgZone.overrideOnEventDone()', 'NgZone.overrideOnTurnDone()', 'NgZone.overrideOnTurnStart()', 'NgZone.run()', 'NgZone.runOutsideAngular()', + 'NgZoneError', + 'NgZoneError.error', + 'NgZoneError.stackTrace', 'NoAnnotationError', 'NoAnnotationError.message', 'NoAnnotationError.stackTrace', @@ -1107,6 +1118,7 @@ var NG_API = [ 'WrappedValue.wrapped', 'WrappedValue.wrapped=', 'WtfScopeFn:dart', + 'ZeroArgFunction:dart', 'applicationCommonBindings()', 'asNativeElements()', 'bind()',