From 878b904b747d23918b91cfed17c970387694cde3 Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Wed, 31 Jan 2018 19:02:48 +0900 Subject: [PATCH] feat(jasmine): support Date.now in fakeAsyncTest --- gulpfile.js | 10 +++ lib/jasmine/jasmine.ts | 72 ++++++++++++++++++-- lib/rxjs/rxjs-fake-async.ts | 23 +++++++ lib/zone-spec/fake-async-test.ts | 74 ++++++++++++++++++++ test/zone-spec/fake-async-test.spec.ts | 94 ++++++++++++++++++++++++++ 5 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 lib/rxjs/rxjs-fake-async.ts diff --git a/gulpfile.js b/gulpfile.js index c79d1970e..ea3d0bcfc 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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')); @@ -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' ]); diff --git a/lib/jasmine/jasmine.ts b/lib/jasmine/jasmine.ts index ce5c018c7..ddca40bcf 100644 --- a/lib/jasmine/jasmine.ts +++ b/lib/jasmine/jasmine.ts @@ -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. @@ -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(); @@ -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); @@ -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 @@ -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. @@ -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 { @@ -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 diff --git a/lib/rxjs/rxjs-fake-async.ts b/lib/rxjs/rxjs-fake-async.ts new file mode 100644 index 000000000..b5bb44bd9 --- /dev/null +++ b/lib/rxjs/rxjs-fake-async.ts @@ -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); + }); +}); diff --git a/lib/zone-spec/fake-async-test.ts b/lib/zone-spec/fake-async-test.ts index 72ef0e115..4e61c04b9 100644 --- a/lib/zone-spec/fake-async-test.ts +++ b/lib/zone-spec/fake-async-test.ts @@ -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; @@ -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 { @@ -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(); @@ -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; diff --git a/test/zone-spec/fake-async-test.spec.ts b/test/zone-spec/fake-async-test.spec.ts index 7e2dfe5c8..d9d3434ac 100644 --- a/test/zone-spec/fake-async-test.spec.ts +++ b/test/zone-spec/fake-async-test.spec.ts @@ -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; @@ -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(); + }); + }); + }); });