fix(ivy): errors not being logged to ErrorHandler (#28447)

Fixes Ivy not passing thrown errors along to the `ErrorHandler`.

**Note:** the failing test had to be reworked a little bit, because it has some assertions that depend on an error context being logged, however Ivy doesn't keep track of the error context.

This PR resolves FW-840.

PR Close #28447
This commit is contained in:
Kristiyan Kostadinov 2019-01-30 15:23:23 +01:00 committed by Matias Niemelä
parent a98d66078d
commit fc88a79b32
3 changed files with 137 additions and 79 deletions

View File

@ -8,6 +8,7 @@
import {InjectFlags, InjectionToken, Injector} from '../di'; import {InjectFlags, InjectionToken, Injector} from '../di';
import {resolveForwardRef} from '../di/forward_ref'; import {resolveForwardRef} from '../di/forward_ref';
import {ErrorHandler} from '../error_handler';
import {Type} from '../interface/type'; import {Type} from '../interface/type';
import {validateAttribute, validateProperty} from '../sanitization/sanitization'; import {validateAttribute, validateProperty} from '../sanitization/sanitization';
import {Sanitizer} from '../sanitization/security'; import {Sanitizer} from '../sanitization/security';
@ -936,6 +937,7 @@ function listenerInternal(
const tView = lView[TVIEW]; const tView = lView[TVIEW];
const firstTemplatePass = tView.firstTemplatePass; const firstTemplatePass = tView.firstTemplatePass;
const tCleanup: false|any[] = firstTemplatePass && (tView.cleanup || (tView.cleanup = [])); const tCleanup: false|any[] = firstTemplatePass && (tView.cleanup || (tView.cleanup = []));
ngDevMode && assertNodeOfPossibleTypes( ngDevMode && assertNodeOfPossibleTypes(
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer); tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer);
@ -2605,13 +2607,17 @@ function wrapListener(
markViewDirty(startView); markViewDirty(startView);
} }
const result = listenerFn(e); try {
if (wrapWithPreventDefault && result === false) { const result = listenerFn(e);
e.preventDefault(); if (wrapWithPreventDefault && result === false) {
// Necessary for legacy browsers that don't support preventDefault (e.g. IE) e.preventDefault();
e.returnValue = false; // Necessary for legacy browsers that don't support preventDefault (e.g. IE)
e.returnValue = false;
}
return result;
} catch (error) {
handleError(lView, error);
} }
return result;
}; };
} }
/** /**
@ -2723,12 +2729,17 @@ export function detectChangesInternal<T>(view: LView, context: T) {
if (rendererFactory.begin) rendererFactory.begin(); if (rendererFactory.begin) rendererFactory.begin();
if (isCreationMode(view)) { try {
checkView(view, context); // creation mode pass if (isCreationMode(view)) {
checkView(view, context); // creation mode pass
}
checkView(view, context); // update mode pass
} catch (error) {
handleError(view, error);
throw error;
} finally {
if (rendererFactory.end) rendererFactory.end();
} }
checkView(view, context); // update mode pass
if (rendererFactory.end) rendererFactory.end();
} }
/** /**
@ -3237,3 +3248,10 @@ function loadComponentRenderer(tNode: TNode, lView: LView): Renderer3 {
const componentLView = lView[tNode.index] as LView; const componentLView = lView[tNode.index] as LView;
return componentLView[RENDERER]; return componentLView[RENDERER];
} }
/** Handles an error thrown in an LView. */
function handleError(lView: LView, error: any): void {
const injector = lView[INJECTOR];
const errorHandler = injector ? injector.get(ErrorHandler, null) : null;
errorHandler && errorHandler.handleError(error);
}

View File

@ -50,12 +50,24 @@
{ {
"name": "EMPTY_OBJ" "name": "EMPTY_OBJ"
}, },
{
"name": "ERROR_DEBUG_CONTEXT"
},
{
"name": "ERROR_LOGGER"
},
{
"name": "ERROR_ORIGINAL_ERROR"
},
{ {
"name": "ElementRef" "name": "ElementRef"
}, },
{ {
"name": "EmptyErrorImpl" "name": "EmptyErrorImpl"
}, },
{
"name": "ErrorHandler"
},
{ {
"name": "FLAGS" "name": "FLAGS"
}, },
@ -491,6 +503,9 @@
{ {
"name": "decreaseElementDepthCount" "name": "decreaseElementDepthCount"
}, },
{
"name": "defaultErrorLogger"
},
{ {
"name": "defaultScheduler" "name": "defaultScheduler"
}, },
@ -641,6 +656,9 @@
{ {
"name": "getCurrentView" "name": "getCurrentView"
}, },
{
"name": "getDebugContext"
},
{ {
"name": "getDirectiveDef" "name": "getDirectiveDef"
}, },
@ -656,6 +674,9 @@
{ {
"name": "getElementDepthCount" "name": "getElementDepthCount"
}, },
{
"name": "getErrorLogger"
},
{ {
"name": "getHighestElementOrICUContainer" "name": "getHighestElementOrICUContainer"
}, },
@ -722,6 +743,9 @@
{ {
"name": "getOrCreateTView" "name": "getOrCreateTView"
}, },
{
"name": "getOriginalError"
},
{ {
"name": "getParentInjectorIndex" "name": "getParentInjectorIndex"
}, },
@ -803,6 +827,9 @@
{ {
"name": "getValue" "name": "getValue"
}, },
{
"name": "handleError"
},
{ {
"name": "hasClassInput" "name": "hasClassInput"
}, },

View File

@ -450,84 +450,97 @@ function declareTestsUsingBootstrap() {
if (getDOM().supportsDOMEvents()) { if (getDOM().supportsDOMEvents()) {
// This test needs a real DOM.... // This test needs a real DOM....
fixmeIvy('FW-840: Exceptions thrown in event handlers are not reported to ErrorHandler') it('should keep change detecting if there was an error', (done) => {
.it('should keep change detecting if there was an error', (done) => { @Component({
@Component({ selector: COMP_SELECTOR,
selector: COMP_SELECTOR, template:
template: '<button (click)="next()"></button><button (click)="nextAndThrow()"></button><button (dirClick)="nextAndThrow()"></button><span>Value:{{value}}</span><span>{{throwIfNeeded()}}</span>'
'<button (click)="next()"></button><button (click)="nextAndThrow()"></button><button (dirClick)="nextAndThrow()"></button><span>Value:{{value}}</span><span>{{throwIfNeeded()}}</span>' })
}) class ErrorComp {
class ErrorComp { value = 0;
value = 0; thrownValue = 0;
thrownValue = 0; next() { this.value++; }
next() { this.value++; } nextAndThrow() {
nextAndThrow() { this.value++;
this.value++; this.throwIfNeeded();
this.throwIfNeeded(); }
} throwIfNeeded() {
throwIfNeeded() { NgZone.assertInAngularZone();
NgZone.assertInAngularZone(); if (this.thrownValue !== this.value) {
if (this.thrownValue !== this.value) { this.thrownValue = this.value;
this.thrownValue = this.value; throw new Error(`Error: ${this.value}`);
throw new Error(`Error: ${this.value}`);
}
}
} }
}
}
@Directive({selector: '[dirClick]'}) @Directive({selector: '[dirClick]'})
class EventDir { class EventDir {
@Output() @Output()
dirClick = new EventEmitter(); dirClick = new EventEmitter();
@HostListener('click', ['$event']) @HostListener('click', ['$event'])
onClick(event: any) { this.dirClick.next(event); } onClick(event: any) { this.dirClick.next(event); }
} }
@NgModule({ @NgModule({
imports: [BrowserModule], imports: [BrowserModule],
declarations: [ErrorComp, EventDir], declarations: [ErrorComp, EventDir],
bootstrap: [ErrorComp], bootstrap: [ErrorComp],
providers: [{provide: ErrorHandler, useValue: errorHandler}], providers: [{provide: ErrorHandler, useValue: errorHandler}],
}) })
class TestModule { class TestModule {
} }
platformBrowserDynamic().bootstrapModule(TestModule).then((ref) => { platformBrowserDynamic().bootstrapModule(TestModule).then((ref) => {
NgZone.assertNotInAngularZone(); NgZone.assertNotInAngularZone();
const appRef = ref.injector.get(ApplicationRef) as ApplicationRef; const appRef = ref.injector.get(ApplicationRef) as ApplicationRef;
const compRef = appRef.components[0] as ComponentRef<ErrorComp>; const compRef = appRef.components[0] as ComponentRef<ErrorComp>;
const compEl = compRef.location.nativeElement; const compEl = compRef.location.nativeElement;
const nextBtn = compEl.children[0]; const nextBtn = compEl.children[0];
const nextAndThrowBtn = compEl.children[1]; const nextAndThrowBtn = compEl.children[1];
const nextAndThrowDirBtn = compEl.children[2]; const nextAndThrowDirBtn = compEl.children[2];
nextBtn.click(); // Note: the amount of events sent to the logger will differ between ViewEngine
assertValueAndErrors(compEl, 1, 0); // and Ivy, because Ivy doesn't attach an error context. This means that the amount
nextBtn.click(); // of logged errors increases by 1 for Ivy and 2 for ViewEngine after each event.
assertValueAndErrors(compEl, 2, 2); const errorDelta = ivyEnabled ? 1 : 2;
let currentErrorIndex = 0;
nextAndThrowBtn.click(); nextBtn.click();
assertValueAndErrors(compEl, 3, 4); assertValueAndErrors(compEl, 1, currentErrorIndex);
nextAndThrowBtn.click(); currentErrorIndex += errorDelta;
assertValueAndErrors(compEl, 4, 6); nextBtn.click();
assertValueAndErrors(compEl, 2, currentErrorIndex);
currentErrorIndex += errorDelta;
nextAndThrowDirBtn.click(); nextAndThrowBtn.click();
assertValueAndErrors(compEl, 5, 8); assertValueAndErrors(compEl, 3, currentErrorIndex);
nextAndThrowDirBtn.click(); currentErrorIndex += errorDelta;
assertValueAndErrors(compEl, 6, 10); nextAndThrowBtn.click();
assertValueAndErrors(compEl, 4, currentErrorIndex);
currentErrorIndex += errorDelta;
// Assert that there were no more errors nextAndThrowDirBtn.click();
expect(logger.errors.length).toBe(12); assertValueAndErrors(compEl, 5, currentErrorIndex);
done(); currentErrorIndex += errorDelta;
}); nextAndThrowDirBtn.click();
assertValueAndErrors(compEl, 6, currentErrorIndex);
currentErrorIndex += errorDelta;
function assertValueAndErrors(compEl: any, value: number, errorIndex: number) { // Assert that there were no more errors
expect(compEl).toHaveText(`Value:${value}`); expect(logger.errors.length).toBe(currentErrorIndex);
expect(logger.errors[errorIndex][0]).toBe('ERROR'); done();
expect(logger.errors[errorIndex][1].message).toBe(`Error: ${value}`); });
expect(logger.errors[errorIndex + 1][0]).toBe('ERROR CONTEXT');
} function assertValueAndErrors(compEl: any, value: number, errorIndex: number) {
}); expect(compEl).toHaveText(`Value:${value}`);
expect(logger.errors[errorIndex][0]).toBe('ERROR');
expect(logger.errors[errorIndex][1].message).toBe(`Error: ${value}`);
// Ivy doesn't attach an error context.
!ivyEnabled && expect(logger.errors[errorIndex + 1][0]).toBe('ERROR CONTEXT');
}
});
} }
}); });
} }