From e263e19a2ae8f7dc6457e5cefb4d27887c111f96 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 28 Apr 2017 11:50:45 -0700 Subject: [PATCH] =?UTF-8?q?fix(core):=20don=E2=80=99t=20stop=20change=20de?= =?UTF-8?q?tection=20because=20of=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - prevents unsubscribing from the zone on error - prevents unsubscribing from directive `EventEmitter`s on error - prevents detaching views in dev mode if there on error - ensures that `ngOnInit` is only called 1x (also in prod mode) Fixes #9531 Fixes #2413 Fixes #15925 --- modules/benchmarks/src/tree/ng2_next/tree.ts | 3 +- packages/core/src/application_ref.ts | 3 + packages/core/src/view/element.ts | 9 +- packages/core/src/view/provider.ts | 9 +- packages/core/src/view/services.ts | 5 +- packages/core/src/view/types.ts | 11 +- packages/core/src/view/util.ts | 4 +- packages/core/src/view/view.ts | 15 ++- packages/core/test/application_ref_spec.ts | 41 +++--- .../change_detection_integration_spec.ts | 4 +- packages/core/test/linker/integration_spec.ts | 24 ++-- .../linker/regression_integration_spec.ts | 118 +++++++++++++++++- .../source_map_integration_node_only_spec.ts | 11 +- .../core/test/view/component_view_spec.ts | 6 +- packages/core/test/view/element_spec.ts | 12 +- packages/core/test/view/provider_spec.ts | 11 +- 16 files changed, 218 insertions(+), 68 deletions(-) diff --git a/modules/benchmarks/src/tree/ng2_next/tree.ts b/modules/benchmarks/src/tree/ng2_next/tree.ts index 95bdc911bf..573b8fec24 100644 --- a/modules/benchmarks/src/tree/ng2_next/tree.ts +++ b/modules/benchmarks/src/tree/ng2_next/tree.ts @@ -7,7 +7,7 @@ */ import {NgIf} from '@angular/common'; -import {ComponentFactory, ComponentFactoryResolver, ComponentRef, Injector, NgModuleRef, RendererFactory2, RootRenderer, Sanitizer, TemplateRef, ViewContainerRef} from '@angular/core'; +import {ComponentFactory, ComponentFactoryResolver, ComponentRef, ErrorHandler, Injector, NgModuleRef, RendererFactory2, RootRenderer, Sanitizer, TemplateRef, ViewContainerRef} from '@angular/core'; import {ArgumentType, BindingFlags, NodeFlags, ViewDefinition, ViewFlags, anchorDef, createComponentFactory, directiveDef, elementDef, initServicesIfNeeded, textDef, viewDef} from '@angular/core/src/view/index'; import {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer'; import {DomSanitizerImpl, SafeStyle} from '@angular/platform-browser/src/security/dom_sanitization_service'; @@ -108,6 +108,7 @@ export class AppModule implements Injector, NgModuleRef { case Sanitizer: return this.sanitizer; case RootRenderer: + case ErrorHandler: return null; case NgModuleRef: return this; diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index 0d3b395547..321125c864 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -560,6 +560,9 @@ export class ApplicationRef_ extends ApplicationRef { if (this._enforceNoNewChanges) { this._views.forEach((view) => view.checkNoChanges()); } + } catch (e) { + // Attention: Don't rethrow as it could cancel subscriptions to Observables! + this._exceptionHandler.handleError(e); } finally { this._runningTick = false; wtfLeave(scope); diff --git a/packages/core/src/view/element.ts b/packages/core/src/view/element.ts index a3242fea4f..f2bf0199e4 100644 --- a/packages/core/src/view/element.ts +++ b/packages/core/src/view/element.ts @@ -189,7 +189,14 @@ export function listenToElementOutputs(view: ViewData, compView: ViewData, def: } function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) { - return (event: any) => dispatchEvent(view, index, eventName, event); + return (event: any) => { + try { + return dispatchEvent(view, index, eventName, event); + } catch (e) { + // Attention: Don't rethrow, to keep in sync with directive events. + view.root.errorHandler.handleError(e); + } + } } diff --git a/packages/core/src/view/provider.ts b/packages/core/src/view/provider.ts index 87c4b25df1..55236fbd09 100644 --- a/packages/core/src/view/provider.ts +++ b/packages/core/src/view/provider.ts @@ -148,7 +148,14 @@ export function createDirectiveInstance(view: ViewData, def: NodeDef): any { } function eventHandlerClosure(view: ViewData, index: number, eventName: string) { - return (event: any) => dispatchEvent(view, index, eventName, event); + return (event: any) => { + try { + return dispatchEvent(view, index, eventName, event); + } catch (e) { + // Attention: Don't rethrow, as it would cancel Observable subscriptions! + view.root.errorHandler.handleError(e); + } + } } export function checkAndUpdateDirectiveInline( diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index 5783fc258c..edfa53b357 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -9,6 +9,7 @@ import {isDevMode} from '../application_ref'; import {DebugElement, DebugNode, EventListener, getDebugNode, indexDebugNode, removeDebugNodeFromIndex} from '../debug/debug_node'; import {Injector} from '../di'; +import {ErrorHandler} from '../error_handler'; import {NgModuleRef} from '../linker/ng_module_factory'; import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api'; import {Sanitizer} from '../security'; @@ -104,11 +105,12 @@ function createRootData( elInjector: Injector, ngModule: NgModuleRef, rendererFactory: RendererFactory2, projectableNodes: any[][], rootSelectorOrNode: any): RootData { const sanitizer = ngModule.injector.get(Sanitizer); + const errorHandler = ngModule.injector.get(ErrorHandler); const renderer = rendererFactory.createRenderer(null, null); return { ngModule, injector: elInjector, projectableNodes, - selectorOrNode: rootSelectorOrNode, sanitizer, rendererFactory, renderer + selectorOrNode: rootSelectorOrNode, sanitizer, rendererFactory, renderer, errorHandler }; } @@ -439,7 +441,6 @@ function callWithDebugContext(action: DebugAction, fn: any, self: any, args: any if (isViewDebugError(e) || !_currentView) { throw e; } - _currentView.state |= ViewState.Errored; throw viewWrappedDebugError(e, getCurrentDebugContext() !); } } diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index 104f3c7c9d..67a985081e 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -7,6 +7,7 @@ */ import {Injector} from '../di'; +import {ErrorHandler} from '../error_handler'; import {NgModuleRef} from '../linker/ng_module_factory'; import {QueryList} from '../linker/query_list'; import {TemplateRef} from '../linker/template_ref'; @@ -323,13 +324,14 @@ export interface ViewData { * Bitmask of states */ export const enum ViewState { - FirstCheck = 1 << 0, - Attached = 1 << 1, - ChecksEnabled = 1 << 2, - Errored = 1 << 3, + BeforeFirstCheck = 1 << 0, + FirstCheck = 1 << 1, + Attached = 1 << 2, + ChecksEnabled = 1 << 3, Destroyed = 1 << 4, CatDetectChanges = Attached | ChecksEnabled, + CatInit = BeforeFirstCheck | CatDetectChanges } export interface DisposableFn { (): void; } @@ -432,6 +434,7 @@ export interface RootData { selectorOrNode: any; renderer: Renderer2; rendererFactory: RendererFactory2; + errorHandler: ErrorHandler; sanitizer: Sanitizer; } diff --git a/packages/core/src/view/util.ts b/packages/core/src/view/util.ts index fb6042d34b..c6fd9b333b 100644 --- a/packages/core/src/view/util.ts +++ b/packages/core/src/view/util.ts @@ -100,10 +100,10 @@ export function checkAndUpdateBinding( export function checkBindingNoChanges( view: ViewData, def: NodeDef, bindingIdx: number, value: any) { const oldValue = view.oldValues[def.bindingIndex + bindingIdx]; - if ((view.state & ViewState.FirstCheck) || !devModeEqual(oldValue, value)) { + if ((view.state & ViewState.BeforeFirstCheck) || !devModeEqual(oldValue, value)) { throw expressionChangedAfterItHasBeenCheckedError( Services.createDebugContext(view, def.index), oldValue, value, - (view.state & ViewState.FirstCheck) !== 0); + (view.state & ViewState.BeforeFirstCheck) !== 0); } } diff --git a/packages/core/src/view/view.ts b/packages/core/src/view/view.ts index 44f36d6190..da82ed6a6c 100644 --- a/packages/core/src/view/view.ts +++ b/packages/core/src/view/view.ts @@ -211,7 +211,7 @@ function createView( viewContainerParent: null, parentNodeDef, context: null, component: null, nodes, - state: ViewState.FirstCheck | ViewState.CatDetectChanges, root, renderer, + state: ViewState.CatInit, root, renderer, oldValues: new Array(def.bindingCount), disposables }; return view; @@ -323,6 +323,12 @@ export function checkNoChangesView(view: ViewData) { } export function checkAndUpdateView(view: ViewData) { + if (view.state & ViewState.BeforeFirstCheck) { + view.state &= ~ViewState.BeforeFirstCheck; + view.state |= ViewState.FirstCheck; + } else { + view.state &= ~ViewState.FirstCheck; + } Services.updateDirectives(view, CheckType.CheckAndUpdate); execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate); execQueriesAction( @@ -345,7 +351,6 @@ export function checkAndUpdateView(view: ViewData) { if (view.def.flags & ViewFlags.OnPush) { view.state &= ~ViewState.ChecksEnabled; } - view.state &= ~ViewState.FirstCheck; } export function checkAndUpdateNode( @@ -453,7 +458,7 @@ function checkNoChangesQuery(view: ViewData, nodeDef: NodeDef) { if (queryList.dirty) { throw expressionChangedAfterItHasBeenCheckedError( Services.createDebugContext(view, nodeDef.index), `Query ${nodeDef.query!.id} not dirty`, - `Query ${nodeDef.query!.id} dirty`, (view.state & ViewState.FirstCheck) !== 0); + `Query ${nodeDef.query!.id} dirty`, (view.state & ViewState.BeforeFirstCheck) !== 0); } } @@ -543,13 +548,13 @@ function callViewAction(view: ViewData, action: ViewAction) { switch (action) { case ViewAction.CheckNoChanges: if ((viewState & ViewState.CatDetectChanges) === ViewState.CatDetectChanges && - (viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) { + (viewState & ViewState.Destroyed) === 0) { checkNoChangesView(view); } break; case ViewAction.CheckAndUpdate: if ((viewState & ViewState.CatDetectChanges) === ViewState.CatDetectChanges && - (viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) { + (viewState & ViewState.Destroyed) === 0) { checkAndUpdateView(view); } break; diff --git a/packages/core/test/application_ref_spec.ts b/packages/core/test/application_ref_spec.ts index b3e3c1009e..a3f49184a7 100644 --- a/packages/core/test/application_ref_spec.ts +++ b/packages/core/test/application_ref_spec.ts @@ -131,20 +131,33 @@ export function main() { describe('ApplicationRef', () => { beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); }); - it('should throw when reentering tick', inject([ApplicationRef], (ref: ApplicationRef_) => { - const view = jasmine.createSpyObj('view', ['detach', 'attachToAppRef']); - const viewRef = jasmine.createSpyObj( - 'viewRef', ['detectChanges', 'detachFromAppRef', 'attachToAppRef']); - viewRef.internalView = view; - view.ref = viewRef; - try { - ref.attachView(viewRef); - viewRef.detectChanges.and.callFake(() => ref.tick()); - expect(() => ref.tick()).toThrowError('ApplicationRef.tick is called recursively'); - } finally { - ref.detachView(viewRef); - } - })); + it('should throw when reentering tick', () => { + @Component({template: '{{reenter()}}'}) + class ReenteringComponent { + reenterCount = 1; + reenterErr: any; + + constructor(private appRef: ApplicationRef) {} + + reenter() { + if (this.reenterCount--) { + try { + this.appRef.tick(); + } catch (e) { + this.reenterErr = e; + } + } + } + } + + const fixture = TestBed.configureTestingModule({declarations: [ReenteringComponent]}) + .createComponent(ReenteringComponent); + const appRef = TestBed.get(ApplicationRef) as ApplicationRef; + appRef.attachView(fixture.componentRef.hostView); + appRef.tick(); + expect(fixture.componentInstance.reenterErr.message) + .toBe('ApplicationRef.tick is called recursively'); + }); describe('APP_BOOTSTRAP_LISTENER', () => { let capturedCompRefs: ComponentRef[]; diff --git a/packages/core/test/linker/change_detection_integration_spec.ts b/packages/core/test/linker/change_detection_integration_spec.ts index fe6444071c..3df8f8df53 100644 --- a/packages/core/test/linker/change_detection_integration_spec.ts +++ b/packages/core/test/linker/change_detection_integration_spec.ts @@ -765,6 +765,7 @@ export function main() { try { ctx.detectChanges(false); } catch (e) { + expect(e.message).toBe('Boom!'); errored = true; } expect(errored).toBe(true); @@ -776,7 +777,8 @@ export function main() { try { ctx.detectChanges(false); } catch (e) { - throw new Error('Second detectChanges() should not have run detection.'); + expect(e.message).toBe('Boom!'); + throw new Error('Second detectChanges() should not have called ngOnInit.'); } expect(directiveLog.filter(['ngOnInit'])).toEqual([]); })); diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 5ad4966b07..1c2c3e47f2 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {Compiler, ComponentFactory, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleRef, OnDestroy, ReflectiveInjector, SkipSelf} from '@angular/core'; +import {Compiler, ComponentFactory, ErrorHandler, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleRef, OnDestroy, ReflectiveInjector, SkipSelf} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection'; import {getDebugContext} from '@angular/core/src/errors'; import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver'; @@ -1480,16 +1480,18 @@ function declareTests({useJit}: {useJit: boolean}) { const tc = fixture.debugElement.children[0]; - try { - tc.injector.get(DirectiveEmittingEvent).fireEvent('boom'); - } catch (e) { - const c = getDebugContext(e); - expect(getDOM().nodeName(c.renderNode).toUpperCase()).toEqual('SPAN'); - expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV'); - expect((c.injector).get).toBeTruthy(); - expect(c.context).toBe(fixture.componentInstance); - expect(c.references['local']).toBeDefined(); - } + const errorHandler = tc.injector.get(ErrorHandler); + let err: any; + spyOn(errorHandler, 'handleError').and.callFake((e: any) => err = e); + tc.injector.get(DirectiveEmittingEvent).fireEvent('boom'); + + expect(err).toBeTruthy(); + const c = getDebugContext(err); + expect(getDOM().nodeName(c.renderNode).toUpperCase()).toEqual('SPAN'); + expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV'); + expect((c.injector).get).toBeTruthy(); + expect(c.context).toBe(fixture.componentInstance); + expect(c.references['local']).toBeDefined(); })); } }); diff --git a/packages/core/test/linker/regression_integration_spec.ts b/packages/core/test/linker/regression_integration_spec.ts index 7a65d3f7f2..1f844ffb8b 100644 --- a/packages/core/test/linker/regression_integration_spec.ts +++ b/packages/core/test/linker/regression_integration_spec.ts @@ -6,9 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, ContentChild, Directive, InjectionToken, Injector, Input, NgModule, NgModuleRef, Pipe, PipeTransform, Provider, QueryList, Renderer2, SimpleChanges, TemplateRef, ViewChildren, ViewContainerRef} from '@angular/core'; -import {TestBed, fakeAsync, tick} from '@angular/core/testing'; -import {By} from '@angular/platform-browser'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, ApplicationRef, Component, ComponentRef, ContentChild, Directive, ErrorHandler, EventEmitter, HostListener, InjectionToken, Injector, Input, NgModule, NgModuleRef, NgZone, Output, Pipe, PipeTransform, Provider, QueryList, Renderer2, SimpleChanges, TemplateRef, ViewChildren, ViewContainerRef, destroyPlatform} from '@angular/core'; +import {TestBed, async, fakeAsync, inject, tick} from '@angular/core/testing'; +import {BrowserModule, By, DOCUMENT} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -16,6 +17,8 @@ export function main() { describe('jit', () => { declareTests({useJit: true}); }); describe('no jit', () => { declareTests({useJit: false}); }); + + declareTestsUsingBootstrap(); } function declareTests({useJit}: {useJit: boolean}) { @@ -386,6 +389,115 @@ function declareTests({useJit}: {useJit: boolean}) { }); } +function declareTestsUsingBootstrap() { + // Place to put reproductions for regressions + describe('regressions using bootstrap', () => { + const COMP_SELECTOR = 'root-comp'; + + class MockConsole { + errors: any[][] = []; + error(...s: any[]): void { this.errors.push(s); } + } + + let logger: MockConsole; + let errorHandler: ErrorHandler; + + beforeEach(inject([DOCUMENT], (doc: any) => { + destroyPlatform(); + const el = getDOM().createElement(COMP_SELECTOR, doc); + getDOM().appendChild(doc.body, el); + + logger = new MockConsole(); + errorHandler = new ErrorHandler(); + errorHandler._console = logger as any; + })); + + afterEach(() => { destroyPlatform(); }); + + if (getDOM().supportsDOMEvents()) { + // This test needs a real DOM.... + + it('should keep change detecting if there was an error', (done) => { + @Component({ + selector: COMP_SELECTOR, + template: + 'Value:{{value}}{{throwIfNeeded()}}' + }) + class ErrorComp { + value = 0; + thrownValue = 0; + next() { this.value++; } + nextAndThrow() { + this.value++; + this.throwIfNeeded(); + } + throwIfNeeded() { + NgZone.assertInAngularZone(); + if (this.thrownValue !== this.value) { + this.thrownValue = this.value; + throw new Error(`Error: ${this.value}`); + } + } + } + + @Directive({selector: '[dirClick]'}) + class EventDir { + @Output() + dirClick = new EventEmitter(); + + @HostListener('click', ['$event']) + onClick(event: any) { this.dirClick.next(event); } + } + + @NgModule({ + imports: [BrowserModule], + declarations: [ErrorComp, EventDir], + bootstrap: [ErrorComp], + providers: [{provide: ErrorHandler, useValue: errorHandler}], + }) + class TestModule { + } + + platformBrowserDynamic().bootstrapModule(TestModule).then((ref) => { + NgZone.assertNotInAngularZone(); + const appRef = ref.injector.get(ApplicationRef) as ApplicationRef; + const compRef = appRef.components[0] as ComponentRef; + const compEl = compRef.location.nativeElement; + const nextBtn = compEl.children[0]; + const nextAndThrowBtn = compEl.children[1]; + const nextAndThrowDirBtn = compEl.children[2]; + + nextBtn.click(); + assertValueAndErrors(compEl, 1, 0); + nextBtn.click(); + assertValueAndErrors(compEl, 2, 2); + + nextAndThrowBtn.click(); + assertValueAndErrors(compEl, 3, 4); + nextAndThrowBtn.click(); + assertValueAndErrors(compEl, 4, 6); + + nextAndThrowDirBtn.click(); + assertValueAndErrors(compEl, 5, 8); + nextAndThrowDirBtn.click(); + assertValueAndErrors(compEl, 6, 10); + + // Assert that there were no more errors + expect(logger.errors.length).toBe(12); + done(); + }); + + 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}`); + expect(logger.errors[errorIndex + 1][0]).toBe('ERROR CONTEXT'); + } + }); + } + }); +} + @Component({selector: 'my-comp', template: ''}) class MyComp1 { constructor(public injector: Injector) {} diff --git a/packages/core/test/linker/source_map_integration_node_only_spec.ts b/packages/core/test/linker/source_map_integration_node_only_spec.ts index 2c59f949cc..cf29d1bfff 100644 --- a/packages/core/test/linker/source_map_integration_node_only_spec.ts +++ b/packages/core/test/linker/source_map_integration_node_only_spec.ts @@ -10,7 +10,7 @@ import {ResourceLoader} from '@angular/compiler'; import {SourceMap} from '@angular/compiler/src/output/source_map'; import {extractSourceMap, originalPositionFor} from '@angular/compiler/test/output/source_map_util'; import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock'; -import {Attribute, Component, Directive, ɵglobal} from '@angular/core'; +import {Attribute, Component, Directive, ErrorHandler, ɵglobal} from '@angular/core'; import {getErrorLogger} from '@angular/core/src/errors'; import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; @@ -231,11 +231,10 @@ export function main() { const comp = compileAndCreateComponent(MyComp); let error: any; - try { - comp.debugElement.children[0].children[0].triggerEventHandler('click', 'EVENT'); - } catch (e) { - error = e; - } + const errorHandler = TestBed.get(ErrorHandler); + spyOn(errorHandler, 'handleError').and.callFake((e: any) => error = e); + comp.debugElement.children[0].children[0].triggerEventHandler('click', 'EVENT'); + expect(error).toBeTruthy(); // the stack should point to the binding expect(getSourcePositionForStack(error.stack)).toEqual({ line: 2, diff --git a/packages/core/test/view/component_view_spec.ts b/packages/core/test/view/component_view_spec.ts index f1b680f83c..07eed2abab 100644 --- a/packages/core/test/view/component_view_spec.ts +++ b/packages/core/test/view/component_view_spec.ts @@ -230,7 +230,7 @@ export function main() { }); } - it('should stop dirty checking views that threw errors in change detection', () => { + it('should not stop dirty checking views that threw errors in change detection', () => { class AComp { a: any; } @@ -255,8 +255,8 @@ export function main() { expect(update).toHaveBeenCalled(); update.calls.reset(); - Services.checkAndUpdateView(view); - expect(update).not.toHaveBeenCalled(); + expect(() => Services.checkAndUpdateView(view)).toThrowError('Test'); + expect(update).toHaveBeenCalled(); }); }); diff --git a/packages/core/test/view/element_spec.ts b/packages/core/test/view/element_spec.ts index b6fa0a13b0..ca3b12b65e 100644 --- a/packages/core/test/view/element_spec.ts +++ b/packages/core/test/view/element_spec.ts @@ -6,9 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core'; +import {ErrorHandler, Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core'; import {getDebugContext} from '@angular/core/src/errors'; import {ArgumentType, BindingFlags, DebugContext, NodeDef, NodeFlags, OutputType, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {TestBed} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, isBrowser, removeNodes} from './helper'; @@ -282,17 +283,14 @@ export function main() { }); it('should report debug info on event errors', () => { + const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError'); const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough(); const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef( NodeFlags.None, null !, null !, 0, 'button', null !, null !, [[null !, 'click']], () => { throw new Error('Test'); })])); - let err: any; - try { - addListenerSpy.calls.mostRecent().args[1]('SomeEvent'); - } catch (e) { - err = e; - } + addListenerSpy.calls.mostRecent().args[1]('SomeEvent'); + const err = handleErrorSpy.calls.mostRecent().args[0]; expect(err).toBeTruthy(); expect(err.message).toBe('Test'); const debugCtx = getDebugContext(err); diff --git a/packages/core/test/view/provider_spec.ts b/packages/core/test/view/provider_spec.ts index 0fa98275da..02c1be11a1 100644 --- a/packages/core/test/view/provider_spec.ts +++ b/packages/core/test/view/provider_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectorRef, DoCheck, ElementRef, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, Renderer2, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core'; +import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectorRef, DoCheck, ElementRef, ErrorHandler, EventEmitter, Injector, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, Renderer2, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core'; import {getDebugContext} from '@angular/core/src/errors'; import {ArgumentType, BindingFlags, DebugContext, DepFlags, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, directiveDef, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; import {TestBed, inject, withModule} from '@angular/core/testing'; @@ -381,6 +381,7 @@ export function main() { }); it('should report debug info on event errors', () => { + const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError'); let emitter = new EventEmitter(); class SomeService { @@ -395,12 +396,8 @@ export function main() { NodeFlags.None, null !, 0, SomeService, [], null !, {emitter: 'someEventName'}) ])); - let err: any; - try { - emitter.emit('someEventInstance'); - } catch (e) { - err = e; - } + emitter.emit('someEventInstance'); + const err = handleErrorSpy.calls.mostRecent().args[0]; expect(err).toBeTruthy(); const debugCtx = getDebugContext(err); expect(debugCtx.view).toBe(view);