refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
515
packages/core/test/application_ref_spec.ts
Normal file
515
packages/core/test/application_ref_spec.ts
Normal file
@ -0,0 +1,515 @@
|
||||
/**
|
||||
* @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, 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/browser_util';
|
||||
import {expect} from '@angular/platform-browser/testing/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(false);
|
||||
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;
|
||||
}
|
||||
|
||||
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).toEqual(['EXCEPTION: 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]).toEqual('EXCEPTION: ' + 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).toEqual(['EXCEPTION: 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(s: any): void { this.res.push(s); }
|
||||
error(s: any): void { this.res.push(s); }
|
||||
}
|
Reference in New Issue
Block a user