diff --git a/gulpfile.js b/gulpfile.js index 481e17076..9ae07cfe7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -199,6 +199,14 @@ gulp.task('build/sync-test.js', ['compile-esm'], function(cb) { return generateScript('./lib/zone-spec/sync-test.ts', 'sync-test.js', false, cb); }); +gulp.task('build/rxjs.js', ['compile-esm'], function(cb) { + return generateScript('./lib/rxjs/rxjs.ts', 'zone-patch-rxjs.js', false, cb); +}); + +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/closure.js', function() { return gulp.src('./lib/closure/zone_externs.js') .pipe(gulp.dest('./dist')); @@ -237,6 +245,8 @@ gulp.task('build', [ 'build/async-test.js', 'build/fake-async-test.js', 'build/sync-test.js', + 'build/rxjs.js', + 'build/rxjs.min.js', 'build/closure.js' ]); diff --git a/karma-base.conf.js b/karma-base.conf.js index d4dc0f165..c4fb05a91 100644 --- a/karma-base.conf.js +++ b/karma-base.conf.js @@ -13,6 +13,7 @@ module.exports = function (config) { 'node_modules/systemjs/dist/system-polyfills.js', 'node_modules/systemjs/dist/system.src.js', 'node_modules/whatwg-fetch/fetch.js', + {pattern: 'node_modules/rxjs/bundles/Rx.js', watched: true, served: true, included: false}, {pattern: 'test/assets/**/*.*', watched: true, served: true, included: false}, {pattern: 'build/**/*.js.map', watched: true, served: true, included: false}, {pattern: 'build/**/*.js', watched: true, served: true, included: false} @@ -24,7 +25,7 @@ module.exports = function (config) { require('karma-sourcemap-loader') ], - preprocessors: { + preprocessors: { '**/*.js': ['sourcemap'] }, diff --git a/karma-dist.conf.js b/karma-dist.conf.js index 844aef697..0632f8d5d 100644 --- a/karma-dist.conf.js +++ b/karma-dist.conf.js @@ -18,5 +18,7 @@ module.exports = function (config) { config.files.push('dist/sync-test.js'); config.files.push('dist/task-tracking.js'); config.files.push('dist/wtf.js'); + config.files.push('node_modules/rxjs/bundles/Rx.js'); + config.files.push('dist/zone-patch-rxjs.js'); config.files.push('build/test/main.js'); }; diff --git a/lib/rxjs/rxjs.ts b/lib/rxjs/rxjs.ts new file mode 100644 index 000000000..0732c1dba --- /dev/null +++ b/lib/rxjs/rxjs.ts @@ -0,0 +1,234 @@ +/** + * @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 * as Rx from 'rxjs/Rx'; + +(Zone as any).__load_patch('rxjs', (global: any, Zone: ZoneType, api: any) => { + const symbol: (symbolString: string) => string = (Zone as any).__symbol__; + const subscribeSource = 'rxjs.subscribe'; + const nextSource = 'rxjs.Subscriber.next'; + const errorSource = 'rxjs.Subscriber.error'; + const completeSource = 'rxjs.Subscriber.complete'; + const unsubscribeSource = 'rxjs.Subscriber.unsubscribe'; + const teardownSource = 'rxjs.Subscriber.teardownLogic'; + + const patchObservableInstance = function(observable: any) { + observable._zone = Zone.current; + // patch inner function this._subscribe to check + // SubscriptionZone is same with ConstuctorZone or not + if (observable._subscribe && typeof observable._subscribe === 'function' && + !observable._originalSubscribe) { + observable._originalSubscribe = observable._subscribe; + observable._subscribe = _patchedSubscribe; + } + }; + + const _patchedSubscribe = function() { + const currentZone = Zone.current; + const _zone = this._zone; + + const args = Array.prototype.slice.call(arguments); + const subscriber = args.length > 0 ? args[0] : undefined; + // also keep currentZone in Subscriber + // for later Subscriber.next/error/complete method + if (subscriber && !subscriber._zone) { + subscriber._zone = currentZone; + } + // _subscribe should run in ConstructorZone + // but for performance concern, we should check + // whether ConsturctorZone === Zone.current here + const tearDownLogic = _zone !== Zone.current ? + _zone.run(this._originalSubscribe, this, args, subscribeSource) : + this._originalSubscribe.apply(this, args); + if (tearDownLogic && typeof tearDownLogic === 'function') { + const patchedTearDownLogic = function() { + // tearDownLogic should also run in ConstructorZone + // but for performance concern, we should check + // whether ConsturctorZone === Zone.current here + if (_zone && _zone !== Zone.current) { + return _zone.run(tearDownLogic, this, arguments, teardownSource); + } else { + return tearDownLogic.apply(this, arguments); + } + }; + return patchedTearDownLogic; + } + return tearDownLogic; + }; + + const patchObservable = function(Rx: any, observableType: string) { + const symbolObservable = symbol(observableType); + + const Observable = Rx[observableType]; + if (!Observable || Observable[symbolObservable]) { + // the subclass of Observable not loaded or have been patched + return; + } + + // monkey-patch Observable to save the + // current zone as ConstructorZone + const patchedObservable: any = Rx[observableType] = function() { + Observable.apply(this, arguments); + patchObservableInstance(this); + return this; + }; + + patchedObservable.prototype = Observable.prototype; + patchedObservable[symbolObservable] = Observable; + + Object.keys(Observable).forEach(key => { + patchedObservable[key] = Observable[key]; + }); + + const ObservablePrototype: any = Observable.prototype; + const symbolSubscribe = symbol('subscribe'); + + if (!ObservablePrototype[symbolSubscribe]) { + const subscribe = ObservablePrototype[symbolSubscribe] = ObservablePrototype.subscribe; + // patch Observable.prototype.subscribe + // if SubscripitionZone is different with ConstructorZone + // we should run _subscribe in ConstructorZone and + // create sinke in SubscriptionZone, + // and tearDown should also run into ConstructorZone + Observable.prototype.subscribe = function() { + const _zone = this._zone; + const currentZone = Zone.current; + + // if operator is involved, we should also + // patch the call method to save the Subscription zone + if (this.operator && _zone && _zone !== currentZone) { + const call = this.operator.call; + this.operator.call = function() { + const args = Array.prototype.slice.call(arguments); + const subscriber = args.length > 0 ? args[0] : undefined; + if (!subscriber._zone) { + subscriber._zone = currentZone; + } + return _zone.run(call, this, args, subscribeSource); + }; + } + const result = subscribe.apply(this, arguments); + // the result is the subscriber sink, + // we save the current Zone here + if (!result._zone) { + result._zone = currentZone; + } + return result; + }; + } + + const symbolLift = symbol('lift'); + if (!ObservablePrototype[symbolLift]) { + const lift = ObservablePrototype[symbolLift] = ObservablePrototype.lift; + + // patch lift method to save ConstructorZone of Observable + Observable.prototype.lift = function() { + const observable = lift.apply(this, arguments); + patchObservableInstance(observable); + + return observable; + }; + } + + const symbolCreate = symbol('create'); + if (!patchedObservable[symbolCreate]) { + const create = patchedObservable[symbolCreate] = Observable.create; + // patch create method to save ConstructorZone of Observable + Rx.Observable.create = function() { + const observable = create.apply(this, arguments); + patchObservableInstance(observable); + + return observable; + }; + } + }; + + const patchSubscriber = function() { + const Subscriber = Rx.Subscriber; + + const next = Subscriber.prototype.next; + const error = Subscriber.prototype.error; + const complete = Subscriber.prototype.complete; + const unsubscribe = Subscriber.prototype.unsubscribe; + + // patch Subscriber.next to make sure it run + // into SubscriptionZone + Subscriber.prototype.next = function() { + const currentZone = Zone.current; + const subscriptionZone = this._zone; + + // for performance concern, check Zone.current + // equal with this._zone(SubscriptionZone) or not + if (subscriptionZone && subscriptionZone !== currentZone) { + return subscriptionZone.run(next, this, arguments, nextSource); + } else { + return next.apply(this, arguments); + } + }; + + Subscriber.prototype.error = function() { + const currentZone = Zone.current; + const subscriptionZone = this._zone; + + // for performance concern, check Zone.current + // equal with this._zone(SubscriptionZone) or not + if (subscriptionZone && subscriptionZone !== currentZone) { + return subscriptionZone.run(error, this, arguments, errorSource); + } else { + return error.apply(this, arguments); + } + }; + + Subscriber.prototype.complete = function() { + const currentZone = Zone.current; + const subscriptionZone = this._zone; + + // for performance concern, check Zone.current + // equal with this._zone(SubscriptionZone) or not + if (subscriptionZone && subscriptionZone !== currentZone) { + return subscriptionZone.run(complete, this, arguments, completeSource); + } else { + return complete.apply(this, arguments); + } + }; + + Subscriber.prototype.unsubscribe = function() { + const currentZone = Zone.current; + const subscriptionZone = this._zone; + + // for performance concern, check Zone.current + // equal with this._zone(SubscriptionZone) or not + if (subscriptionZone && subscriptionZone !== currentZone) { + return subscriptionZone.run(unsubscribe, this, arguments, unsubscribeSource); + } else { + return unsubscribe.apply(this, arguments); + } + }; + }; + + const patchObservableFactoryCreator = function(obj: any, factoryName: string) { + const symbolFactory: string = symbol(factoryName); + if (obj[symbolFactory]) { + return; + } + const factoryCreator: any = obj[symbolFactory] = obj[factoryName]; + obj[factoryName] = function() { + const factory: any = factoryCreator.apply(this, arguments); + return function() { + const observable = factory.apply(this, arguments); + patchObservableInstance(observable); + return observable; + }; + }; + }; + + patchObservable(Rx, 'Observable'); + patchSubscriber(); + patchObservableFactoryCreator(Rx.Observable, 'bindCallback'); + patchObservableFactoryCreator(Rx.Observable, 'bindNodeCallback'); +}); \ No newline at end of file diff --git a/package.json b/package.json index c375d452b..885b39088 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "webdriver-sauce-test": "node test/webdriver/test.sauce.js", "ws-client": "node ./test/ws-client.js", "ws-server": "node ./test/ws-server.js", - "tsc": "tsc", - "tsc:w": "tsc -w", + "tsc": "tsc -p .", + "tsc:w": "tsc -w -p .", "test": "npm run tsc && concurrently \"npm run tsc:w\" \"npm run ws-server\" \"npm run karma-jasmine\"", "test:phantomjs": "npm run tsc && concurrently \"npm run tsc:w\" \"npm run ws-server\" \"npm run karma-jasmine:phantomjs\"", "test:phantomjs-single": "concurrently \"npm run ws-server\" \"npm run karma-jasmine-phantomjs:autoclose\"", @@ -87,6 +87,7 @@ "phantomjs": "^2.1.7", "promises-aplus-tests": "^2.1.2", "pump": "^1.0.1", + "rxjs": "^5.4.2", "selenium-webdriver": "^3.4.0", "systemjs": "^0.19.37", "ts-loader": "^0.6.0", diff --git a/test/browser-zone-setup.ts b/test/browser-zone-setup.ts index 1c16e96a8..57c641595 100644 --- a/test/browser-zone-setup.ts +++ b/test/browser-zone-setup.ts @@ -17,4 +17,5 @@ import '../lib/zone-spec/proxy'; import '../lib/zone-spec/sync-test'; import '../lib/zone-spec/task-tracking'; import '../lib/zone-spec/wtf'; -import '../lib/extra/cordova'; \ No newline at end of file +import '../lib/extra/cordova'; +import '../lib/rxjs/rxjs'; \ No newline at end of file diff --git a/test/browser_entry_point.ts b/test/browser_entry_point.ts index 25efb0b0f..d9f602e24 100644 --- a/test/browser_entry_point.ts +++ b/test/browser_entry_point.ts @@ -23,4 +23,5 @@ import './browser/MediaQuery.spec'; import './browser/Notification.spec'; import './mocha-patch.spec'; import './jasmine-patch.spec'; -import './extra/cordova.spec'; \ No newline at end of file +import './extra/cordova.spec'; +import './rxjs/rxjs.spec'; \ No newline at end of file diff --git a/test/common_tests.ts b/test/common_tests.ts index bd684d273..186274955 100644 --- a/test/common_tests.ts +++ b/test/common_tests.ts @@ -21,5 +21,6 @@ import './zone-spec/sync-test.spec'; import './zone-spec/fake-async-test.spec'; import './zone-spec/proxy.spec'; import './zone-spec/task-tracking.spec'; +import './rxjs/rxjs.spec'; Error.stackTraceLimit = Number.POSITIVE_INFINITY; \ No newline at end of file diff --git a/test/global-rxjs.ts b/test/global-rxjs.ts new file mode 100644 index 000000000..f012a136a --- /dev/null +++ b/test/global-rxjs.ts @@ -0,0 +1,11 @@ +/** + * @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 + */ +const globalRx: any = (window as any).Rx; +exports.Observable = globalRx.Observable; +exports.Subject = globalRx.Subject; +exports.Scheduler = globalRx.Scheduler; \ No newline at end of file diff --git a/test/main.ts b/test/main.ts index aa4f8bd2b..871994246 100644 --- a/test/main.ts +++ b/test/main.ts @@ -15,11 +15,18 @@ declare const __karma__: { __karma__.loaded = function() {}; (window as any).global = window; -System.config({defaultJSExtensions: true}); let browserPatchedPromise: any = null; if ((window as any)[(Zone as any).__symbol__('setTimeout')]) { + System.config({ + defaultJSExtensions: true, + map: {'rxjs/Rx': 'base/build/test/global-rxjs.js'}, + }); browserPatchedPromise = Promise.resolve('browserPatched'); } else { + System.config({ + defaultJSExtensions: true, + map: {'rxjs/Rx': 'base/node_modules/rxjs/bundles/Rx.js'}, + }); // this means that Zone has not patched the browser yet, which means we must be running in // build mode and need to load the browser patch. browserPatchedPromise = System.import('/base/build/test/browser-zone-setup'); diff --git a/test/node_entry_point.ts b/test/node_entry_point.ts index 0c54d2511..1a926e3c5 100644 --- a/test/node_entry_point.ts +++ b/test/node_entry_point.ts @@ -22,6 +22,7 @@ import '../lib/zone-spec/proxy'; import '../lib/zone-spec/sync-test'; import '../lib/zone-spec/task-tracking'; import '../lib/zone-spec/wtf'; +import '../lib/rxjs/rxjs'; // Setup test environment import './test-env-setup-jasmine'; diff --git a/test/rxjs/rxjs.bindCallback.spec.ts b/test/rxjs/rxjs.bindCallback.spec.ts new file mode 100644 index 000000000..014a7b503 --- /dev/null +++ b/test/rxjs/rxjs.bindCallback.spec.ts @@ -0,0 +1,86 @@ +/** + * @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 * as Rx from 'rxjs/Rx'; +import {asyncTest} from '../test-util'; + +describe('Observable.bindCallback', () => { + let log: string[]; + const constructorZone: Zone = Zone.root.fork({name: 'Constructor Zone'}); + const subscriptionZone: Zone = Zone.root.fork({name: 'Subscription Zone'}); + let func: any; + let boundFunc: any; + let observable: any; + + beforeEach(() => { + log = []; + }); + + it('bindCallback func callback should run in the correct zone', () => { + constructorZone.run(() => { + func = function(arg0: any, callback: Function) { + expect(Zone.current.name).toEqual(constructorZone.name); + callback(arg0); + }; + boundFunc = Rx.Observable.bindCallback(func); + observable = boundFunc('test'); + }); + + subscriptionZone.run(() => { + observable.subscribe((arg: any) => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('next' + arg); + }); + }); + + expect(log).toEqual(['nexttest']); + }); + + it('bindCallback with selector should run in correct zone', () => { + constructorZone.run(() => { + func = function(arg0: any, callback: Function) { + expect(Zone.current.name).toEqual(constructorZone.name); + callback(arg0); + }; + boundFunc = Rx.Observable.bindCallback(func, (arg: any) => { + return 'selector' + arg; + }); + observable = boundFunc('test'); + }); + + subscriptionZone.run(() => { + observable.subscribe((arg: any) => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('next' + arg); + }); + }); + + expect(log).toEqual(['nextselectortest']); + }); + + xit('bindCallback with async scheduler should run in correct zone', asyncTest((done: any) => { + constructorZone.run(() => { + func = function(arg0: any, callback: Function) { + expect(Zone.current.name).toEqual(constructorZone.name); + callback(arg0); + }; + boundFunc = Rx.Observable.bindCallback(func, null, Rx.Scheduler.asap); + observable = boundFunc('test'); + }); + + subscriptionZone.run(() => { + observable.subscribe((arg: any) => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('next' + arg); + done(); + }); + }); + + expect(log).toEqual([]); + }, Zone.root)); +}); \ No newline at end of file diff --git a/test/rxjs/rxjs.bindNodeCallback.spec.ts b/test/rxjs/rxjs.bindNodeCallback.spec.ts new file mode 100644 index 000000000..26ea5e1b2 --- /dev/null +++ b/test/rxjs/rxjs.bindNodeCallback.spec.ts @@ -0,0 +1,110 @@ +/** + * @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 * as Rx from 'rxjs/Rx'; +import {asyncTest} from '../test-util'; + +describe('Observable.bindNodeCallback', () => { + let log: string[]; + const constructorZone: Zone = Zone.root.fork({name: 'Constructor Zone'}); + const subscriptionZone: Zone = Zone.root.fork({name: 'Subscription Zone'}); + let func: any; + let boundFunc: any; + let observable: any; + + beforeEach(() => { + log = []; + }); + + it('bindNodeCallback func callback should run in the correct zone', () => { + constructorZone.run(() => { + func = function(arg: any, callback: (error: any, result: any) => any) { + expect(Zone.current.name).toEqual(constructorZone.name); + callback(null, arg); + }; + boundFunc = Rx.Observable.bindNodeCallback(func); + observable = boundFunc('test'); + }); + + subscriptionZone.run(() => { + observable.subscribe((arg: any) => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('next' + arg); + }); + }); + + expect(log).toEqual(['nexttest']); + }); + + it('bindNodeCallback with selector should run in correct zone', () => { + constructorZone.run(() => { + func = function(arg: any, callback: (error: any, result: any) => any) { + expect(Zone.current.name).toEqual(constructorZone.name); + callback(null, arg); + }; + boundFunc = Rx.Observable.bindNodeCallback(func, (arg: any) => { + return 'selector' + arg; + }); + observable = boundFunc('test'); + }); + + subscriptionZone.run(() => { + observable.subscribe((arg: any) => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('next' + arg); + }); + }); + + expect(log).toEqual(['nextselectortest']); + }); + + xit('bindNodeCallback with async scheduler should run in correct zone', asyncTest((done: any) => { + constructorZone.run(() => { + func = function(arg: any, callback: (error: any, result: any) => any) { + expect(Zone.current.name).toEqual(constructorZone.name); + callback(null, arg); + }; + boundFunc = Rx.Observable.bindCallback(func, null, Rx.Scheduler.asap); + observable = boundFunc('test'); + }); + + subscriptionZone.run(() => { + observable.subscribe((arg: any) => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('next' + arg); + done(); + }); + }); + + expect(log).toEqual([]); + })); + + it('bindNodeCallback call with error should run in correct zone', () => { + constructorZone.run(() => { + func = function(arg: any, callback: (error: any, result: any) => any) { + expect(Zone.current.name).toEqual(constructorZone.name); + callback(arg, null); + }; + boundFunc = Rx.Observable.bindCallback(func); + observable = boundFunc('test'); + }); + + subscriptionZone.run(() => { + observable.subscribe( + (arg: any) => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('next' + arg); + }, + (error: any) => { + log.push('error' + error); + }); + }); + + expect(log).toEqual(['nexttest,']); + }); +}); \ No newline at end of file diff --git a/test/rxjs/rxjs.combineLatest.spec.ts b/test/rxjs/rxjs.combineLatest.spec.ts new file mode 100644 index 000000000..e4d6e249e --- /dev/null +++ b/test/rxjs/rxjs.combineLatest.spec.ts @@ -0,0 +1,57 @@ +/** + * @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 * as Rx from 'rxjs/Rx'; + +describe('Observable.combineLatest', () => { + let log: string[]; + const constructorZone1: Zone = Zone.current.fork({name: 'Constructor Zone1'}); + const constructorZone2: Zone = Zone.current.fork({name: 'Constructor Zone2'}); + const constructorZone3: Zone = Zone.current.fork({name: 'Constructor Zone3'}); + const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'}); + let observable1: any; + let observable2: any; + let subscriber1: any; + let subscriber2: any; + + let combinedObservable: any; + + beforeEach(() => { + log = []; + }); + + it('bindCallback func callback should run in the correct zone', () => { + observable1 = constructorZone1.run(() => new Rx.Observable((_subscriber) => { + subscriber1 = _subscriber; + expect(Zone.current.name).toEqual(constructorZone1.name); + log.push('setup1'); + })); + observable2 = constructorZone2.run(() => new Rx.Observable((_subscriber) => { + subscriber2 = _subscriber; + expect(Zone.current.name).toEqual(constructorZone2.name); + log.push('setup2'); + })); + + constructorZone3.run(() => { + combinedObservable = Rx.Observable.combineLatest(observable1, observable2); + }); + + subscriptionZone.run(() => { + combinedObservable.subscribe((combined: any) => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push(combined); + }); + }); + + subscriber1.next(1); + subscriber2.next(2); + subscriber2.next(3); + + expect(log).toEqual(['setup1', 'setup2', [1, 2], [1, 3]]); + }); +}); \ No newline at end of file diff --git a/test/rxjs/rxjs.common.spec.ts b/test/rxjs/rxjs.common.spec.ts new file mode 100644 index 000000000..106321d96 --- /dev/null +++ b/test/rxjs/rxjs.common.spec.ts @@ -0,0 +1,208 @@ +/** + * @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 * as Rx from 'rxjs/Rx'; + +/** + * The point of these tests, is to ensure that all callbacks execute in the Zone which was active + * when the callback was passed into the Rx. + * + * The implications are: + * - Observable callback passed into `Observable` executes in the same Zone as when the + * `new Observable` was invoked. + * - The subscription callbacks passed into `subscribe` execute in the same Zone as when the + * `subscribe` method was invoked. + * - The operator callbacks passe into `map`, etc..., execute in the same Zone as when the + * `operator` (`lift`) method was invoked. + */ +describe('Zone interaction', () => { + it('should run methods in the zone of declaration', () => { + const log: string[] = []; + const constructorZone: Zone = Zone.current.fork({name: 'Constructor Zone'}); + const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'}); + let subscriber: any = null; + const observable: any = constructorZone.run(() => new Rx.Observable((_subscriber: any) => { + subscriber = _subscriber; + log.push('setup'); + expect(Zone.current.name).toEqual(constructorZone.name); + return () => { + expect(Zone.current.name).toEqual(constructorZone.name); + log.push('cleanup'); + }; + })); + subscriptionZone.run( + () => observable.subscribe( + () => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('next'); + }, + (): any => null, + () => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('complete'); + })); + subscriber.next('MyValue'); + subscriber.complete(); + + expect(log).toEqual(['setup', 'next', 'complete', 'cleanup']); + log.length = 0; + + subscriptionZone.run(() => observable.subscribe((): any => null, () => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('error'); + }, (): any => null)); + subscriber.next('MyValue'); + subscriber.error('MyError'); + + expect(log).toEqual(['setup', 'error', 'cleanup']); + }); + + it('should run methods in the zone of declaration when nexting synchronously', () => { + const log: string[] = []; + const rootZone: Zone = Zone.current; + const constructorZone: Zone = Zone.current.fork({name: 'Constructor Zone'}); + const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'}); + const observable: any = constructorZone.run(() => new Rx.Observable((subscriber: any) => { + // Execute the `next`/`complete` in different zone, and assert that + // correct zone + // is restored. + rootZone.run(() => { + subscriber.next('MyValue'); + subscriber.complete(); + }); + return () => { + expect(Zone.current.name).toEqual(constructorZone.name); + log.push('cleanup'); + }; + })); + + subscriptionZone.run( + () => observable.subscribe( + () => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('next'); + }, + (): any => null, + () => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('complete'); + })); + + expect(log).toEqual(['next', 'complete', 'cleanup']); + }); + + it('should run operators in the zone of declaration', () => { + const log: string[] = []; + const rootZone: Zone = Zone.current; + const constructorZone: Zone = Zone.current.fork({name: 'Constructor Zone'}); + const operatorZone: Zone = Zone.current.fork({name: 'Operator Zone'}); + const subscriptionZone: Zone = Zone.current.fork({name: 'Subscription Zone'}); + let observable: any = constructorZone.run(() => new Rx.Observable((subscriber: any) => { + // Execute the `next`/`complete` in different zone, and assert that + // correct zone + // is restored. + rootZone.run(() => { + subscriber.next('MyValue'); + subscriber.complete(); + }); + return () => { + expect(Zone.current.name).toEqual(constructorZone.name); + log.push('cleanup'); + }; + })); + + observable = operatorZone.run(() => observable.map((value: any) => { + expect(Zone.current.name).toEqual(operatorZone.name); + log.push('map: ' + value); + return value; + })); + + subscriptionZone.run( + () => observable.subscribe( + () => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('next'); + }, + (e: any) => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('error: ' + e); + }, + () => { + expect(Zone.current.name).toEqual(subscriptionZone.name); + log.push('complete'); + })); + + expect(log).toEqual(['map: MyValue', 'next', 'complete', 'cleanup']); + }); + + it('should run subscribe in zone of declaration with Observable.create', () => { + const log: string[] = []; + const constructorZone: Zone = Zone.current.fork({name: 'Constructor Zone'}); + let observable: any = constructorZone.run(() => Rx.Observable.create((subscriber: any) => { + subscriber.next(1); + subscriber.complete(); + return () => { + expect(Zone.current.name).toEqual(constructorZone.name); + log.push('cleanup'); + }; + })); + + observable.subscribe(() => { + log.push('next'); + }); + + expect(log).toEqual(['next', 'cleanup']); + }); + + it('should run in the zone when subscribe is called to the same Subject', () => { + const log: string[] = []; + const constructorZone: Zone = Zone.current.fork({name: 'Constructor Zone'}); + const subscriptionZone1: Zone = Zone.current.fork({name: 'Subscription Zone 1'}); + const subscriptionZone2: Zone = Zone.current.fork({name: 'Subscription Zone 2'}); + + let subject: any; + + constructorZone.run(() => { + subject = new Rx.Subject(); + }); + + let subscription1: any; + let subscription2: any; + + subscriptionZone1.run(() => { + subscription1 = subject.subscribe( + () => { + expect(Zone.current.name).toEqual(subscriptionZone1.name); + log.push('next1'); + }, + () => {}, + () => { + expect(Zone.current.name).toEqual(subscriptionZone1.name); + log.push('complete1'); + }); + }); + + subscriptionZone2.run(() => { + subscription2 = subject.subscribe( + () => { + expect(Zone.current.name).toEqual(subscriptionZone2.name); + log.push('next2'); + }, + () => {}, + () => { + expect(Zone.current.name).toEqual(subscriptionZone2.name); + log.push('complete2'); + }); + }); + + subject.next(1); + subject.complete(); + + expect(log).toEqual(['next1', 'next2', 'complete1', 'complete2']); + }); +}); \ No newline at end of file diff --git a/test/rxjs/rxjs.spec.ts b/test/rxjs/rxjs.spec.ts new file mode 100644 index 000000000..0b93d66b2 --- /dev/null +++ b/test/rxjs/rxjs.spec.ts @@ -0,0 +1,11 @@ +/** + * @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 './rxjs.common.spec'; +import './rxjs.bindCallback.spec'; +import './rxjs.bindNodeCallback.spec'; +import './rxjs.combineLatest.spec'; \ No newline at end of file diff --git a/test/test-util.ts b/test/test-util.ts index c970c7b84..86781bc83 100644 --- a/test/test-util.ts +++ b/test/test-util.ts @@ -85,4 +85,14 @@ export function isSupportSetErrorStack() { return supportSetErrorStack; } -(isSupportSetErrorStack as any).message = 'supportSetErrorStack'; \ No newline at end of file +(isSupportSetErrorStack as any).message = 'supportSetErrorStack'; + +export function asyncTest(testFn: Function, zone: Zone = Zone.current) { + const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec']; + return (done: Function) => { + let asyncTestZone: Zone = zone.fork(new AsyncTestZoneSpec(done, (error: Error) => { + fail(error); + }, 'asyncTest')); + asyncTestZone.run(testFn, this, [done]); + }; +} \ No newline at end of file diff --git a/tsconfig-esm.json b/tsconfig-esm.json index 8bfc69710..0f7cf2776 100644 --- a/tsconfig-esm.json +++ b/tsconfig-esm.json @@ -12,6 +12,7 @@ "noEmitOnError": false, "stripInternal": true, "sourceMap": true, + "moduleResolution": "node", "lib": ["es5", "dom", "es2015.promise"] }, "exclude": [