Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Commit

Permalink
feat(jasmine): support Date.now in fakeAsyncTest (#1009)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiaLiPassion authored and mhevery committed Feb 10, 2018
1 parent 6a1a830 commit f22065e
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 5 deletions.
10 changes: 10 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,14 @@ gulp.task('build/rxjs.min.js', ['compile-esm'], function(cb) {
return generateScript('./lib/rxjs/rxjs.ts', 'zone-patch-rxjs.min.js', true, cb);
});

gulp.task('build/rxjs-fake-async.js', ['compile-esm'], function(cb) {
return generateScript('./lib/rxjs/rxjs-fake-async.ts', 'zone-patch-rxjs-fake-async.js', false, cb);
});

gulp.task('build/rxjs-fake-async.min.js', ['compile-esm'], function(cb) {
return generateScript('./lib/rxjs/rxjs-fake-async.ts', 'zone-patch-rxjs-fake-async.min.js', true, cb);
});

gulp.task('build/closure.js', function() {
return gulp.src('./lib/closure/zone_externs.js')
.pipe(gulp.dest('./dist'));
Expand Down Expand Up @@ -337,6 +345,8 @@ gulp.task('build', [
'build/sync-test.js',
'build/rxjs.js',
'build/rxjs.min.js',
'build/rxjs-fake-async.js',
'build/rxjs-fake-async.min.js',
'build/closure.js'
]);

Expand Down
72 changes: 67 additions & 5 deletions lib/jasmine/jasmine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
// a `beforeEach` or `it`.
const syncZone = ambientZone.fork(new SyncTestZoneSpec('jasmine.describe'));

const symbol = Zone.__symbol__;

// This is the zone which will be used for running individual tests.
// It will be a proxy zone, so that the tests function can retroactively install
// different zones.
Expand All @@ -45,6 +47,7 @@
// - Because ProxyZone is parent fo `childZone` fakeAsync can retroactively add
// fakeAsync behavior to the childZone.
let testProxyZone: Zone = null;
let testProxyZoneSpec: ZoneSpec = null;

// Monkey patch all of the jasmine DSL so that each function runs in appropriate zone.
const jasmineEnv: any = jasmine.getEnv();
Expand All @@ -56,7 +59,7 @@
});
['it', 'xit', 'fit'].forEach((methodName) => {
let originalJasmineFn: Function = jasmineEnv[methodName];
jasmineEnv[Zone.__symbol__(methodName)] = originalJasmineFn;
jasmineEnv[symbol(methodName)] = originalJasmineFn;
jasmineEnv[methodName] = function(
description: string, specDefinitions: Function, timeout: number) {
arguments[1] = wrapTestInZone(specDefinitions);
Expand All @@ -65,12 +68,45 @@
});
['beforeEach', 'afterEach'].forEach((methodName) => {
let originalJasmineFn: Function = jasmineEnv[methodName];
jasmineEnv[Zone.__symbol__(methodName)] = originalJasmineFn;
jasmineEnv[symbol(methodName)] = originalJasmineFn;
jasmineEnv[methodName] = function(specDefinitions: Function, timeout: number) {
arguments[0] = wrapTestInZone(specDefinitions);
return originalJasmineFn.apply(this, arguments);
};
});
const originalClockFn: Function = (jasmine as any)[symbol('clock')] = jasmine['clock'];
(jasmine as any)['clock'] = function() {
const clock = originalClockFn.apply(this, arguments);
const originalTick = clock[symbol('tick')] = clock.tick;
clock.tick = function() {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec) {
return fakeAsyncZoneSpec.tick.apply(fakeAsyncZoneSpec, arguments);
}
return originalTick.apply(this, arguments);
};
const originalMockDate = clock[symbol('mockDate')] = clock.mockDate;
clock.mockDate = function() {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec) {
const dateTime = arguments[0];
return fakeAsyncZoneSpec.setCurrentRealTime.apply(fakeAsyncZoneSpec, dateTime && typeof dateTime.getTime === 'function' ? [dateTime.getTime()] : arguments);
}
return originalMockDate.apply(this, arguments);
};
['install', 'uninstall'].forEach(methodName => {
const originalClockFn: Function = clock[symbol(methodName)] = clock[methodName];
clock[methodName] = function() {
const FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
if (FakeAsyncTestZoneSpec) {
(jasmine as any)[symbol('clockInstalled')] = 'install' === methodName;
return;
}
return originalClockFn.apply(this, arguments);
};
});
return clock;
};

/**
* Gets a function wrapping the body of a Jasmine `describe` block to execute in a
Expand All @@ -82,6 +118,30 @@
};
}

function runInTestZone(testBody: Function, done?: Function) {
const isClockInstalled = !!(jasmine as any)[symbol('clockInstalled')];
let lastDelegate;
if (isClockInstalled) {
const FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
if (FakeAsyncTestZoneSpec) {
const _fakeAsyncTestZoneSpec = new FakeAsyncTestZoneSpec();
lastDelegate = (testProxyZoneSpec as any).getDelegate();
(testProxyZoneSpec as any).setDelegate(_fakeAsyncTestZoneSpec);
}
}
try {
if (done) {
return testProxyZone.run(testBody, this, [done]);
} else {
return testProxyZone.run(testBody, this);
}
} finally {
if (isClockInstalled) {
(testProxyZoneSpec as any).setDelegate(lastDelegate);
}
}
}

/**
* Gets a function wrapping the body of a Jasmine `it/beforeEach/afterEach` block to
* execute in a ProxyZone zone.
Expand All @@ -92,9 +152,9 @@
// Note we have to make a function with correct number of arguments, otherwise jasmine will
// think that all functions are sync or async.
return testBody && (testBody.length ? function(done: Function) {
return testProxyZone.run(testBody, this, [done]);
runInTestZone(testBody, done);
} : function() {
return testProxyZone.run(testBody, this);
runInTestZone(testBody);
});
}
interface QueueRunner {
Expand All @@ -118,13 +178,15 @@
attrs.onComplete = ((fn) => () => {
// All functions are done, clear the test zone.
testProxyZone = null;
testProxyZoneSpec = null;
ambientZone.scheduleMicroTask('jasmine.onComplete', fn);
})(attrs.onComplete);
_super.call(this, attrs);
}
ZoneQueueRunner.prototype.execute = function() {
if (Zone.current !== ambientZone) throw new Error('Unexpected Zone: ' + Zone.current.name);
testProxyZone = ambientZone.fork(new ProxyZoneSpec());
testProxyZoneSpec = new ProxyZoneSpec();
testProxyZone = ambientZone.fork(testProxyZoneSpec);
if (!Zone.currentTask) {
// if we are not running in a task then if someone would register a
// element.addEventListener and then calling element.click() the
Expand Down
23 changes: 23 additions & 0 deletions lib/rxjs/rxjs-fake-async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @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 {Scheduler} from 'rxjs/Scheduler';
import {async} from 'rxjs/scheduler/async';
import {asap} from 'rxjs/scheduler/asap';

Zone.__load_patch('rxjs.Scheduler.now', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
api.patchMethod(Scheduler, 'now', (delegate: Function) => (self: any, args: any[]) => {
return Date.now.apply(self, args);
});
api.patchMethod(async, 'now', (delegate: Function) => (self: any, args: any[]) => {
return Date.now.apply(self, args);
});
api.patchMethod(asap, 'now', (delegate: Function) => (self: any, args: any[]) => {
return Date.now.apply(self, args);
});
});
74 changes: 74 additions & 0 deletions lib/zone-spec/fake-async-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@
callbackArgs?: any;
}

const OriginalDate = global.Date;
class FakeDate {
constructor() {
const d = new OriginalDate();
d.setTime(global.Date.now());
return d;
}

static UTC() {
return OriginalDate.UTC();
}

static now() {
const fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncTestZoneSpec) {
return fakeAsyncTestZoneSpec.getCurrentRealTime() + fakeAsyncTestZoneSpec.getCurrentTime();
}
return OriginalDate.now.apply(this, arguments);
}

static parse() {
return OriginalDate.parse();
}
}

class Scheduler {
// Next scheduler id.
public nextId: number = 0;
Expand All @@ -37,9 +62,23 @@
private _schedulerQueue: ScheduledFunction[] = [];
// Current simulated time in millis.
private _currentTime: number = 0;
// Current real time in millis.
private _currentRealTime: number = Date.now();

constructor() {}

getCurrentTime() {
return this._currentTime;
}

getCurrentRealTime() {
return this._currentRealTime;
}

setCurrentRealTime(realTime: number) {
this._currentRealTime = realTime;
}

scheduleFunction(
cb: Function, delay: number, args: any[] = [], isPeriodic: boolean = false,
isRequestAnimationFrame: boolean = false, id: number = -1): number {
Expand Down Expand Up @@ -281,6 +320,32 @@
throw error;
}

getCurrentTime() {
return this._scheduler.getCurrentTime();
}

getCurrentRealTime() {
return this._scheduler.getCurrentRealTime();
}

setCurrentRealTime(realTime: number) {
this._scheduler.setCurrentRealTime(realTime);
}

static patchDate() {
if (global['Date'] === FakeDate) {
// already patched
return;
}
global['Date'] = FakeDate;
}

static resetDate() {
if (global['Date'] === FakeDate) {
global['Date'] = OriginalDate;
}
}

tick(millis: number = 0, doTick?: (elapsed: number) => void): void {
FakeAsyncTestZoneSpec.assertInZone();
this.flushMicrotasks();
Expand Down Expand Up @@ -415,6 +480,15 @@
}
}

onInvoke(delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any, applyArgs: any[], source: string): any {
try {
FakeAsyncTestZoneSpec.patchDate();
return delegate.invoke(target, callback, applyThis, applyArgs, source);
} finally {
FakeAsyncTestZoneSpec.resetDate();
}
}

findMacroTaskOption(task: Task) {
if (!this.macroTaskOptions) {
return null;
Expand Down
94 changes: 94 additions & 0 deletions test/zone-spec/fake-async-test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import '../../lib/zone-spec/fake-async-test';

import {isNode, patchMacroTask} from '../../lib/common/utils';
import {ifEnvSupports} from '../test-util';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/delay';
import '../../lib/rxjs/rxjs-fake-async';

function supportNode() {
return isNode;
Expand Down Expand Up @@ -805,4 +808,95 @@ describe('FakeAsyncTestZoneSpec', () => {
});
});
});

describe('fakeAsyncTest should patch Date', () => {
let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
let testZoneSpec: any;
let fakeAsyncTestZone: Zone;

beforeEach(() => {
testZoneSpec = new FakeAsyncTestZoneSpec(
'name', false);
fakeAsyncTestZone = Zone.current.fork(testZoneSpec);
});

it('should get date diff correctly', () => {
fakeAsyncTestZone.run(() => {
const start = Date.now();
testZoneSpec.tick(100);
const end = Date.now();
expect(end - start).toBe(100);
});
});
});

describe('fakeAsyncTest should patch jasmine.clock', ifEnvSupports(() => {
return typeof jasmine.clock === 'function';
}, () => {
beforeEach(() => {
jasmine.clock().install();
});

afterEach(() => {
jasmine.clock().uninstall();
});

it('should get date diff correctly', () => {
const start = Date.now();
jasmine.clock().tick(100);
const end = Date.now();
expect(end - start).toBe(100);
});

it('should mock date correctly', () => {
const baseTime = new Date(2013, 9, 23);
jasmine.clock().mockDate(baseTime);
const start = Date.now();
expect(start).toBe(baseTime.getTime());
jasmine.clock().tick(100);
const end = Date.now();
expect(end - start).toBe(100);
expect(end).toBe(baseTime.getTime() + 100);
});

it('should handle new Date correctly', () => {
const baseTime = new Date(2013, 9, 23);
jasmine.clock().mockDate(baseTime);
const start = new Date();
expect(start.getTime()).toBe(baseTime.getTime());
jasmine.clock().tick(100);
const end = new Date();
expect(end.getTime() - start.getTime()).toBe(100);
expect(end.getTime()).toBe(baseTime.getTime() + 100);
});
}));

describe('fakeAsyncTest should patch rxjs scheduler', () => {
let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
let testZoneSpec: any;
let fakeAsyncTestZone: Zone;

beforeEach(() => {
testZoneSpec = new FakeAsyncTestZoneSpec(
'name', false);
fakeAsyncTestZone = Zone.current.fork(testZoneSpec);
});

it('should get date diff correctly', (done) => {
fakeAsyncTestZone.run(() => {
let result = null;
const observable = new Observable((subscribe: any) => {
subscribe.next('hello');
subscribe.complete();
});
observable.delay(1000).subscribe(v => {
result = v;
});
expect(result).toBe(null);
testZoneSpec.tick(1000);
expect(result).toBe('hello');
done();
});
});
});
});

0 comments on commit f22065e

Please sign in to comment.