In the early Zone.js versions (< 0.10.3), `ZoneAwarePromise` did not support `Symbol.species`, so when user used a 3rd party `Promise` such as `es6-promise`, and try to load the promise library after import of `zone.js`, the loading promise library will overwrite the patched `Promise` from `zone.js` and will break `Promise` semantics with respect to `zone.js`. Starting with `zone.js` 0.10.3, `Symbol.species` is supported therefore this will not longer be an issue. (https://github.com//pull/34533) Before 0.10.3, the logic in zone.js tried to handle the case in the wrong way. It did so by overriding the descriptor of `global.Promise`, to allow the 3rd party libraries to override native `Promise` instead of `ZoneAwarePromise`. This is not the correct solution, and since the `Promise.species` is now supported, the 3rd party solution of overriding `global.Promise` is no longer needed. PR removes the wrong work around logic. (This will improve the bundle size.) PR Close #36851
		
			
				
	
	
		
			422 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			422 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * Copyright Google LLC 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 {zoneSymbol} from '../../lib/common/utils';
 | |
| 
 | |
| describe('Zone', function() {
 | |
|   const rootZone = Zone.current;
 | |
| 
 | |
|   it('should have a name', function() {
 | |
|     expect(Zone.current.name).toBeDefined();
 | |
|   });
 | |
| 
 | |
|   describe('hooks', function() {
 | |
|     it('should throw if onError is not defined', function() {
 | |
|       expect(function() {
 | |
|         Zone.current.run(throwError);
 | |
|       }).toThrow();
 | |
|     });
 | |
| 
 | |
| 
 | |
|     it('should fire onError if a function run by a zone throws', function() {
 | |
|       const errorSpy = jasmine.createSpy('error');
 | |
|       const myZone = Zone.current.fork({name: 'spy', onHandleError: errorSpy});
 | |
| 
 | |
|       expect(errorSpy).not.toHaveBeenCalled();
 | |
| 
 | |
|       expect(function() {
 | |
|         myZone.runGuarded(throwError);
 | |
|       }).not.toThrow();
 | |
| 
 | |
|       expect(errorSpy).toHaveBeenCalled();
 | |
|     });
 | |
| 
 | |
|     it('should send correct currentZone in hook method when in nested zone', function() {
 | |
|       const zone = Zone.current;
 | |
|       const zoneA = zone.fork({
 | |
|         name: 'A',
 | |
|         onInvoke: function(
 | |
|             parentDelegate, currentZone, targetZone, callback, applyThis, applyArgs, source) {
 | |
|           expect(currentZone.name).toEqual('A');
 | |
|           return parentDelegate.invoke(targetZone, callback, applyThis, applyArgs, source);
 | |
|         }
 | |
|       });
 | |
|       const zoneB = zoneA.fork({
 | |
|         name: 'B',
 | |
|         onInvoke: function(
 | |
|             parentDelegate, currentZone, targetZone, callback, applyThis, applyArgs, source) {
 | |
|           expect(currentZone.name).toEqual('B');
 | |
|           return parentDelegate.invoke(targetZone, callback, applyThis, applyArgs, source);
 | |
|         }
 | |
|       });
 | |
|       const zoneC = zoneB.fork({name: 'C'});
 | |
|       zoneC.run(function() {});
 | |
|     });
 | |
| 
 | |
|     it('should send correct currentZone in hook method when in nested zone with empty implementation',
 | |
|        function() {
 | |
|          const zone = Zone.current;
 | |
|          const zoneA = zone.fork({
 | |
|            name: 'A',
 | |
|            onInvoke: function(
 | |
|                parentDelegate, currentZone, targetZone, callback, applyThis, applyArgs, source) {
 | |
|              expect(currentZone.name).toEqual('A');
 | |
|              return parentDelegate.invoke(targetZone, callback, applyThis, applyArgs, source);
 | |
|            }
 | |
|          });
 | |
|          const zoneB = zoneA.fork({name: 'B'});
 | |
|          const zoneC = zoneB.fork({name: 'C'});
 | |
|          zoneC.run(function() {});
 | |
|        });
 | |
|   });
 | |
| 
 | |
|   it('should allow zones to be run from within another zone', function() {
 | |
|     const zone = Zone.current;
 | |
|     const zoneA = zone.fork({name: 'A'});
 | |
|     const zoneB = zone.fork({name: 'B'});
 | |
| 
 | |
|     zoneA.run(function() {
 | |
|       zoneB.run(function() {
 | |
|         expect(Zone.current).toBe(zoneB);
 | |
|       });
 | |
|       expect(Zone.current).toBe(zoneA);
 | |
|     });
 | |
|     expect(Zone.current).toBe(zone);
 | |
|   });
 | |
| 
 | |
| 
 | |
|   describe('wrap', function() {
 | |
|     it('should throw if argument is not a function', function() {
 | |
|       expect(function() {
 | |
|         (<Function>Zone.current.wrap)(11);
 | |
|       }).toThrowError('Expecting function got: 11');
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('run out side of current zone', function() {
 | |
|     it('should be able to get root zone', function() {
 | |
|       Zone.current.fork({name: 'testZone'}).run(function() {
 | |
|         expect(Zone.root.name).toEqual('<root>');
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('should be able to get run under rootZone', function() {
 | |
|       Zone.current.fork({name: 'testZone'}).run(function() {
 | |
|         Zone.root.run(() => {
 | |
|           expect(Zone.current.name).toEqual('<root>');
 | |
|         });
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('should be able to get run outside of current zone', function() {
 | |
|       Zone.current.fork({name: 'testZone'}).run(function() {
 | |
|         Zone.root.fork({name: 'newTestZone'}).run(() => {
 | |
|           expect(Zone.current.name).toEqual('newTestZone');
 | |
|           expect(Zone.current.parent!.name).toEqual('<root>');
 | |
|         });
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('get', function() {
 | |
|     it('should store properties', function() {
 | |
|       const testZone = Zone.current.fork({name: 'A', properties: {key: 'value'}});
 | |
|       expect(testZone.get('key')).toEqual('value');
 | |
|       expect(testZone.getZoneWith('key')).toEqual(testZone);
 | |
|       const childZone = testZone.fork({name: 'B', properties: {key: 'override'}});
 | |
|       expect(testZone.get('key')).toEqual('value');
 | |
|       expect(testZone.getZoneWith('key')).toEqual(testZone);
 | |
|       expect(childZone.get('key')).toEqual('override');
 | |
|       expect(childZone.getZoneWith('key')).toEqual(childZone);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('task', () => {
 | |
|     function noop() {}
 | |
|     let log: any[];
 | |
|     const zone: Zone = Zone.current.fork({
 | |
|       name: 'parent',
 | |
|       onHasTask: (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState):
 | |
|           void => {
 | |
|             (hasTaskState as any)['zone'] = target.name;
 | |
|             log.push(hasTaskState);
 | |
|           },
 | |
|       onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task) => {
 | |
|         // Do nothing to prevent tasks from being run on VM turn;
 | |
|         // Tests run task explicitly.
 | |
|         return task;
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       log = [];
 | |
|     });
 | |
| 
 | |
|     it('task can only run in the zone of creation', () => {
 | |
|       const task =
 | |
|           zone.fork({name: 'createZone'}).scheduleMacroTask('test', noop, undefined, noop, noop);
 | |
|       expect(() => {
 | |
|         Zone.current.fork({name: 'anotherZone'}).runTask(task);
 | |
|       })
 | |
|           .toThrowError(
 | |
|               'A task can only be run in the zone of creation! (Creation: createZone; Execution: anotherZone)');
 | |
|       task.zone.cancelTask(task);
 | |
|     });
 | |
| 
 | |
|     it('task can only cancel in the zone of creation', () => {
 | |
|       const task =
 | |
|           zone.fork({name: 'createZone'}).scheduleMacroTask('test', noop, undefined, noop, noop);
 | |
|       expect(() => {
 | |
|         Zone.current.fork({name: 'anotherZone'}).cancelTask(task);
 | |
|       })
 | |
|           .toThrowError(
 | |
|               'A task can only be cancelled in the zone of creation! (Creation: createZone; Execution: anotherZone)');
 | |
|       task.zone.cancelTask(task);
 | |
|     });
 | |
| 
 | |
|     it('should prevent double cancellation', () => {
 | |
|       const task =
 | |
|           zone.scheduleMacroTask('test', () => log.push('macroTask'), undefined, noop, noop);
 | |
|       zone.cancelTask(task);
 | |
|       try {
 | |
|         zone.cancelTask(task);
 | |
|       } catch (e) {
 | |
|         expect(e.message).toContain(
 | |
|             'macroTask \'test\': can not transition to \'canceling\', expecting state \'scheduled\' or \'running\', was \'notScheduled\'.');
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     it('should not decrement counters on periodic tasks', () => {
 | |
|       zone.run(() => {
 | |
|         const task = zone.scheduleMacroTask(
 | |
|             'test', () => log.push('macroTask'), {isPeriodic: true}, noop, noop);
 | |
|         zone.runTask(task);
 | |
|         zone.runTask(task);
 | |
|         zone.cancelTask(task);
 | |
|       });
 | |
|       expect(log).toEqual([
 | |
|         {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'parent'},
 | |
|         'macroTask', 'macroTask',
 | |
|         {microTask: false, macroTask: false, eventTask: false, change: 'macroTask', zone: 'parent'}
 | |
|       ]);
 | |
|     });
 | |
| 
 | |
|     it('should notify of queue status change', () => {
 | |
|       zone.run(() => {
 | |
|         const z = Zone.current;
 | |
|         z.runTask(z.scheduleMicroTask('test', () => log.push('microTask')));
 | |
|         z.cancelTask(
 | |
|             z.scheduleMacroTask('test', () => log.push('macroTask'), undefined, noop, noop));
 | |
|         z.cancelTask(
 | |
|             z.scheduleEventTask('test', () => log.push('eventTask'), undefined, noop, noop));
 | |
|       });
 | |
|       expect(log).toEqual([
 | |
|         {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
 | |
|         'microTask',
 | |
|         {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
 | |
|         {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'parent'},
 | |
|         {microTask: false, macroTask: false, eventTask: false, change: 'macroTask', zone: 'parent'},
 | |
|         {microTask: false, macroTask: false, eventTask: true, change: 'eventTask', zone: 'parent'},
 | |
|         {microTask: false, macroTask: false, eventTask: false, change: 'eventTask', zone: 'parent'}
 | |
|       ]);
 | |
|     });
 | |
| 
 | |
|     it('should notify of queue status change on parent task', () => {
 | |
|       zone.fork({name: 'child'}).run(() => {
 | |
|         const z = Zone.current;
 | |
|         z.runTask(z.scheduleMicroTask('test', () => log.push('microTask')));
 | |
|       });
 | |
|       expect(log).toEqual([
 | |
|         {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'child'},
 | |
|         {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
 | |
|         'microTask',
 | |
|         {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'child'},
 | |
|         {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'},
 | |
|       ]);
 | |
|     });
 | |
| 
 | |
|     it('should allow rescheduling a task on a separate zone', () => {
 | |
|       const log: any[] = [];
 | |
|       const zone = Zone.current.fork({
 | |
|         name: 'test-root',
 | |
|         onHasTask:
 | |
|             (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {
 | |
|               (hasTaskState as any)['zone'] = target.name;
 | |
|               log.push(hasTaskState);
 | |
|             }
 | |
|       });
 | |
|       const left = zone.fork({name: 'left'});
 | |
|       const right = zone.fork({
 | |
|         name: 'right',
 | |
|         onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task => {
 | |
|           log.push(
 | |
|               {pos: 'before', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
 | |
|           // Cancel the current scheduling of the task
 | |
|           task.cancelScheduleRequest();
 | |
|           // reschedule on a different zone.
 | |
|           task = left.scheduleTask(task);
 | |
|           log.push(
 | |
|               {pos: 'after', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
 | |
|           return task;
 | |
|         }
 | |
|       });
 | |
|       const rchild = right.fork({
 | |
|         name: 'rchild',
 | |
|         onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task => {
 | |
|           log.push(
 | |
|               {pos: 'before', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
 | |
|           task = delegate.scheduleTask(target, task);
 | |
|           log.push(
 | |
|               {pos: 'after', method: 'onScheduleTask', zone: current.name, task: task.zone.name});
 | |
|           expect((task as any)._zoneDelegates.map((zd: ZoneDelegate) => zd.zone.name)).toEqual([
 | |
|             'left', 'test-root', 'ProxyZone'
 | |
|           ]);
 | |
|           return task;
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       const task = rchild.scheduleMacroTask('testTask', () => log.push('WORK'), {}, noop, noop);
 | |
|       expect(task.zone).toEqual(left);
 | |
|       log.push(task.zone.name);
 | |
|       task.invoke();
 | |
|       expect(log).toEqual([
 | |
|         {pos: 'before', method: 'onScheduleTask', zone: 'rchild', task: 'rchild'},
 | |
|         {pos: 'before', method: 'onScheduleTask', zone: 'right', task: 'rchild'},
 | |
|         {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'left'}, {
 | |
|           microTask: false,
 | |
|           macroTask: true,
 | |
|           eventTask: false,
 | |
|           change: 'macroTask',
 | |
|           zone: 'test-root'
 | |
|         },
 | |
|         {pos: 'after', method: 'onScheduleTask', zone: 'right', task: 'left'},
 | |
|         {pos: 'after', method: 'onScheduleTask', zone: 'rchild', task: 'left'}, 'left', 'WORK',
 | |
|         {microTask: false, macroTask: false, eventTask: false, change: 'macroTask', zone: 'left'}, {
 | |
|           microTask: false,
 | |
|           macroTask: false,
 | |
|           eventTask: false,
 | |
|           change: 'macroTask',
 | |
|           zone: 'test-root'
 | |
|         }
 | |
|       ]);
 | |
|     });
 | |
| 
 | |
|     it('period task should not transit to scheduled state after being cancelled in running state',
 | |
|        () => {
 | |
|          const zone = Zone.current.fork({name: 'testZone'});
 | |
| 
 | |
|          const task = zone.scheduleMacroTask('testPeriodTask', () => {
 | |
|            zone.cancelTask(task);
 | |
|          }, {isPeriodic: true}, () => {}, () => {});
 | |
| 
 | |
|          task.invoke();
 | |
|          expect(task.state).toBe('notScheduled');
 | |
|        });
 | |
| 
 | |
|     it('event task should not transit to scheduled state after being cancelled in running state',
 | |
|        () => {
 | |
|          const zone = Zone.current.fork({name: 'testZone'});
 | |
| 
 | |
|          const task = zone.scheduleEventTask('testEventTask', () => {
 | |
|            zone.cancelTask(task);
 | |
|          }, undefined, () => {}, () => {});
 | |
| 
 | |
|          task.invoke();
 | |
|          expect(task.state).toBe('notScheduled');
 | |
|        });
 | |
| 
 | |
|     describe('assert ZoneAwarePromise', () => {
 | |
|       it('should not throw when all is OK', () => {
 | |
|         Zone.assertZonePatched();
 | |
|       });
 | |
| 
 | |
|       xit('should throw error if ZoneAwarePromise has been overwritten', () => {
 | |
|         class WrongPromise {
 | |
|           static resolve(value: any) {}
 | |
| 
 | |
|           then() {}
 | |
|         }
 | |
| 
 | |
|         const ZoneAwarePromise = global.Promise;
 | |
|         try {
 | |
|           global.Promise = WrongPromise;
 | |
|           expect(Zone.assertZonePatched()).toThrow();
 | |
|         } finally {
 | |
|           // restore it.
 | |
|           global.Promise = ZoneAwarePromise;
 | |
|         }
 | |
|         Zone.assertZonePatched();
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('invoking tasks', () => {
 | |
|     let log: string[];
 | |
|     function noop() {}
 | |
| 
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       log = [];
 | |
|     });
 | |
| 
 | |
|     it('should not drain the microtask queue too early', () => {
 | |
|       const z = Zone.current;
 | |
|       const event = z.scheduleEventTask('test', () => log.push('eventTask'), undefined, noop, noop);
 | |
| 
 | |
|       z.scheduleMicroTask('test', () => log.push('microTask'));
 | |
| 
 | |
|       const macro = z.scheduleMacroTask('test', () => {
 | |
|         event.invoke();
 | |
|         // At this point, we should not have invoked the microtask.
 | |
|         expect(log).toEqual(['eventTask']);
 | |
|       }, undefined, noop, noop);
 | |
| 
 | |
|       macro.invoke();
 | |
|     });
 | |
| 
 | |
|     it('should convert task to json without cyclic error', () => {
 | |
|       const z = Zone.current;
 | |
|       const event = z.scheduleEventTask('test', () => {}, undefined, noop, noop);
 | |
|       const micro = z.scheduleMicroTask('test', () => {});
 | |
|       const macro = z.scheduleMacroTask('test', () => {}, undefined, noop, noop);
 | |
|       expect(function() {
 | |
|         JSON.stringify(event);
 | |
|       }).not.toThrow();
 | |
|       expect(function() {
 | |
|         JSON.stringify(micro);
 | |
|       }).not.toThrow();
 | |
|       expect(function() {
 | |
|         JSON.stringify(macro);
 | |
|       }).not.toThrow();
 | |
|     });
 | |
| 
 | |
|     it('should call onHandleError callback when zoneSpec onHasTask throw error', () => {
 | |
|       const spy = jasmine.createSpy('error');
 | |
|       const hasTaskZone = Zone.current.fork({
 | |
|         name: 'hasTask',
 | |
|         onHasTask:
 | |
|             (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
 | |
|              hasTasState: HasTaskState) => {
 | |
|               throw new Error('onHasTask Error');
 | |
|             },
 | |
|         onHandleError:
 | |
|             (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: Error) => {
 | |
|               spy(error.message);
 | |
|               return delegate.handleError(targetZone, error);
 | |
|             }
 | |
|       });
 | |
| 
 | |
|       const microTask = hasTaskZone.scheduleMicroTask('test', () => {}, undefined, () => {});
 | |
|       expect(spy).toHaveBeenCalledWith('onHasTask Error');
 | |
|     });
 | |
|   });
 | |
| });
 | |
| 
 | |
| function throwError() {
 | |
|   throw new Error();
 | |
| }
 |