426 lines
16 KiB
TypeScript
426 lines
16 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 {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;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
});
|