feat(fakeAsync): allow simulating the passage of time

This commit is contained in:
Victor Berchet 2015-05-12 16:28:57 +02:00
parent b066b8d15a
commit 0f002a5b18
10 changed files with 522 additions and 5 deletions

View File

@ -17,8 +17,9 @@ dependencies:
logging: '>=0.9.0 <0.11.0' logging: '>=0.9.0 <0.11.0'
source_span: '^1.0.0' source_span: '^1.0.0'
stack_trace: '^1.1.1' stack_trace: '^1.1.1'
quiver: '^0.21.3+1'
dev_dependencies: dev_dependencies:
guinness: "^0.1.17" guinness: '^0.1.17'
transformers: transformers:
- angular2 - angular2
- $dart2js: - $dart2js:

View File

@ -25,8 +25,19 @@ class PromiseWrapper {
static _Completer completer() => new _Completer(new Completer()); static _Completer completer() => new _Completer(new Completer());
static void setTimeout(fn(), int millis) { // TODO(vic): create a TimerWrapper
new Timer(new Duration(milliseconds: millis), fn); 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) { static bool isPromise(maybePromise) {

View File

@ -45,7 +45,12 @@ export class PromiseWrapper {
return {promise: p, resolve: resolve, reject: reject}; 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; } static isPromise(maybePromise): boolean { return maybePromise instanceof Promise; }
} }

View File

@ -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');
}
}

View File

@ -0,0 +1,131 @@
import {BaseException, global} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
var _scheduler;
var _microtasks:List<Function> = [];
var _pendingPeriodicTimers: List<number> = [];
var _pendingTimers: List<number> = [];
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');
}
}

View File

@ -1,3 +1,5 @@
library test_lib.lang_utils;
import 'dart:mirrors'; import 'dart:mirrors';
Type getTypeOf(instance) => instance.runtimeType; Type getTypeOf(instance) => instance.runtimeType;

View File

@ -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');
});
});
});
}

View File

@ -1,2 +1,3 @@
export * from './src/test_lib/test_lib'; export * from './src/test_lib/test_lib';
export * from './src/test_lib/utils'; export * from './src/test_lib/utils';
export * from './src/test_lib/fake_async';

View File

@ -4,3 +4,4 @@ environment:
dev_dependencies: dev_dependencies:
guinness: '^0.1.17' guinness: '^0.1.17'
unittest: '^0.11.5+4' unittest: '^0.11.5+4'
quiver: '^0.21.3+1'

View File

@ -22,7 +22,8 @@ module.exports = function makeNodeTree(destinationPath) {
exclude: [ exclude: [
// the following code and tests are not compatible with CJS/node environment // the following code and tests are not compatible with CJS/node environment
'angular2/src/core/zone/ng_zone.es6', 'angular2/src/core/zone/ng_zone.es6',
'angular2/test/core/zone/**' 'angular2/test/core/zone/**',
'angular2/test/test_lib/fake_async_spec.js'
] ]
}); });