diff --git a/modules/angular2/pubspec.yaml b/modules/angular2/pubspec.yaml index 93c73388df..d0a89c8616 100644 --- a/modules/angular2/pubspec.yaml +++ b/modules/angular2/pubspec.yaml @@ -17,8 +17,9 @@ dependencies: logging: '>=0.9.0 <0.11.0' source_span: '^1.0.0' stack_trace: '^1.1.1' + quiver: '^0.21.3+1' dev_dependencies: - guinness: "^0.1.17" + guinness: '^0.1.17' transformers: - angular2 - $dart2js: diff --git a/modules/angular2/src/facade/async.dart b/modules/angular2/src/facade/async.dart index 60796ea4df..fadb9c724c 100644 --- a/modules/angular2/src/facade/async.dart +++ b/modules/angular2/src/facade/async.dart @@ -25,8 +25,19 @@ class PromiseWrapper { static _Completer completer() => new _Completer(new Completer()); - static void setTimeout(fn(), int millis) { - new Timer(new Duration(milliseconds: millis), fn); + // TODO(vic): create a TimerWrapper + static Timer setTimeout(fn(), int millis) + => new Timer(new Duration(milliseconds: millis), fn); + static void clearTimeout(Timer timer) { + timer.cancel(); + } + + static Timer setInterval(fn(), int millis) { + var interval = new Duration(milliseconds: millis); + return new Timer.periodic(interval, (Timer timer) { fn(); }); + } + static void clearInterval(Timer timer) { + timer.cancel(); } static bool isPromise(maybePromise) { diff --git a/modules/angular2/src/facade/async.ts b/modules/angular2/src/facade/async.ts index f7d60a8a95..6d58145322 100644 --- a/modules/angular2/src/facade/async.ts +++ b/modules/angular2/src/facade/async.ts @@ -45,7 +45,12 @@ export class PromiseWrapper { return {promise: p, resolve: resolve, reject: reject}; } - static setTimeout(fn: Function, millis: int) { global.setTimeout(fn, millis); } + // TODO(vicb): create a TimerWrapper + static setTimeout(fn: Function, millis: int): int { return global.setTimeout(fn, millis); } + static clearTimeout(id: int): void { global.clearTimeout(id); } + + static setInterval(fn: Function, millis: int): int { return global.setInterval(fn, millis); } + static clearInterval(id: int): void { global.clearInterval(id); } static isPromise(maybePromise): boolean { return maybePromise instanceof Promise; } } diff --git a/modules/angular2/src/test_lib/fake_async.dart b/modules/angular2/src/test_lib/fake_async.dart new file mode 100644 index 0000000000..b197e39ee3 --- /dev/null +++ b/modules/angular2/src/test_lib/fake_async.dart @@ -0,0 +1,74 @@ +library test_lib.fake_async; + +import 'dart:async' show runZoned, ZoneSpecification; +import 'package:quiver/testing/async.dart' as quiver; +import 'package:angular2/src/facade/lang.dart' show BaseException; + +const _u = const Object(); + +quiver.FakeAsync _fakeAsync = null; + +/** + * Wraps the [fn] to be executed in the fakeAsync zone: + * - microtasks are manually executed by calling [flushMicrotasks], + * - timers are synchronous, [tick] simulates the asynchronous passage of time. + * + * If there are any pending timers at the end of the function, an exception + * will be thrown. + * + * Returns a `Function` that wraps [fn]. + */ +Function fakeAsync(Function fn) { + if (_fakeAsync != null) { + throw 'fakeAsync() calls can not be nested'; + } + + return ([a0 = _u, a1 = _u, a2 = _u, a3 = _u, a4 = _u, a5 = _u, a6 = _u, + a7 = _u, a8 = _u, a9 = _u]) { + // runZoned() to install a custom exception handler that re-throws + return runZoned(() { + new quiver.FakeAsync().run((quiver.FakeAsync async) { + try { + _fakeAsync = async; + List args = [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9] + .takeWhile((a) => a != _u).toList(); + return Function.apply(fn , args); + } finally { + _fakeAsync = null; + } + }); + }, + zoneSpecification: new ZoneSpecification( + handleUncaughtError: (self, parent, zone, error, stackTrace) + => throw error + )); + }; +} + +/** + * Simulates the asynchronous passage of [millis] milliseconds for the timers + * in the fakeAsync zone. + * + * The microtasks queue is drained at the very start of this function and after + * any timer callback has been executed. + */ +void tick([int millis = 0]) { + _assertInFakeAsyncZone(); + var duration = new Duration(milliseconds: millis); + _fakeAsync.elapse(duration); +} + +/** + * Flush any pending microtasks. + */ +void flushMicrotasks() { + _assertInFakeAsyncZone(); + _fakeAsync.flushMicrotasks(); +} + +void _assertInFakeAsyncZone() { + if (_fakeAsync == null) { + throw new BaseException('The code should be running in the fakeAsync zone ' + 'to call this function'); + } +} diff --git a/modules/angular2/src/test_lib/fake_async.es6 b/modules/angular2/src/test_lib/fake_async.es6 new file mode 100644 index 0000000000..a559012d6e --- /dev/null +++ b/modules/angular2/src/test_lib/fake_async.es6 @@ -0,0 +1,131 @@ +import {BaseException, global} from 'angular2/src/facade/lang'; +import {ListWrapper} from 'angular2/src/facade/collection'; + +var _scheduler; +var _microtasks:List = []; +var _pendingPeriodicTimers: List = []; +var _pendingTimers: List = []; +var _error = null; + +/** + * Wraps a function to be executed in the fakeAsync zone: + * - microtasks are manually executed by calling `flushMicrotasks()`, + * - timers are synchronous, `tick()` simulates the asynchronous passage of time. + * + * If there are any pending timers at the end of the function, an exception will be thrown. + * + * @param fn + * @returns {Function} The function wrapped to be executed in the fakeAsync zone + */ +export function fakeAsync(fn: Function): Function { + // TODO(vicb) re-enable once the jasmine patch from zone.js is applied + //if (global.zone._inFakeAsyncZone) { + // throw new Error('fakeAsync() calls can not be nested'); + //} + + var fakeAsyncZone = global.zone.fork({ + setTimeout: _setTimeout, + clearTimeout: _clearTimeout, + setInterval: _setInterval, + clearInterval: _clearInterval, + scheduleMicrotask: _scheduleMicrotask, + _inFakeAsyncZone: true + }); + + return function(...args) { + _scheduler = global.jasmine.DelayedFunctionScheduler(); + ListWrapper.clear(_microtasks); + ListWrapper.clear(_pendingPeriodicTimers); + ListWrapper.clear(_pendingTimers); + + var res = fakeAsyncZone.run(() => { + var res = fn(...args); + }); + + if (_pendingPeriodicTimers.length > 0) { + throw new BaseException(`${_pendingPeriodicTimers.length} periodic timer(s) still in the queue.`); + } + + if (_pendingTimers.length > 0) { + throw new BaseException(`${_pendingTimers.length} timer(s) still in the queue.`); + } + + _scheduler = null; + ListWrapper.clear(_microtasks); + + return res; + } +} + +/** + * Simulates the asynchronous passage of time for the timers in the fakeAsync zone. + * + * The microtasks queue is drained at the very start of this function and after any timer callback has been executed. + * + * @param {number} millis Number of millisecond, defaults to 0 + */ +export function tick(millis: number = 0): void { + _assertInFakeAsyncZone(); + flushMicrotasks(); + _scheduler.tick(millis); +} + +/** + * Flush any pending microtasks. + */ +export function flushMicrotasks(): void { + _assertInFakeAsyncZone(); + while (_microtasks.length > 0) { + var microtask = ListWrapper.removeAt(_microtasks, 0); + microtask(); + } +} + +function _setTimeout(fn: Function, delay: number, ...args): number { + var cb = _fnAndFlush(fn); + var id = _scheduler.scheduleFunction(cb, delay, args); + ListWrapper.push(_pendingTimers, id); + _scheduler.scheduleFunction(_dequeueTimer(id), delay); + return id; +} + +function _clearTimeout(id: number) { + _dequeueTimer(id); + return _scheduler.removeFunctionWithId(id); +} + +function _setInterval(fn: Function, interval: number, ...args) { + var cb = _fnAndFlush(fn); + var id = _scheduler.scheduleFunction(cb, interval, args, true); + _pendingPeriodicTimers.push(id); + return id; +} + +function _clearInterval(id: number) { + ListWrapper.remove(_pendingPeriodicTimers, id); + return _scheduler.removeFunctionWithId(id); +} + +function _fnAndFlush(fn: Function): void { + return () => { + fn.apply(global, arguments); + flushMicrotasks(); + } +} + +function _scheduleMicrotask(microtask: Function): void { + ListWrapper.push(_microtasks, microtask); +} + +function _dequeueTimer(id: number): Function { + return function() { + ListWrapper.remove(_pendingTimers, id); + } +} + +function _assertInFakeAsyncZone(): void { + if (!global.zone._inFakeAsyncZone) { + throw new Error('The code should be running in the fakeAsync zone to call this function'); + } +} + diff --git a/modules/angular2/src/test_lib/lang_utils.dart b/modules/angular2/src/test_lib/lang_utils.dart index 9717dc3e62..81a830bf09 100644 --- a/modules/angular2/src/test_lib/lang_utils.dart +++ b/modules/angular2/src/test_lib/lang_utils.dart @@ -1,3 +1,5 @@ +library test_lib.lang_utils; + import 'dart:mirrors'; Type getTypeOf(instance) => instance.runtimeType; diff --git a/modules/angular2/test/test_lib/fake_async_spec.js b/modules/angular2/test/test_lib/fake_async_spec.js new file mode 100644 index 0000000000..b13e64c924 --- /dev/null +++ b/modules/angular2/test/test_lib/fake_async_spec.js @@ -0,0 +1,290 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + expect, + fakeAsync, + flushMicrotasks, + iit, + inject, + IS_DARTIUM, + it, + Log, + tick, + xit +} from 'angular2/test_lib'; +import {PromiseWrapper} from 'angular2/src/facade/async'; +import {BaseException, global} from 'angular2/src/facade/lang'; +import {Parser} from 'angular2/change_detection'; + +export function main() { + describe('fake async', () => { + it('should run synchronous code', () => { + var ran = false; + fakeAsync(() => { + ran = true; + })(); + + expect(ran).toEqual(true); + }); + + it('should pass arguments to the wrapped function', () => { + fakeAsync((foo, bar) => { + expect(foo).toEqual('foo'); + expect(bar).toEqual('bar'); + })('foo', 'bar'); + }); + + it('should work with inject()', inject([Parser], fakeAsync((parser) => { + expect(parser).toBeAnInstanceOf(Parser); + }))); + + if (!IS_DARTIUM) { + it('should throw on nested calls', () => { + // TODO(vicb): re-enable once the jasmine patch from zone.js is applied + if (!IS_DARTIUM) return; + expect(() => { + fakeAsync(() => { + fakeAsync(() => null)(); + })(); + }).toThrowError('fakeAsync() calls can not be nested'); + }); + } + + describe('Promise', () => { + it('should run asynchronous code', fakeAsync(() => { + var thenRan = false; + PromiseWrapper.resolve(null).then((_) => { + thenRan = true; + }); + + expect(thenRan).toEqual(false); + + flushMicrotasks(); + expect(thenRan).toEqual(true); + })); + + it('should run chained thens', fakeAsync(() => { + var log = new Log(); + + PromiseWrapper + .resolve(null) + .then((_) => log.add(1)) + .then((_) => log.add(2)); + + expect(log.result()).toEqual(''); + + flushMicrotasks(); + expect(log.result()).toEqual('1; 2'); + })); + + it('should run Promise created in Promise', fakeAsync(() => { + var log = new Log(); + + PromiseWrapper + .resolve(null) + .then((_) => { + log.add(1); + PromiseWrapper.resolve(null).then((_) => log.add(2)); + }); + + expect(log.result()).toEqual(''); + + flushMicrotasks(); + expect(log.result()).toEqual('1; 2'); + })); + + // TODO(vicb): check why this doesn't work in JS - linked to open issues on GH ? + xit('should complain if the test throws an exception during async calls', () => { + expect(() => { + fakeAsync(() => { + PromiseWrapper.resolve(null).then((_) => { + throw new BaseException('async'); + }); + flushMicrotasks(); + })(); + }).toThrowError('async'); + }); + + it('should complain if a test throws an exception', () => { + expect(() => { + fakeAsync(() => { + throw new BaseException('sync'); + })(); + }).toThrowError('sync'); + }); + + }); + + describe('timers', () => { + it('should run queued zero duration timer on zero tick', fakeAsync(() => { + var ran = false; + PromiseWrapper.setTimeout(() => { ran = true }, 0); + + expect(ran).toEqual(false); + + tick(); + expect(ran).toEqual(true); + })); + + + it('should run queued timer after sufficient clock ticks', fakeAsync(() => { + var ran = false; + PromiseWrapper.setTimeout(() => { ran = true; }, 10); + + tick(6); + expect(ran).toEqual(false); + + tick(6); + expect(ran).toEqual(true); + })); + + it('should run queued timer only once', fakeAsync(() => { + var cycles = 0; + PromiseWrapper.setTimeout(() => { cycles++; }, 10); + + tick(10); + expect(cycles).toEqual(1); + + tick(10); + expect(cycles).toEqual(1); + + tick(10); + expect(cycles).toEqual(1); + })); + + it('should not run cancelled timer', fakeAsync(() => { + var ran = false; + var id = PromiseWrapper.setTimeout(() => { ran = true; }, 10); + PromiseWrapper.clearTimeout(id); + + tick(10); + expect(ran).toEqual(false); + })); + + it('should throw an error on dangling timers', () => { + // TODO(vicb): https://github.com/google/quiver-dart/issues/248 + if (IS_DARTIUM) return; + expect(() => { + fakeAsync(() => { + PromiseWrapper.setTimeout(() => { }, 10); + })(); + }).toThrowError('1 timer(s) still in the queue.'); + }); + + it('should throw an error on dangling periodic timers', () => { + // TODO(vicb): https://github.com/google/quiver-dart/issues/248 + if (IS_DARTIUM) return; + expect(() => { + fakeAsync(() => { + PromiseWrapper.setInterval(() => { }, 10); + })(); + }).toThrowError('1 periodic timer(s) still in the queue.'); + }); + + it('should run periodic timers', fakeAsync(() => { + var cycles = 0; + var id = PromiseWrapper.setInterval(() => { cycles++; }, 10); + + tick(10); + expect(cycles).toEqual(1); + + tick(10); + expect(cycles).toEqual(2); + + tick(10); + expect(cycles).toEqual(3); + + PromiseWrapper.clearInterval(id); + })); + + it('should not run cancelled periodic timer', fakeAsync(() => { + var ran = false; + var id = PromiseWrapper.setInterval(() => { ran = true; }, 10); + PromiseWrapper.clearInterval(id); + + tick(10); + expect(ran).toEqual(false); + })); + + it('should be able to cancel periodic timers from a callback', fakeAsync(() => { + if (global != null && global.jasmine) { + // TODO(vicb): remove this when we switch to jasmine 2.3.3+ + // see https://github.com/jasmine/jasmine/commit/51462f369b376615bc9d761dcaa5d822ea1ff8ee + return; + } + + var cycles = 0; + var id; + + id = PromiseWrapper.setInterval(() => { + cycles++; + PromiseWrapper.clearInterval(id); + }, 10); + + tick(10); + expect(cycles).toEqual(1); + + tick(10); + expect(cycles).toEqual(1); + })); + + it('should process microtasks before timers', fakeAsync(() => { + var log = new Log(); + + PromiseWrapper.resolve(null).then((_) => log.add('microtask')); + + PromiseWrapper.setTimeout(() => log.add('timer'), 9); + + var id = PromiseWrapper.setInterval(() => log.add('periodic timer'), 10); + + expect(log.result()).toEqual(''); + + tick(10); + expect(log.result()).toEqual('microtask; timer; periodic timer'); + + PromiseWrapper.clearInterval(id); + })); + + it('should process micro-tasks created in timers before next timers', fakeAsync(() => { + var log = new Log(); + + PromiseWrapper.resolve(null).then((_) => log.add('microtask')); + + PromiseWrapper.setTimeout(() => { + log.add('timer'); + PromiseWrapper.resolve(null).then((_) => log.add('t microtask')); + }, 9); + + var id = PromiseWrapper.setInterval(() => { + log.add('periodic timer'); + PromiseWrapper.resolve(null).then((_) => log.add('pt microtask')); + }, 10); + + tick(10); + expect(log.result()).toEqual('microtask; timer; t microtask; periodic timer; pt microtask'); + + tick(10); + expect(log.result()).toEqual('microtask; timer; t microtask; periodic timer; pt microtask; periodic timer; pt microtask'); + + PromiseWrapper.clearInterval(id); + })); + }); + + describe('outside of the fakeAsync zone', () => { + it('calling flushMicrotasks should throw', () => { + expect(() => { + flushMicrotasks(); + }).toThrowError('The code should be running in the fakeAsync zone to call this function'); + }); + + it('calling tick should throw', () => { + expect(() => { + tick(); + }).toThrowError('The code should be running in the fakeAsync zone to call this function'); + }); + }); + + }); +} diff --git a/modules/angular2/test_lib.js b/modules/angular2/test_lib.js index fac6ae874f..d9ea79250a 100644 --- a/modules/angular2/test_lib.js +++ b/modules/angular2/test_lib.js @@ -1,2 +1,3 @@ export * from './src/test_lib/test_lib'; export * from './src/test_lib/utils'; +export * from './src/test_lib/fake_async'; diff --git a/pubspec.yaml b/pubspec.yaml index 8e297ea692..290f615990 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,3 +4,4 @@ environment: dev_dependencies: guinness: '^0.1.17' unittest: '^0.11.5+4' + quiver: '^0.21.3+1' diff --git a/tools/broccoli/trees/node_tree.ts b/tools/broccoli/trees/node_tree.ts index 1489a9d938..d00b839ec1 100644 --- a/tools/broccoli/trees/node_tree.ts +++ b/tools/broccoli/trees/node_tree.ts @@ -22,7 +22,8 @@ module.exports = function makeNodeTree(destinationPath) { exclude: [ // the following code and tests are not compatible with CJS/node environment 'angular2/src/core/zone/ng_zone.es6', - 'angular2/test/core/zone/**' + 'angular2/test/core/zone/**', + 'angular2/test/test_lib/fake_async_spec.js' ] });