mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Error-handling in AIO happens mainly in two places: 1. For errors happening inside the app we have a custom `ErrorHandler` implementation, `ReportingErrorHandler`. `ReportingErrorHandler` passes errors to the default `ErrorHandler` (for them to be logged to the console) and also forwards them to `window.onerror()`. 2. Errors happening outside the app and errors forwarded by `ReportingErrorHandler` are handled by `window.onerror()`, which in turn reports them to Google analytics. Previously, we were making some assumptions (which turned out to be incorrect based on the info captured in Google analytics - see #28106): - `ReportingErrorHandler` assumed that the errors passed to its `handleError()` method would be either strings or `Error` instances. _Apparently, other values (such as `null` or `undefined`) may also be passed._ - `window.onerror()` assumed that if an `Error` instance was passed in, it would always have a stacktrace (i.e. its `stack` property would be defined). _This is not necessarily true, although it is not clear (based on the logs) whether reported errors of this type are caused by `Error` instance with no stacktrace or by non-string error objects which are incorrectly treated as `Error` instances. This commit ensures that all types of error arguments can be handled correctly, including `Error` instances with no stacktrace and other types of objects or primitives. NOTE: PR #42881 is related as it fixes handling `null` and `undefined` arguments in the default `ErrorHandler`. Fixes #28106 PR Close #42883
95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
import { ErrorHandler, Injector } from '@angular/core';
|
|
import { TestBed } from '@angular/core/testing';
|
|
import { WindowToken } from 'app/shared/window';
|
|
import { AppModule } from 'app/app.module';
|
|
|
|
import { ReportingErrorHandler } from './reporting-error-handler';
|
|
|
|
describe('ReportingErrorHandler service', () => {
|
|
let handler: ReportingErrorHandler;
|
|
let superHandler: jasmine.Spy;
|
|
let onerrorSpy: jasmine.Spy;
|
|
|
|
beforeEach(() => {
|
|
onerrorSpy = jasmine.createSpy('onerror');
|
|
superHandler = spyOn(ErrorHandler.prototype, 'handleError');
|
|
|
|
const injector = Injector.create({providers: [
|
|
{ provide: ErrorHandler, useClass: ReportingErrorHandler, deps: [WindowToken] },
|
|
{ provide: WindowToken, useFactory: () => ({ onerror: onerrorSpy }), deps: [] }
|
|
]});
|
|
|
|
handler = injector.get(ErrorHandler) as unknown as ReportingErrorHandler;
|
|
});
|
|
|
|
it('should be registered on the AppModule', () => {
|
|
handler = TestBed.configureTestingModule({ imports: [AppModule] }).inject(ErrorHandler) as any;
|
|
expect(handler).toEqual(jasmine.any(ReportingErrorHandler));
|
|
});
|
|
|
|
describe('handleError', () => {
|
|
it('should call the super class handleError', () => {
|
|
const error = new Error();
|
|
handler.handleError(error);
|
|
expect(superHandler).toHaveBeenCalledWith(error);
|
|
});
|
|
|
|
it('should cope with the super handler throwing an error', () => {
|
|
const error = new Error('initial error');
|
|
superHandler.and.throwError('super handler error');
|
|
handler.handleError(error);
|
|
|
|
expect(onerrorSpy).toHaveBeenCalledTimes(2);
|
|
|
|
// Error from super handler is reported first
|
|
expect(onerrorSpy.calls.argsFor(0)[0]).toEqual('super handler error');
|
|
expect(onerrorSpy.calls.argsFor(0)[4]).toEqual(jasmine.any(Error));
|
|
|
|
// Then error from initial exception
|
|
expect(onerrorSpy.calls.argsFor(1)[0]).toEqual('initial error');
|
|
expect(onerrorSpy.calls.argsFor(1)[4]).toEqual(error);
|
|
});
|
|
|
|
it('should send an error object to window.onerror', () => {
|
|
const error = new Error('this is an error message');
|
|
handler.handleError(error);
|
|
expect(onerrorSpy).toHaveBeenCalledWith(error.message, undefined, undefined, undefined, error);
|
|
});
|
|
|
|
it('should send a non-error object to window.onerror', () => {
|
|
const error = {reason: 'this is an error message'};
|
|
handler.handleError(error);
|
|
expect(onerrorSpy).toHaveBeenCalledWith(JSON.stringify(error));
|
|
});
|
|
|
|
it('should send a non-error object with circular references to window.onerror', () => {
|
|
const error = {
|
|
reason: 'this is an error message',
|
|
get self() { return this; },
|
|
toString() { return `{reason: ${this.reason}}`; },
|
|
};
|
|
handler.handleError(error);
|
|
expect(onerrorSpy).toHaveBeenCalledWith('{reason: this is an error message}');
|
|
});
|
|
|
|
it('should send an error string to window.onerror', () => {
|
|
const error = 'this is an error message';
|
|
handler.handleError(error);
|
|
expect(onerrorSpy).toHaveBeenCalledWith(error);
|
|
});
|
|
|
|
it('should send a non-object, non-string error stringified to window.onerror', () => {
|
|
handler.handleError(404);
|
|
handler.handleError(false);
|
|
handler.handleError(null);
|
|
handler.handleError(undefined);
|
|
|
|
expect(onerrorSpy.calls.allArgs()).toEqual([
|
|
['404'],
|
|
['false'],
|
|
['null'],
|
|
['undefined'],
|
|
]);
|
|
});
|
|
});
|
|
});
|