diff --git a/gulpfile.js b/gulpfile.js index b524bf3ca..ba9918ac9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -268,6 +268,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')); @@ -317,6 +325,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..977edcf3c 100644 --- a/lib/jasmine/jasmine.ts +++ b/lib/jasmine/jasmine.ts @@ -45,6 +45,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(); @@ -71,6 +72,30 @@ return originalJasmineFn.apply(this, arguments); }; }); + const originalClockFn: Function = (jasmine as any)[Zone.__symbol__('clock')] = jasmine['clock']; + (jasmine as any)['clock'] = function() { + const clock = originalClockFn.apply(this, arguments); + const originalTick = clock[Zone.__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); + }; + ['install', 'uninstall'].forEach(methodName => { + const originalClockFn: Function = clock[Zone.__symbol__(methodName)] = clock[methodName]; + clock[methodName] = function() { + const FakeAsyncTestZoneSpec = (Zone as any)['FakeAsyncTestZoneSpec']; + if (FakeAsyncTestZoneSpec) { + (jasmine as any)[Zone.__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 +107,30 @@ }; } + function runInTestZone(testBody: Function, done?: Function) { + const isClockInstalled = !!(jasmine as any)[Zone.__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 +141,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 +167,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..e7973d129 100644 --- a/lib/zone-spec/fake-async-test.ts +++ b/lib/zone-spec/fake-async-test.ts @@ -37,9 +37,19 @@ 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; + } + scheduleFunction( cb: Function, delay: number, args: any[] = [], isPeriodic: boolean = false, isRequestAnimationFrame: boolean = false, id: number = -1): number { @@ -281,6 +291,37 @@ throw error; } + getCurrentTime() { + return this._scheduler.getCurrentTime(); + } + + getCurrentRealTime() { + return this._scheduler.getCurrentRealTime(); + } + + static patchDate() { + if ((Date as any)[Zone.__symbol__('now')]) { + // already patched + return; + } + const originalDateNow = (Date as any)[Zone.__symbol__('now')] = Date.now; + Date.now = function() { + const fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec'); + if (fakeAsyncTestZoneSpec) { + return fakeAsyncTestZoneSpec.getCurrentRealTime() + fakeAsyncTestZoneSpec.getCurrentTime(); + } + return originalDateNow.apply(this, arguments); + }; + + } + + static resetDate() { + if ((Date as any)[Zone.__symbol__('now')]) { + Date.now = ((Date as any)[Zone.__symbol__('now')]); + (Date as any)[Zone.__symbol__('now')] = undefined; + } + } + tick(millis: number = 0, doTick?: (elapsed: number) => void): void { FakeAsyncTestZoneSpec.assertInZone(); this.flushMicrotasks(); @@ -415,6 +456,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..1d30c5e61 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,70 @@ 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', () => { + 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); + }); + }); + + 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'); + }); + observable.delay(1000).subscribe(v => { + result = v; + }); + expect(result).toBeNull(); + testZoneSpec.tick(1000); + expect(result).toBe('hello'); + done(); + }); + }); + }); });