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

Commit f22065e

Browse files
JiaLiPassionmhevery
authored andcommitted
feat(jasmine): support Date.now in fakeAsyncTest (#1009)
1 parent 6a1a830 commit f22065e

File tree

5 files changed

+268
-5
lines changed

5 files changed

+268
-5
lines changed

gulpfile.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,14 @@ gulp.task('build/rxjs.min.js', ['compile-esm'], function(cb) {
284284
return generateScript('./lib/rxjs/rxjs.ts', 'zone-patch-rxjs.min.js', true, cb);
285285
});
286286

287+
gulp.task('build/rxjs-fake-async.js', ['compile-esm'], function(cb) {
288+
return generateScript('./lib/rxjs/rxjs-fake-async.ts', 'zone-patch-rxjs-fake-async.js', false, cb);
289+
});
290+
291+
gulp.task('build/rxjs-fake-async.min.js', ['compile-esm'], function(cb) {
292+
return generateScript('./lib/rxjs/rxjs-fake-async.ts', 'zone-patch-rxjs-fake-async.min.js', true, cb);
293+
});
294+
287295
gulp.task('build/closure.js', function() {
288296
return gulp.src('./lib/closure/zone_externs.js')
289297
.pipe(gulp.dest('./dist'));
@@ -337,6 +345,8 @@ gulp.task('build', [
337345
'build/sync-test.js',
338346
'build/rxjs.js',
339347
'build/rxjs.min.js',
348+
'build/rxjs-fake-async.js',
349+
'build/rxjs-fake-async.min.js',
340350
'build/closure.js'
341351
]);
342352

lib/jasmine/jasmine.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
// a `beforeEach` or `it`.
3636
const syncZone = ambientZone.fork(new SyncTestZoneSpec('jasmine.describe'));
3737

38+
const symbol = Zone.__symbol__;
39+
3840
// This is the zone which will be used for running individual tests.
3941
// It will be a proxy zone, so that the tests function can retroactively install
4042
// different zones.
@@ -45,6 +47,7 @@
4547
// - Because ProxyZone is parent fo `childZone` fakeAsync can retroactively add
4648
// fakeAsync behavior to the childZone.
4749
let testProxyZone: Zone = null;
50+
let testProxyZoneSpec: ZoneSpec = null;
4851

4952
// Monkey patch all of the jasmine DSL so that each function runs in appropriate zone.
5053
const jasmineEnv: any = jasmine.getEnv();
@@ -56,7 +59,7 @@
5659
});
5760
['it', 'xit', 'fit'].forEach((methodName) => {
5861
let originalJasmineFn: Function = jasmineEnv[methodName];
59-
jasmineEnv[Zone.__symbol__(methodName)] = originalJasmineFn;
62+
jasmineEnv[symbol(methodName)] = originalJasmineFn;
6063
jasmineEnv[methodName] = function(
6164
description: string, specDefinitions: Function, timeout: number) {
6265
arguments[1] = wrapTestInZone(specDefinitions);
@@ -65,12 +68,45 @@
6568
});
6669
['beforeEach', 'afterEach'].forEach((methodName) => {
6770
let originalJasmineFn: Function = jasmineEnv[methodName];
68-
jasmineEnv[Zone.__symbol__(methodName)] = originalJasmineFn;
71+
jasmineEnv[symbol(methodName)] = originalJasmineFn;
6972
jasmineEnv[methodName] = function(specDefinitions: Function, timeout: number) {
7073
arguments[0] = wrapTestInZone(specDefinitions);
7174
return originalJasmineFn.apply(this, arguments);
7275
};
7376
});
77+
const originalClockFn: Function = (jasmine as any)[symbol('clock')] = jasmine['clock'];
78+
(jasmine as any)['clock'] = function() {
79+
const clock = originalClockFn.apply(this, arguments);
80+
const originalTick = clock[symbol('tick')] = clock.tick;
81+
clock.tick = function() {
82+
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
83+
if (fakeAsyncZoneSpec) {
84+
return fakeAsyncZoneSpec.tick.apply(fakeAsyncZoneSpec, arguments);
85+
}
86+
return originalTick.apply(this, arguments);
87+
};
88+
const originalMockDate = clock[symbol('mockDate')] = clock.mockDate;
89+
clock.mockDate = function() {
90+
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
91+
if (fakeAsyncZoneSpec) {
92+
const dateTime = arguments[0];
93+
return fakeAsyncZoneSpec.setCurrentRealTime.apply(fakeAsyncZoneSpec, dateTime && typeof dateTime.getTime === 'function' ? [dateTime.getTime()] : arguments);
94+
}
95+
return originalMockDate.apply(this, arguments);
96+
};
97+
['install', 'uninstall'].forEach(methodName => {
98+
const originalClockFn: Function = clock[symbol(methodName)] = clock[methodName];
99+
clock[methodName] = function() {
100+
const FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
101+
if (FakeAsyncTestZoneSpec) {
102+
(jasmine as any)[symbol('clockInstalled')] = 'install' === methodName;
103+
return;
104+
}
105+
return originalClockFn.apply(this, arguments);
106+
};
107+
});
108+
return clock;
109+
};
74110

75111
/**
76112
* Gets a function wrapping the body of a Jasmine `describe` block to execute in a
@@ -82,6 +118,30 @@
82118
};
83119
}
84120

121+
function runInTestZone(testBody: Function, done?: Function) {
122+
const isClockInstalled = !!(jasmine as any)[symbol('clockInstalled')];
123+
let lastDelegate;
124+
if (isClockInstalled) {
125+
const FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
126+
if (FakeAsyncTestZoneSpec) {
127+
const _fakeAsyncTestZoneSpec = new FakeAsyncTestZoneSpec();
128+
lastDelegate = (testProxyZoneSpec as any).getDelegate();
129+
(testProxyZoneSpec as any).setDelegate(_fakeAsyncTestZoneSpec);
130+
}
131+
}
132+
try {
133+
if (done) {
134+
return testProxyZone.run(testBody, this, [done]);
135+
} else {
136+
return testProxyZone.run(testBody, this);
137+
}
138+
} finally {
139+
if (isClockInstalled) {
140+
(testProxyZoneSpec as any).setDelegate(lastDelegate);
141+
}
142+
}
143+
}
144+
85145
/**
86146
* Gets a function wrapping the body of a Jasmine `it/beforeEach/afterEach` block to
87147
* execute in a ProxyZone zone.
@@ -92,9 +152,9 @@
92152
// Note we have to make a function with correct number of arguments, otherwise jasmine will
93153
// think that all functions are sync or async.
94154
return testBody && (testBody.length ? function(done: Function) {
95-
return testProxyZone.run(testBody, this, [done]);
155+
runInTestZone(testBody, done);
96156
} : function() {
97-
return testProxyZone.run(testBody, this);
157+
runInTestZone(testBody);
98158
});
99159
}
100160
interface QueueRunner {
@@ -118,13 +178,15 @@
118178
attrs.onComplete = ((fn) => () => {
119179
// All functions are done, clear the test zone.
120180
testProxyZone = null;
181+
testProxyZoneSpec = null;
121182
ambientZone.scheduleMicroTask('jasmine.onComplete', fn);
122183
})(attrs.onComplete);
123184
_super.call(this, attrs);
124185
}
125186
ZoneQueueRunner.prototype.execute = function() {
126187
if (Zone.current !== ambientZone) throw new Error('Unexpected Zone: ' + Zone.current.name);
127-
testProxyZone = ambientZone.fork(new ProxyZoneSpec());
188+
testProxyZoneSpec = new ProxyZoneSpec();
189+
testProxyZone = ambientZone.fork(testProxyZoneSpec);
128190
if (!Zone.currentTask) {
129191
// if we are not running in a task then if someone would register a
130192
// element.addEventListener and then calling element.click() the

lib/rxjs/rxjs-fake-async.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Scheduler} from 'rxjs/Scheduler';
10+
import {async} from 'rxjs/scheduler/async';
11+
import {asap} from 'rxjs/scheduler/asap';
12+
13+
Zone.__load_patch('rxjs.Scheduler.now', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
14+
api.patchMethod(Scheduler, 'now', (delegate: Function) => (self: any, args: any[]) => {
15+
return Date.now.apply(self, args);
16+
});
17+
api.patchMethod(async, 'now', (delegate: Function) => (self: any, args: any[]) => {
18+
return Date.now.apply(self, args);
19+
});
20+
api.patchMethod(asap, 'now', (delegate: Function) => (self: any, args: any[]) => {
21+
return Date.now.apply(self, args);
22+
});
23+
});

lib/zone-spec/fake-async-test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,31 @@
2929
callbackArgs?: any;
3030
}
3131

32+
const OriginalDate = global.Date;
33+
class FakeDate {
34+
constructor() {
35+
const d = new OriginalDate();
36+
d.setTime(global.Date.now());
37+
return d;
38+
}
39+
40+
static UTC() {
41+
return OriginalDate.UTC();
42+
}
43+
44+
static now() {
45+
const fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
46+
if (fakeAsyncTestZoneSpec) {
47+
return fakeAsyncTestZoneSpec.getCurrentRealTime() + fakeAsyncTestZoneSpec.getCurrentTime();
48+
}
49+
return OriginalDate.now.apply(this, arguments);
50+
}
51+
52+
static parse() {
53+
return OriginalDate.parse();
54+
}
55+
}
56+
3257
class Scheduler {
3358
// Next scheduler id.
3459
public nextId: number = 0;
@@ -37,9 +62,23 @@
3762
private _schedulerQueue: ScheduledFunction[] = [];
3863
// Current simulated time in millis.
3964
private _currentTime: number = 0;
65+
// Current real time in millis.
66+
private _currentRealTime: number = Date.now();
4067

4168
constructor() {}
4269

70+
getCurrentTime() {
71+
return this._currentTime;
72+
}
73+
74+
getCurrentRealTime() {
75+
return this._currentRealTime;
76+
}
77+
78+
setCurrentRealTime(realTime: number) {
79+
this._currentRealTime = realTime;
80+
}
81+
4382
scheduleFunction(
4483
cb: Function, delay: number, args: any[] = [], isPeriodic: boolean = false,
4584
isRequestAnimationFrame: boolean = false, id: number = -1): number {
@@ -281,6 +320,32 @@
281320
throw error;
282321
}
283322

323+
getCurrentTime() {
324+
return this._scheduler.getCurrentTime();
325+
}
326+
327+
getCurrentRealTime() {
328+
return this._scheduler.getCurrentRealTime();
329+
}
330+
331+
setCurrentRealTime(realTime: number) {
332+
this._scheduler.setCurrentRealTime(realTime);
333+
}
334+
335+
static patchDate() {
336+
if (global['Date'] === FakeDate) {
337+
// already patched
338+
return;
339+
}
340+
global['Date'] = FakeDate;
341+
}
342+
343+
static resetDate() {
344+
if (global['Date'] === FakeDate) {
345+
global['Date'] = OriginalDate;
346+
}
347+
}
348+
284349
tick(millis: number = 0, doTick?: (elapsed: number) => void): void {
285350
FakeAsyncTestZoneSpec.assertInZone();
286351
this.flushMicrotasks();
@@ -415,6 +480,15 @@
415480
}
416481
}
417482

483+
onInvoke(delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any, applyArgs: any[], source: string): any {
484+
try {
485+
FakeAsyncTestZoneSpec.patchDate();
486+
return delegate.invoke(target, callback, applyThis, applyArgs, source);
487+
} finally {
488+
FakeAsyncTestZoneSpec.resetDate();
489+
}
490+
}
491+
418492
findMacroTaskOption(task: Task) {
419493
if (!this.macroTaskOptions) {
420494
return null;

test/zone-spec/fake-async-test.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import '../../lib/zone-spec/fake-async-test';
1010

1111
import {isNode, patchMacroTask} from '../../lib/common/utils';
1212
import {ifEnvSupports} from '../test-util';
13+
import {Observable} from 'rxjs/Observable';
14+
import 'rxjs/add/operator/delay';
15+
import '../../lib/rxjs/rxjs-fake-async';
1316

1417
function supportNode() {
1518
return isNode;
@@ -805,4 +808,95 @@ describe('FakeAsyncTestZoneSpec', () => {
805808
});
806809
});
807810
});
811+
812+
describe('fakeAsyncTest should patch Date', () => {
813+
let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
814+
let testZoneSpec: any;
815+
let fakeAsyncTestZone: Zone;
816+
817+
beforeEach(() => {
818+
testZoneSpec = new FakeAsyncTestZoneSpec(
819+
'name', false);
820+
fakeAsyncTestZone = Zone.current.fork(testZoneSpec);
821+
});
822+
823+
it('should get date diff correctly', () => {
824+
fakeAsyncTestZone.run(() => {
825+
const start = Date.now();
826+
testZoneSpec.tick(100);
827+
const end = Date.now();
828+
expect(end - start).toBe(100);
829+
});
830+
});
831+
});
832+
833+
describe('fakeAsyncTest should patch jasmine.clock', ifEnvSupports(() => {
834+
return typeof jasmine.clock === 'function';
835+
}, () => {
836+
beforeEach(() => {
837+
jasmine.clock().install();
838+
});
839+
840+
afterEach(() => {
841+
jasmine.clock().uninstall();
842+
});
843+
844+
it('should get date diff correctly', () => {
845+
const start = Date.now();
846+
jasmine.clock().tick(100);
847+
const end = Date.now();
848+
expect(end - start).toBe(100);
849+
});
850+
851+
it('should mock date correctly', () => {
852+
const baseTime = new Date(2013, 9, 23);
853+
jasmine.clock().mockDate(baseTime);
854+
const start = Date.now();
855+
expect(start).toBe(baseTime.getTime());
856+
jasmine.clock().tick(100);
857+
const end = Date.now();
858+
expect(end - start).toBe(100);
859+
expect(end).toBe(baseTime.getTime() + 100);
860+
});
861+
862+
it('should handle new Date correctly', () => {
863+
const baseTime = new Date(2013, 9, 23);
864+
jasmine.clock().mockDate(baseTime);
865+
const start = new Date();
866+
expect(start.getTime()).toBe(baseTime.getTime());
867+
jasmine.clock().tick(100);
868+
const end = new Date();
869+
expect(end.getTime() - start.getTime()).toBe(100);
870+
expect(end.getTime()).toBe(baseTime.getTime() + 100);
871+
});
872+
}));
873+
874+
describe('fakeAsyncTest should patch rxjs scheduler', () => {
875+
let FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec'];
876+
let testZoneSpec: any;
877+
let fakeAsyncTestZone: Zone;
878+
879+
beforeEach(() => {
880+
testZoneSpec = new FakeAsyncTestZoneSpec(
881+
'name', false);
882+
fakeAsyncTestZone = Zone.current.fork(testZoneSpec);
883+
});
884+
885+
it('should get date diff correctly', (done) => {
886+
fakeAsyncTestZone.run(() => {
887+
let result = null;
888+
const observable = new Observable((subscribe: any) => {
889+
subscribe.next('hello');
890+
subscribe.complete();
891+
});
892+
observable.delay(1000).subscribe(v => {
893+
result = v;
894+
});
895+
expect(result).toBe(null);
896+
testZoneSpec.tick(1000);
897+
expect(result).toBe('hello');
898+
done();
899+
});
900+
});
901+
});
808902
});

0 commit comments

Comments
 (0)