angular-docs-cn/packages/zone.js/test/common/zone.spec.ts

405 lines
15 KiB
TypeScript

/**
* @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 {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(); });
it('should keep ZoneAwarePromise has been patched', () => {
class WrongPromise {
static resolve(value: any) {}
then() {}
}
const ZoneAwarePromise = global.Promise;
const NativePromise = (global as any)[zoneSymbol('Promise')];
global.Promise = WrongPromise;
try {
expect(ZoneAwarePromise).toBeTruthy();
Zone.assertZonePatched();
expect(global.Promise).toBe(ZoneAwarePromise);
} finally {
// restore it.
global.Promise = NativePromise;
}
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();
}