ErrorHandler can not throw errors because it will unsubscribe itself from the error stream. Zones captures errors and feed it into NgZone, which than has a Rx Observable to feed it into ErrorHandler. If the ErroHandler throws, then Rx will teardown the observable which in essence causes the ErrorHandler to be removed from the error handling. This implies that the ErrorHandler can never throw errors. Closes #14949 Closes #15182 Closes #14316
543 lines
21 KiB
TypeScript
543 lines
21 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 {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
|
|
import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref';
|
|
import {ErrorHandler} from '@angular/core/src/error_handler';
|
|
import {ComponentRef} from '@angular/core/src/linker/component_factory';
|
|
import {TestComponentRenderer} from '@angular/core/testing';
|
|
import {BrowserModule} from '@angular/platform-browser';
|
|
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
|
import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
|
|
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
|
|
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
|
import {ServerModule} from '@angular/platform-server';
|
|
|
|
import {ComponentFixture, ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
|
|
|
|
@Component({selector: 'bootstrap-app', template: 'hello'})
|
|
class SomeComponent {
|
|
}
|
|
|
|
export function main() {
|
|
describe('bootstrap', () => {
|
|
let mockConsole: MockConsole;
|
|
|
|
beforeEach(() => { mockConsole = new MockConsole(); });
|
|
|
|
function createRootEl() {
|
|
const doc = TestBed.get(DOCUMENT);
|
|
const rootEl = <HTMLElement>getDOM().firstChild(
|
|
getDOM().content(getDOM().createTemplate(`<bootstrap-app></bootstrap-app>`)));
|
|
const oldRoots = getDOM().querySelectorAll(doc, 'bootstrap-app');
|
|
for (let i = 0; i < oldRoots.length; i++) {
|
|
getDOM().remove(oldRoots[i]);
|
|
}
|
|
getDOM().appendChild(doc.body, rootEl);
|
|
}
|
|
|
|
type CreateModuleOptions = {providers?: any[], ngDoBootstrap?: any, bootstrap?: any[]};
|
|
|
|
function createModule(providers?: any[]): Type<any>;
|
|
function createModule(options: CreateModuleOptions): Type<any>;
|
|
function createModule(providersOrOptions: any[] | CreateModuleOptions): Type<any> {
|
|
let options: CreateModuleOptions = {};
|
|
if (providersOrOptions instanceof Array) {
|
|
options = {providers: providersOrOptions};
|
|
} else {
|
|
options = providersOrOptions || {};
|
|
}
|
|
const errorHandler = new ErrorHandler();
|
|
errorHandler._console = mockConsole as any;
|
|
|
|
const platformModule = getDOM().supportsDOMEvents() ? BrowserModule : ServerModule;
|
|
|
|
@NgModule({
|
|
providers: [{provide: ErrorHandler, useValue: errorHandler}, options.providers || []],
|
|
imports: [platformModule],
|
|
declarations: [SomeComponent],
|
|
entryComponents: [SomeComponent],
|
|
bootstrap: options.bootstrap || []
|
|
})
|
|
class MyModule {
|
|
}
|
|
if (options.ngDoBootstrap !== false) {
|
|
(<any>MyModule.prototype).ngDoBootstrap = options.ngDoBootstrap || (() => {});
|
|
}
|
|
return MyModule;
|
|
}
|
|
|
|
it('should bootstrap a component from a child module',
|
|
async(inject([ApplicationRef, Compiler], (app: ApplicationRef, compiler: Compiler) => {
|
|
@Component({
|
|
selector: 'bootstrap-app',
|
|
template: '',
|
|
})
|
|
class SomeComponent {
|
|
}
|
|
|
|
@NgModule({
|
|
providers: [{provide: 'hello', useValue: 'component'}],
|
|
declarations: [SomeComponent],
|
|
entryComponents: [SomeComponent],
|
|
})
|
|
class SomeModule {
|
|
}
|
|
|
|
createRootEl();
|
|
const modFactory = compiler.compileModuleSync(SomeModule);
|
|
const module = modFactory.create(TestBed);
|
|
const cmpFactory = module.componentFactoryResolver.resolveComponentFactory(SomeComponent);
|
|
const component = app.bootstrap(cmpFactory);
|
|
|
|
// The component should see the child module providers
|
|
expect(component.injector.get('hello')).toEqual('component');
|
|
})));
|
|
|
|
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);
|
|
}
|
|
}));
|
|
|
|
describe('APP_BOOTSTRAP_LISTENER', () => {
|
|
let capturedCompRefs: ComponentRef<any>[];
|
|
beforeEach(() => {
|
|
capturedCompRefs = [];
|
|
TestBed.configureTestingModule({
|
|
providers: [{
|
|
provide: APP_BOOTSTRAP_LISTENER,
|
|
multi: true,
|
|
useValue: (compRef: any) => { capturedCompRefs.push(compRef); }
|
|
}]
|
|
});
|
|
});
|
|
|
|
it('should be called when a component is bootstrapped',
|
|
inject([ApplicationRef], (ref: ApplicationRef_) => {
|
|
createRootEl();
|
|
const compRef = ref.bootstrap(SomeComponent);
|
|
expect(capturedCompRefs).toEqual([compRef]);
|
|
}));
|
|
});
|
|
|
|
describe('bootstrap', () => {
|
|
it('should throw if an APP_INITIIALIZER is not yet resolved',
|
|
withModule(
|
|
{
|
|
providers: [
|
|
{provide: APP_INITIALIZER, useValue: () => new Promise(() => {}), multi: true}
|
|
]
|
|
},
|
|
inject([ApplicationRef], (ref: ApplicationRef_) => {
|
|
createRootEl();
|
|
expect(() => ref.bootstrap(SomeComponent))
|
|
.toThrowError(
|
|
'Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.');
|
|
})));
|
|
});
|
|
});
|
|
|
|
describe('bootstrapModule', () => {
|
|
let defaultPlatform: PlatformRef;
|
|
beforeEach(inject([PlatformRef], (_platform: PlatformRef) => {
|
|
createRootEl();
|
|
defaultPlatform = _platform;
|
|
}));
|
|
|
|
it('should wait for asynchronous app initializers', async(() => {
|
|
let resolve: (result: any) => void;
|
|
const promise: Promise<any> = new Promise((res) => { resolve = res; });
|
|
let initializerDone = false;
|
|
setTimeout(() => {
|
|
resolve(true);
|
|
initializerDone = true;
|
|
}, 1);
|
|
|
|
defaultPlatform
|
|
.bootstrapModule(
|
|
createModule([{provide: APP_INITIALIZER, useValue: () => promise, multi: true}]))
|
|
.then(_ => { expect(initializerDone).toBe(true); });
|
|
}));
|
|
|
|
it('should rethrow sync errors even if the exceptionHandler is not rethrowing', async(() => {
|
|
defaultPlatform
|
|
.bootstrapModule(createModule(
|
|
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]))
|
|
.then(() => expect(false).toBe(true), (e) => {
|
|
expect(e).toBe('Test');
|
|
// Note: if the modules throws an error during construction,
|
|
// we don't have an injector and therefore no way of
|
|
// getting the exception handler. So
|
|
// the error is only rethrown but not logged via the exception handler.
|
|
expect(mockConsole.res).toEqual([]);
|
|
});
|
|
}));
|
|
|
|
it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
|
|
async(() => {
|
|
defaultPlatform
|
|
.bootstrapModule(createModule([
|
|
{provide: APP_INITIALIZER, useValue: () => Promise.reject('Test'), multi: true}
|
|
]))
|
|
.then(() => expect(false).toBe(true), (e) => {
|
|
expect(e).toBe('Test');
|
|
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
|
|
});
|
|
}));
|
|
|
|
it('should throw useful error when ApplicationRef is not configured', async(() => {
|
|
@NgModule()
|
|
class EmptyModule {
|
|
}
|
|
|
|
return defaultPlatform.bootstrapModule(EmptyModule)
|
|
.then(() => fail('expecting error'), (error) => {
|
|
expect(error.message)
|
|
.toEqual('No ErrorHandler. Is platform module (BrowserModule) included?');
|
|
});
|
|
}));
|
|
|
|
it('should call the `ngDoBootstrap` method with `ApplicationRef` on the main module',
|
|
async(() => {
|
|
const ngDoBootstrap = jasmine.createSpy('ngDoBootstrap');
|
|
defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: ngDoBootstrap}))
|
|
.then((moduleRef) => {
|
|
const appRef = moduleRef.injector.get(ApplicationRef);
|
|
expect(ngDoBootstrap).toHaveBeenCalledWith(appRef);
|
|
});
|
|
}));
|
|
|
|
it('should auto bootstrap components listed in @NgModule.bootstrap', async(() => {
|
|
defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
|
|
.then((moduleRef) => {
|
|
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
|
|
expect(appRef.componentTypes).toEqual([SomeComponent]);
|
|
});
|
|
}));
|
|
|
|
it('should error if neither `ngDoBootstrap` nor @NgModule.bootstrap was specified',
|
|
async(() => {
|
|
defaultPlatform.bootstrapModule(createModule({ngDoBootstrap: false}))
|
|
.then(() => expect(false).toBe(true), (e) => {
|
|
const expectedErrMsg =
|
|
`The module MyModule was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. Please define one of these.`;
|
|
expect(e.message).toEqual(expectedErrMsg);
|
|
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Error: ' + expectedErrMsg);
|
|
});
|
|
}));
|
|
|
|
it('should add bootstrapped module into platform modules list', async(() => {
|
|
defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
|
|
.then(module => expect((<any>defaultPlatform)._modules).toContain(module));
|
|
}));
|
|
});
|
|
|
|
describe('bootstrapModuleFactory', () => {
|
|
let defaultPlatform: PlatformRef;
|
|
beforeEach(inject([PlatformRef], (_platform: PlatformRef) => {
|
|
createRootEl();
|
|
defaultPlatform = _platform;
|
|
}));
|
|
it('should wait for asynchronous app initializers', async(() => {
|
|
let resolve: (result: any) => void;
|
|
const promise: Promise<any> = new Promise((res) => { resolve = res; });
|
|
let initializerDone = false;
|
|
setTimeout(() => {
|
|
resolve(true);
|
|
initializerDone = true;
|
|
}, 1);
|
|
|
|
const compilerFactory: CompilerFactory =
|
|
defaultPlatform.injector.get(CompilerFactory, null);
|
|
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(
|
|
createModule([{provide: APP_INITIALIZER, useValue: () => promise, multi: true}]));
|
|
defaultPlatform.bootstrapModuleFactory(moduleFactory).then(_ => {
|
|
expect(initializerDone).toBe(true);
|
|
});
|
|
}));
|
|
|
|
it('should rethrow sync errors even if the exceptionHandler is not rethrowing', async(() => {
|
|
const compilerFactory: CompilerFactory =
|
|
defaultPlatform.injector.get(CompilerFactory, null);
|
|
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
|
|
[{provide: APP_INITIALIZER, useValue: () => { throw 'Test'; }, multi: true}]));
|
|
expect(() => defaultPlatform.bootstrapModuleFactory(moduleFactory)).toThrow('Test');
|
|
// Note: if the modules throws an error during construction,
|
|
// we don't have an injector and therefore no way of
|
|
// getting the exception handler. So
|
|
// the error is only rethrown but not logged via the exception handler.
|
|
expect(mockConsole.res).toEqual([]);
|
|
}));
|
|
|
|
it('should rethrow promise errors even if the exceptionHandler is not rethrowing',
|
|
async(() => {
|
|
const compilerFactory: CompilerFactory =
|
|
defaultPlatform.injector.get(CompilerFactory, null);
|
|
const moduleFactory = compilerFactory.createCompiler().compileModuleSync(createModule(
|
|
[{provide: APP_INITIALIZER, useValue: () => Promise.reject('Test'), multi: true}]));
|
|
defaultPlatform.bootstrapModuleFactory(moduleFactory)
|
|
.then(() => expect(false).toBe(true), (e) => {
|
|
expect(e).toBe('Test');
|
|
expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test');
|
|
});
|
|
}));
|
|
});
|
|
|
|
describe('attachView / detachView', () => {
|
|
@Component({template: '{{name}}'})
|
|
class MyComp {
|
|
name = 'Initial';
|
|
}
|
|
|
|
@Component({template: '<ng-container #vc></ng-container>'})
|
|
class ContainerComp {
|
|
@ViewChild('vc', {read: ViewContainerRef})
|
|
vc: ViewContainerRef;
|
|
}
|
|
|
|
@Component({template: '<ng-template #t>Dynamic content</ng-template>'})
|
|
class EmbeddedViewComp {
|
|
@ViewChild(TemplateRef)
|
|
tplRef: TemplateRef<Object>;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [MyComp, ContainerComp, EmbeddedViewComp],
|
|
providers: [{provide: ComponentFixtureNoNgZone, useValue: true}]
|
|
});
|
|
});
|
|
|
|
it('should dirty check attached views', () => {
|
|
const comp = TestBed.createComponent(MyComp);
|
|
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
|
|
expect(appRef.viewCount).toBe(0);
|
|
|
|
appRef.tick();
|
|
expect(comp.nativeElement).toHaveText('');
|
|
|
|
appRef.attachView(comp.componentRef.hostView);
|
|
appRef.tick();
|
|
expect(appRef.viewCount).toBe(1);
|
|
expect(comp.nativeElement).toHaveText('Initial');
|
|
});
|
|
|
|
it('should not dirty check detached views', () => {
|
|
const comp = TestBed.createComponent(MyComp);
|
|
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
|
|
|
|
appRef.attachView(comp.componentRef.hostView);
|
|
appRef.tick();
|
|
expect(comp.nativeElement).toHaveText('Initial');
|
|
|
|
appRef.detachView(comp.componentRef.hostView);
|
|
comp.componentInstance.name = 'New';
|
|
appRef.tick();
|
|
expect(appRef.viewCount).toBe(0);
|
|
expect(comp.nativeElement).toHaveText('Initial');
|
|
});
|
|
|
|
it('should detach attached views if they are destroyed', () => {
|
|
const comp = TestBed.createComponent(MyComp);
|
|
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
|
|
|
|
appRef.attachView(comp.componentRef.hostView);
|
|
comp.destroy();
|
|
|
|
expect(appRef.viewCount).toBe(0);
|
|
});
|
|
|
|
it('should detach attached embedded views if they are destroyed', () => {
|
|
const comp = TestBed.createComponent(EmbeddedViewComp);
|
|
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
|
|
const embeddedViewRef = comp.componentInstance.tplRef.createEmbeddedView({});
|
|
|
|
appRef.attachView(embeddedViewRef);
|
|
embeddedViewRef.destroy();
|
|
|
|
expect(appRef.viewCount).toBe(0);
|
|
});
|
|
|
|
it('should not allow to attach a view to both, a view container and the ApplicationRef',
|
|
() => {
|
|
const comp = TestBed.createComponent(MyComp);
|
|
let hostView = comp.componentRef.hostView;
|
|
const containerComp = TestBed.createComponent(ContainerComp);
|
|
containerComp.detectChanges();
|
|
const vc = containerComp.componentInstance.vc;
|
|
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
|
|
|
|
vc.insert(hostView);
|
|
expect(() => appRef.attachView(hostView))
|
|
.toThrowError('This view is already attached to a ViewContainer!');
|
|
hostView = vc.detach(0);
|
|
|
|
appRef.attachView(hostView);
|
|
expect(() => vc.insert(hostView))
|
|
.toThrowError('This view is already attached directly to the ApplicationRef!');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('AppRef', () => {
|
|
@Component({selector: 'sync-comp', template: `<span>{{text}}</span>`})
|
|
class SyncComp {
|
|
text: string = '1';
|
|
}
|
|
|
|
@Component({selector: 'click-comp', template: `<span (click)="onClick()">{{text}}</span>`})
|
|
class ClickComp {
|
|
text: string = '1';
|
|
|
|
onClick() { this.text += '1'; }
|
|
}
|
|
|
|
@Component({selector: 'micro-task-comp', template: `<span>{{text}}</span>`})
|
|
class MicroTaskComp {
|
|
text: string = '1';
|
|
|
|
ngOnInit() {
|
|
Promise.resolve(null).then((_) => { this.text += '1'; });
|
|
}
|
|
}
|
|
|
|
@Component({selector: 'macro-task-comp', template: `<span>{{text}}</span>`})
|
|
class MacroTaskComp {
|
|
text: string = '1';
|
|
|
|
ngOnInit() {
|
|
setTimeout(() => { this.text += '1'; }, 10);
|
|
}
|
|
}
|
|
|
|
@Component({selector: 'micro-macro-task-comp', template: `<span>{{text}}</span>`})
|
|
class MicroMacroTaskComp {
|
|
text: string = '1';
|
|
|
|
ngOnInit() {
|
|
Promise.resolve(null).then((_) => {
|
|
this.text += '1';
|
|
setTimeout(() => { this.text += '1'; }, 10);
|
|
});
|
|
}
|
|
}
|
|
|
|
@Component({selector: 'macro-micro-task-comp', template: `<span>{{text}}</span>`})
|
|
class MacroMicroTaskComp {
|
|
text: string = '1';
|
|
|
|
ngOnInit() {
|
|
setTimeout(() => {
|
|
this.text += '1';
|
|
Promise.resolve(null).then((_: any) => { this.text += '1'; });
|
|
}, 10);
|
|
}
|
|
}
|
|
|
|
let stableCalled = false;
|
|
|
|
beforeEach(() => {
|
|
stableCalled = false;
|
|
TestBed.configureTestingModule({
|
|
declarations: [
|
|
SyncComp, MicroTaskComp, MacroTaskComp, MicroMacroTaskComp, MacroMicroTaskComp, ClickComp
|
|
],
|
|
});
|
|
});
|
|
|
|
afterEach(() => { expect(stableCalled).toBe(true, 'isStable did not emit true on stable'); });
|
|
|
|
function expectStableTexts(component: Type<any>, expected: string[]) {
|
|
const fixture = TestBed.createComponent(component);
|
|
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
|
|
const zone: NgZone = TestBed.get(NgZone);
|
|
appRef.attachView(fixture.componentRef.hostView);
|
|
zone.run(() => appRef.tick());
|
|
|
|
let i = 0;
|
|
appRef.isStable.subscribe({
|
|
next: (stable: boolean) => {
|
|
if (stable) {
|
|
expect(i).toBeLessThan(expected.length);
|
|
expect(fixture.nativeElement).toHaveText(expected[i++]);
|
|
stableCalled = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
it('isStable should fire on synchronous component loading',
|
|
async(() => { expectStableTexts(SyncComp, ['1']); }));
|
|
|
|
it('isStable should fire after a microtask on init is completed',
|
|
async(() => { expectStableTexts(MicroTaskComp, ['11']); }));
|
|
|
|
it('isStable should fire after a macrotask on init is completed',
|
|
async(() => { expectStableTexts(MacroTaskComp, ['11']); }));
|
|
|
|
it('isStable should fire only after chain of micro and macrotasks on init are completed',
|
|
async(() => { expectStableTexts(MicroMacroTaskComp, ['111']); }));
|
|
|
|
it('isStable should fire only after chain of macro and microtasks on init are completed',
|
|
async(() => { expectStableTexts(MacroMicroTaskComp, ['111']); }));
|
|
|
|
describe('unstable', () => {
|
|
let unstableCalled = false;
|
|
|
|
afterEach(
|
|
() => { expect(unstableCalled).toBe(true, 'isStable did not emit false on unstable'); });
|
|
|
|
function expectUnstable(appRef: ApplicationRef) {
|
|
appRef.isStable.subscribe({
|
|
next: (stable: boolean) => {
|
|
if (stable) {
|
|
stableCalled = true;
|
|
}
|
|
if (!stable) {
|
|
unstableCalled = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
it('should be fired after app becomes unstable', async(() => {
|
|
const fixture = TestBed.createComponent(ClickComp);
|
|
const appRef: ApplicationRef = TestBed.get(ApplicationRef);
|
|
const zone: NgZone = TestBed.get(NgZone);
|
|
appRef.attachView(fixture.componentRef.hostView);
|
|
zone.run(() => appRef.tick());
|
|
|
|
fixture.whenStable().then(() => {
|
|
expectUnstable(appRef);
|
|
const element = fixture.debugElement.children[0];
|
|
dispatchEvent(element.nativeElement, 'click');
|
|
});
|
|
}));
|
|
});
|
|
});
|
|
}
|
|
|
|
class MockConsole {
|
|
res: any[][] = [];
|
|
log(...args: any[]): void { this.res.push(args); }
|
|
error(...args: any[]): void { this.res.push(args); }
|
|
}
|