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

461 lines
16 KiB
TypeScript
Raw Normal View History

/**
* @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 {isBrowser} from '../../lib/common/utils';
import {isSafari, zoneSymbol} from '../test-util';
// simulate @angular/facade/src/error.ts
class BaseError extends Error {
/** @internal **/
_nativeError: Error;
constructor(message: string) {
super(message);
const nativeError = new Error(message) as any as Error;
this._nativeError = nativeError;
}
get message() {
return this._nativeError.message;
}
set message(message) {
this._nativeError.message = message;
}
get name() {
return this._nativeError.name;
}
get stack() {
return (this._nativeError as any).stack;
}
set stack(value) {
(this._nativeError as any).stack = value;
}
toString() {
return this._nativeError.toString();
}
}
class WrappedError extends BaseError {
originalError: any;
constructor(message: string, error: any) {
super(`${message} caused by: ${error instanceof Error ? error.message : error}`);
this.originalError = error;
}
get stack() {
return ((this.originalError instanceof Error ? this.originalError : this._nativeError) as any)
.stack;
}
}
class TestError extends WrappedError {
constructor(message: string, error: any) {
super(`${message} caused by: ${error instanceof Error ? error.message : error}`, error);
}
get message() {
return 'test ' + this.originalError.message;
}
}
class TestMessageError extends WrappedError {
constructor(message: string, error: any) {
super(`${message} caused by: ${error instanceof Error ? error.message : error}`, error);
}
get message() {
return 'test ' + this.originalError.message;
}
set message(value) {
this.originalError.message = value;
}
}
describe('ZoneAwareError', () => {
// If the environment does not supports stack rewrites, then these tests will fail
// and there is no point in running them.
const _global: any = typeof window !== 'undefined' ? window : global;
let config: any;
const __karma__ = _global.__karma__;
if (typeof __karma__ !== 'undefined') {
config = __karma__ && (__karma__ as any).config;
} else if (typeof process !== 'undefined') {
config = process.env;
}
const policy = (config && config['errorpolicy']) || 'default';
if (!(Error as any)['stackRewrite'] && policy !== 'disable') return;
it('should keep error prototype chain correctly', () => {
class MyError extends Error {}
const myError = new MyError();
expect(myError instanceof Error).toBe(true);
expect(myError instanceof MyError).toBe(true);
expect(myError.stack).not.toBe(undefined);
});
it('should instanceof error correctly', () => {
let myError = Error('myError');
expect(myError instanceof Error).toBe(true);
let myError1 = Error.call(undefined, 'myError');
expect(myError1 instanceof Error).toBe(true);
let myError2 = Error.call(global, 'myError');
expect(myError2 instanceof Error).toBe(true);
let myError3 = Error.call({}, 'myError');
expect(myError3 instanceof Error).toBe(true);
let myError4 = Error.call({test: 'test'}, 'myError');
expect(myError4 instanceof Error).toBe(true);
});
it('should return error itself from constructor', () => {
class MyError1 extends Error {
constructor() {
const err: any = super('MyError1');
this.message = err.message;
}
}
let myError1 = new MyError1();
expect(myError1.message).toEqual('MyError1');
expect(myError1.name).toEqual('Error');
});
it('should return error by calling error directly', () => {
let myError = Error('myError');
expect(myError.message).toEqual('myError');
let myError1 = Error.call(undefined, 'myError');
expect(myError1.message).toEqual('myError');
let myError2 = Error.call(global, 'myError');
expect(myError2.message).toEqual('myError');
let myError3 = Error.call({}, 'myError');
expect(myError3.message).toEqual('myError');
});
it('should have browser specified property', () => {
let myError = new Error('myError');
if (Object.prototype.hasOwnProperty.call(Error.prototype, 'description')) {
// in IE, error has description property
expect((<any>myError).description).toEqual('myError');
}
if (Object.prototype.hasOwnProperty.call(Error.prototype, 'fileName')) {
// in firefox, error has fileName property
expect((<any>myError).fileName).toBeTruthy();
}
});
it('should not use child Error class get/set in ZoneAwareError constructor', () => {
const func = () => {
const error = new BaseError('test');
expect(error.message).toEqual('test');
};
expect(func).not.toThrow();
});
it('should behave correctly with wrapped error', () => {
const error = new TestError('originalMessage', new Error('error message'));
expect(error.message).toEqual('test error message');
error.originalError.message = 'new error message';
expect(error.message).toEqual('test new error message');
const error1 = new TestMessageError('originalMessage', new Error('error message'));
expect(error1.message).toEqual('test error message');
error1.message = 'new error message';
expect(error1.message).toEqual('test new error message');
});
it('should copy customized NativeError properties to ZoneAwareError', () => {
const spy = jasmine.createSpy('errorCustomFunction');
const NativeError = (global as any)[(Zone as any).__symbol__('Error')];
NativeError.customFunction = function(args: any) {
spy(args);
};
expect((Error as any)['customProperty']).toBe('customProperty');
expect(typeof (Error as any)['customFunction']).toBe('function');
(Error as any)['customFunction']('test');
expect(spy).toHaveBeenCalledWith('test');
});
it('should always have stack property even without throw', () => {
// in IE, the stack will be undefined without throw
// in ZoneAwareError, we will make stack always be
// there event without throw
const error = new Error('test');
const errorWithoutNew = Error('test');
expect(error.stack!.split('\n').length > 0).toBeTruthy();
expect(errorWithoutNew.stack!.split('\n').length > 0).toBeTruthy();
});
it('should show zone names in stack frames and remove extra frames', () => {
if (policy === 'disable' || !(Error as any)['stackRewrite']) {
return;
}
if (isBrowser && isSafari()) {
return;
}
const rootZone = Zone.root;
const innerZone = rootZone.fork({name: 'InnerZone'});
rootZone.run(testFn);
function testFn() {
let outside: any;
let inside: any;
let outsideWithoutNew: any;
let insideWithoutNew: any;
try {
throw new Error('Outside');
} catch (e) {
outside = e;
}
try {
throw Error('Outside');
} catch (e) {
outsideWithoutNew = e;
}
innerZone.run(function insideRun() {
try {
throw new Error('Inside');
} catch (e) {
inside = e;
}
try {
throw Error('Inside');
} catch (e) {
insideWithoutNew = e;
}
});
if (policy === 'lazy') {
outside.stack = outside.zoneAwareStack;
outsideWithoutNew.stack = outsideWithoutNew.zoneAwareStack;
inside.stack = inside.zoneAwareStack;
insideWithoutNew.stack = insideWithoutNew.zoneAwareStack;
}
expect(outside.stack).toEqual(outside.zoneAwareStack);
expect(outsideWithoutNew.stack).toEqual(outsideWithoutNew.zoneAwareStack);
expect(inside!.stack).toEqual(inside!.zoneAwareStack);
expect(insideWithoutNew!.stack).toEqual(insideWithoutNew!.zoneAwareStack);
expect(typeof inside!.originalStack).toEqual('string');
expect(typeof insideWithoutNew!.originalStack).toEqual('string');
const outsideFrames = outside.stack!.split(/\n/);
const insideFrames = inside!.stack!.split(/\n/);
const outsideWithoutNewFrames = outsideWithoutNew!.stack!.split(/\n/);
const insideWithoutNewFrames = insideWithoutNew!.stack!.split(/\n/);
// throw away first line if it contains the error
if (/Outside/.test(outsideFrames[0])) {
outsideFrames.shift();
}
if (/Error /.test(outsideFrames[0])) {
outsideFrames.shift();
}
if (/Outside/.test(outsideWithoutNewFrames[0])) {
outsideWithoutNewFrames.shift();
}
if (/Error /.test(outsideWithoutNewFrames[0])) {
outsideWithoutNewFrames.shift();
}
if (/Inside/.test(insideFrames[0])) {
insideFrames.shift();
}
if (/Error /.test(insideFrames[0])) {
insideFrames.shift();
}
if (/Inside/.test(insideWithoutNewFrames[0])) {
insideWithoutNewFrames.shift();
}
if (/Error /.test(insideWithoutNewFrames[0])) {
insideWithoutNewFrames.shift();
}
expect(outsideFrames[0]).toMatch(/testFn.*[<root>]/);
expect(insideFrames[0]).toMatch(/insideRun.*[InnerZone]]/);
expect(insideFrames[1]).toMatch(/testFn.*[<root>]]/);
expect(outsideWithoutNewFrames[0]).toMatch(/testFn.*[<root>]/);
expect(insideWithoutNewFrames[0]).toMatch(/insideRun.*[InnerZone]]/);
expect(insideWithoutNewFrames[1]).toMatch(/testFn.*[<root>]]/);
}
});
const zoneAwareFrames = [
'Zone.run', 'Zone.runGuarded', 'Zone.scheduleEventTask', 'Zone.scheduleMicroTask',
'Zone.scheduleMacroTask', 'Zone.runTask', 'ZoneDelegate.scheduleTask',
'ZoneDelegate.invokeTask', 'zoneAwareAddListener', 'Zone.prototype.run',
'Zone.prototype.runGuarded', 'Zone.prototype.scheduleEventTask',
'Zone.prototype.scheduleMicroTask', 'Zone.prototype.scheduleMacroTask',
'Zone.prototype.runTask', 'ZoneDelegate.prototype.scheduleTask',
'ZoneDelegate.prototype.invokeTask', 'ZoneTask.invokeTask'
];
function assertStackDoesNotContainZoneFrames(err: any) {
const frames = policy === 'lazy' ? err.zoneAwareStack.split('\n') : err.stack.split('\n');
if (policy === 'disable') {
let hasZoneStack = false;
for (let i = 0; i < frames.length; i++) {
if (hasZoneStack) {
break;
}
hasZoneStack = zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1).length > 0;
}
if (!hasZoneStack) {
console.log('stack', err.originalStack);
}
expect(hasZoneStack).toBe(true);
} else {
for (let i = 0; i < frames.length; i++) {
expect(zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1)).toEqual([]);
}
}
};
const errorZoneSpec = {
name: 'errorZone',
done: <(() => void)|null>null,
onHandleError:
(parentDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: Error) => {
assertStackDoesNotContainZoneFrames(error);
setTimeout(() => {
errorZoneSpec.done && errorZoneSpec.done();
}, 0);
return false;
}
};
const errorZone = Zone.root.fork(errorZoneSpec);
const assertStackDoesNotContainZoneFramesTest = function(testFn: Function) {
return function(done: () => void) {
errorZoneSpec.done = done;
errorZone.run(testFn);
};
};
describe('Error stack', () => {
it('Error with new which occurs in setTimeout callback should not have zone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
setTimeout(() => {
throw new Error('timeout test error');
}, 10);
}));
it('Error without new which occurs in setTimeout callback should not have zone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
setTimeout(() => {
throw Error('test error');
}, 10);
}));
it('Error with new which cause by promise rejection should not have zone frames visible',
(done) => {
const p = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('test error'));
});
});
p.catch(err => {
assertStackDoesNotContainZoneFrames(err);
done();
});
});
it('Error without new which cause by promise rejection should not have zone frames visible',
(done) => {
const p = new Promise((resolve, reject) => {
setTimeout(() => {
reject(Error('test error'));
});
});
p.catch(err => {
assertStackDoesNotContainZoneFrames(err);
done();
});
});
it('Error with new which occurs in eventTask callback should not have zone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
const task = Zone.current.scheduleEventTask('errorEvent', () => {
throw new Error('test error');
}, undefined, () => null, undefined);
task.invoke();
}));
it('Error without new which occurs in eventTask callback should not have zone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
const task = Zone.current.scheduleEventTask('errorEvent', () => {
throw Error('test error');
}, undefined, () => null, undefined);
task.invoke();
}));
it('Error with new which occurs in longStackTraceZone should not have zone frames and longStackTraceZone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec'])
.scheduleEventTask('errorEvent', () => {
throw new Error('test error');
}, undefined, () => null, undefined);
task.invoke();
}));
it('Error without new which occurs in longStackTraceZone should not have zone frames and longStackTraceZone frames visible',
assertStackDoesNotContainZoneFramesTest(() => {
const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec'])
.scheduleEventTask('errorEvent', () => {
throw Error('test error');
}, undefined, () => null, undefined);
task.invoke();
}));
it('stack frames of the callback in user customized zoneSpec should be kept',
assertStackDoesNotContainZoneFramesTest(() => {
const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec'])
.fork({
name: 'customZone',
onScheduleTask: (parentDelegate, currentZone, targetZone, task) => {
return parentDelegate.scheduleTask(targetZone, task);
},
onHandleError: (parentDelegate, currentZone, targetZone, error) => {
parentDelegate.handleError(targetZone, error);
const containsCustomZoneSpecStackTrace =
error.stack.indexOf('onScheduleTask') !== -1;
expect(containsCustomZoneSpecStackTrace).toBeTruthy();
return false;
}
})
.scheduleEventTask('errorEvent', () => {
throw new Error('test error');
}, undefined, () => null, undefined);
task.invoke();
}));
it('should be able to generate zone free stack even NativeError stack is readonly', function() {
const _global: any =
typeof window === 'object' && window || typeof self === 'object' && self || global;
const NativeError = _global[zoneSymbol('Error')];
const desc = Object.getOwnPropertyDescriptor(NativeError.prototype, 'stack');
if (desc) {
const originalSet: ((value: any) => void)|undefined = desc.set;
// make stack readonly
desc.set = null as any;
try {
const error = new Error('test error');
expect(error.stack).toBeTruthy();
assertStackDoesNotContainZoneFrames(error);
} finally {
desc.set = originalSet;
}
}
});
});
});