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

feat(jasmine): support Date.now in fakeAsyncTest #1009

Merged
merged 1 commit into from
Feb 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,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 @@ -327,6 +335,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();
});
});
});
});