ng_module_integration_spec.ts
/**
* @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 {ANALYZE_FOR_ENTRY_COMPONENTS, CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, ComponentFactoryResolver, Directive, HostBinding, Inject, Injectable, InjectionToken, Injector, Input, NgModule, NgModuleRef, Optional, Pipe, Provider, Self, Type, forwardRef, getModuleFactory} from '@angular/core';
import {Console} from '@angular/core/src/console';
import {ComponentFixture, TestBed, inject} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {NgModuleInjector} from '../../src/linker/ng_module_factory';
import {clearModulesForTest} from '../../src/linker/ng_module_factory_loader';
import {stringify} from '../../src/util';
class Engine {}
class BrokenEngine {
constructor() { throw new Error('Broken Engine'); }
}
class DashboardSoftware {}
@Injectable()
class Dashboard {
constructor(software: DashboardSoftware) {}
}
class TurboEngine extends Engine {}
const CARS = new InjectionToken<Car[]>('Cars');
@Injectable()
class Car {
engine: Engine;
constructor(engine: Engine) { this.engine = engine; }
}
@Injectable()
class CarWithOptionalEngine {
engine: Engine;
constructor(@Optional() engine: Engine) { this.engine = engine; }
}
@Injectable()
class CarWithDashboard {
engine: Engine;
dashboard: Dashboard;
constructor(engine: Engine, dashboard: Dashboard) {
this.engine = engine;
this.dashboard = dashboard;
}
}
@Injectable()
class SportsCar extends Car {
engine: Engine;
constructor(engine: Engine) { super(engine); }
}
@Injectable()
class CarWithInject {
engine: Engine;
constructor(@Inject(TurboEngine) engine: Engine) { this.engine = engine; }
}
@Injectable()
class CyclicEngine {
constructor(car: Car) {}
}
class NoAnnotations {
constructor(secretDependency: any) {}
}
function factoryFn(a: any) {}
@Component({selector: 'comp', template: ''})
class SomeComp {
}
@Directive({selector: '[someDir]'})
class SomeDirective {
@HostBinding('title') @Input()
someDir: string;
}
@Pipe({name: 'somePipe'})
class SomePipe {
transform(value: string): any { return `transformed ${value}`; }
}
@Component({selector: 'comp', template: `<div [someDir]="'someValue' | somePipe"></div>`})
class CompUsingModuleDirectiveAndPipe {
}
class DummyConsole implements Console {
public warnings: string[] = [];
log(message: string) {}
warn(message: string) { this.warnings.push(message); }
}
export function main() {
describe('jit', () => { declareTests({useJit: true}); });
describe('no jit', () => { declareTests({useJit: false}); });
}
function declareTests({useJit}: {useJit: boolean}) {
describe('NgModule', () => {
let compiler: Compiler;
let injector: Injector;
let console: DummyConsole;
beforeEach(() => {
console = new DummyConsole();
TestBed.configureCompiler(
{useJit: useJit, providers: [{provide: Console, useValue: console}]});
});
beforeEach(inject([Compiler, Injector], (_compiler: Compiler, _injector: Injector) => {
compiler = _compiler;
injector = _injector;
}));
function createModule<T>(
moduleType: Type<T>, parentInjector?: Injector | null): NgModuleRef<T> {
return compiler.compileModuleSync(moduleType).create(parentInjector || null);
}
function createComp<T>(compType: Type<T>, moduleType: Type<any>): ComponentFixture<T> {
const ngModule = createModule(moduleType, injector);
const cf = ngModule.componentFactoryResolver.resolveComponentFactory(compType) !;
const comp = cf.create(Injector.NULL);
return new ComponentFixture(comp, null !, false);
}
describe('errors', () => {
it('should error when exporting a directive that was neither declared nor imported', () => {
@NgModule({exports: [SomeDirective]})
class SomeModule {
}
expect(() => createModule(SomeModule))
.toThrowError(
`Can't export directive ${stringify(SomeDirective)} from ${stringify(SomeModule)} as it was neither declared nor imported!`);
});
it('should error when exporting a pipe that was neither declared nor imported', () => {
@NgModule({exports: [SomePipe]})
class SomeModule {
}
expect(() => createModule(SomeModule))
.toThrowError(
`Can't export pipe ${stringify(SomePipe)} from ${stringify(SomeModule)} as it was neither declared nor imported!`);
});
it('should error if a directive is declared in more than 1 module', () => {
@NgModule({declarations: [SomeDirective]})
class Module1 {
}
@NgModule({declarations: [SomeDirective]})
class Module2 {
}
createModule(Module1);
expect(() => createModule(Module2))
.toThrowError(
`Type ${stringify(SomeDirective)} is part of the declarations of 2 modules: ${stringify(Module1)} and ${stringify(Module2)}! ` +
`Please consider moving ${stringify(SomeDirective)} to a higher module that imports ${stringify(Module1)} and ${stringify(Module2)}. ` +
`You can also create a new NgModule that exports and includes ${stringify(SomeDirective)} then import that NgModule in ${stringify(Module1)} and ${stringify(Module2)}.`);
});
it('should error if a directive is declared in more than 1 module also if the module declaring it is imported',
() => {
@NgModule({declarations: [SomeDirective], exports: [SomeDirective]})
class Module1 {
}
@NgModule({declarations: [SomeDirective], imports: [Module1]})
class Module2 {
}
expect(() => createModule(Module2))
.toThrowError(
`Type ${stringify(SomeDirective)} is part of the declarations of 2 modules: ${stringify(Module1)} and ${stringify(Module2)}! ` +
`Please consider moving ${stringify(SomeDirective)} to a higher module that imports ${stringify(Module1)} and ${stringify(Module2)}. ` +
`You can also create a new NgModule that exports and includes ${stringify(SomeDirective)} then import that NgModule in ${stringify(Module1)} and ${stringify(Module2)}.`);
});
it('should error if a pipe is declared in more than 1 module', () => {
@NgModule({declarations: [SomePipe]})
class Module1 {
}
@NgModule({declarations: [SomePipe]})
class Module2 {
}
createModule(Module1);
expect(() => createModule(Module2))
.toThrowError(
`Type ${stringify(SomePipe)} is part of the declarations of 2 modules: ${stringify(Module1)} and ${stringify(Module2)}! ` +
`Please consider moving ${stringify(SomePipe)} to a higher module that imports ${stringify(Module1)} and ${stringify(Module2)}. ` +
`You can also create a new NgModule that exports and includes ${stringify(SomePipe)} then import that NgModule in ${stringify(Module1)} and ${stringify(Module2)}.`);
});
it('should error if a pipe is declared in more than 1 module also if the module declaring it is imported',
() => {
@NgModule({declarations: [SomePipe], exports: [SomePipe]})
class Module1 {
}
@NgModule({declarations: [SomePipe], imports: [Module1]})
class Module2 {
}
expect(() => createModule(Module2))
.toThrowError(
`Type ${stringify(SomePipe)} is part of the declarations of 2 modules: ${stringify(Module1)} and ${stringify(Module2)}! ` +
`Please consider moving ${stringify(SomePipe)} to a higher module that imports ${stringify(Module1)} and ${stringify(Module2)}. ` +
`You can also create a new NgModule that exports and includes ${stringify(SomePipe)} then import that NgModule in ${stringify(Module1)} and ${stringify(Module2)}.`);
});
});
describe('schemas', () => {
it('should error on unknown bound properties on custom elements by default', () => {
@Component({template: '<some-element [someUnknownProp]="true"></some-element>'})
class ComponentUsingInvalidProperty {
}
@NgModule({declarations: [ComponentUsingInvalidProperty]})
class SomeModule {
}
expect(() => createModule(SomeModule)).toThrowError(/Can't bind to 'someUnknownProp'/);
});
it('should not error on unknown bound properties on custom elements when using the CUSTOM_ELEMENTS_SCHEMA',
() => {
@Component({template: '<some-element [someUnknownProp]="true"></some-element>'})
class ComponentUsingInvalidProperty {
}
@NgModule(
{schemas: [CUSTOM_ELEMENTS_SCHEMA], declarations: [ComponentUsingInvalidProperty]})
class SomeModule {
}
expect(() => createModule(SomeModule)).not.toThrow();
});
});
describe('id', () => {
const token = 'myid';
@NgModule({id: token})
class SomeModule {
}
@NgModule({id: token})
class SomeOtherModule {
}
afterEach(() => clearModulesForTest());
it('should register loaded modules', () => {
createModule(SomeModule);
const factory = getModuleFactory(token);
expect(factory).toBeTruthy();
expect(factory.moduleType).toBe(SomeModule);
});
it('should throw when registering a duplicate module', () => {
createModule(SomeModule);
expect(() => createModule(SomeOtherModule)).toThrowError(/Duplicate module registered/);
});
});
describe('entryComponents', () => {
it('should create ComponentFactories in root modules', () => {
@NgModule({declarations: [SomeComp], entryComponents: [SomeComp]})
class SomeModule {
}
const ngModule = createModule(SomeModule);
expect(ngModule.componentFactoryResolver.resolveComponentFactory(SomeComp) !.componentType)
.toBe(SomeComp);
expect(ngModule.injector.get(ComponentFactoryResolver)
.resolveComponentFactory(SomeComp)
.componentType)
.toBe(SomeComp);
});
it('should throw if we cannot find a module associated with a module-level entryComponent', () => {
@Component({template: ''})
class SomeCompWithEntryComponents {
}
@NgModule({declarations: [], entryComponents: [SomeCompWithEntryComponents]})
class SomeModule {
}
expect(() => createModule(SomeModule))
.toThrowError(
'Component SomeCompWithEntryComponents is not part of any NgModule or the module has not been imported into your module.');
});
it('should throw if we cannot find a module associated with a component-level entryComponent',
() => {
@Component({template: '', entryComponents: [SomeComp]})
class SomeCompWithEntryComponents {
}
@NgModule({declarations: [SomeCompWithEntryComponents]})
class SomeModule {
}
expect(() => createModule(SomeModule))
.toThrowError(
'Component SomeComp is not part of any NgModule or the module has not been imported into your module.');
});
it('should create ComponentFactories via ANALYZE_FOR_ENTRY_COMPONENTS', () => {
@NgModule({
declarations: [SomeComp],
providers: [{
provide: ANALYZE_FOR_ENTRY_COMPONENTS,
multi: true,
useValue: [{a: 'b', component: SomeComp}]
}]
})
class SomeModule {
}
const ngModule = createModule(SomeModule);
expect(ngModule.componentFactoryResolver.resolveComponentFactory(SomeComp) !.componentType)
.toBe(SomeComp);
expect(ngModule.injector.get(ComponentFactoryResolver)
.resolveComponentFactory(SomeComp)
.componentType)
.toBe(SomeComp);
});
it('should create ComponentFactories in imported modules', () => {
@NgModule({declarations: [SomeComp], entryComponents: [SomeComp]})
class SomeImportedModule {
}
@NgModule({imports: [SomeImportedModule]})
class SomeModule {
}
const ngModule = createModule(SomeModule);
expect(ngModule.componentFactoryResolver.resolveComponentFactory(SomeComp) !.componentType)
.toBe(SomeComp);
expect(ngModule.injector.get(ComponentFactoryResolver)
.resolveComponentFactory(SomeComp)
.componentType)
.toBe(SomeComp);
});
it('should create ComponentFactories if the component was imported', () => {
@NgModule({declarations: [SomeComp], exports: [SomeComp]})
class SomeImportedModule {
}
@NgModule({imports: [SomeImportedModule], entryComponents: [SomeComp]})
class SomeModule {
}
const ngModule = createModule(SomeModule);
expect(ngModule.componentFactoryResolver.resolveComponentFactory(SomeComp) !.componentType)
.toBe(SomeComp);
expect(ngModule.injector.get(ComponentFactoryResolver)
.resolveComponentFactory(SomeComp)
.componentType)
.toBe(SomeComp);
});
});
describe('bootstrap components', () => {
it('should create ComponentFactories', () => {
@NgModule({declarations: [SomeComp], bootstrap: [SomeComp]})
class SomeModule {
}
const ngModule = createModule(SomeModule);
expect(ngModule.componentFactoryResolver.resolveComponentFactory(SomeComp) !.componentType)
.toBe(SomeComp);
});
it('should store the ComponentFactories in the NgModuleInjector', () => {
@NgModule({declarations: [SomeComp], bootstrap: [SomeComp]})
class SomeModule {
}
const ngModule = <NgModuleInjector<any>>createModule(SomeModule);
expect(ngModule.bootstrapFactories.length).toBe(1);
expect(ngModule.bootstrapFactories[0].componentType).toBe(SomeComp);
});
});
describe('directives and pipes', () => {
describe('declarations', () => {
it('should be supported in root modules', () => {
@NgModule({
declarations: [CompUsingModuleDirectiveAndPipe, SomeDirective, SomePipe],
entryComponents: [CompUsingModuleDirectiveAndPipe]
})
class SomeModule {
}
const compFixture = createComp(CompUsingModuleDirectiveAndPipe, SomeModule);
compFixture.detectChanges();
expect(compFixture.debugElement.children[0].properties['title'])
.toBe('transformed someValue');
});
it('should be supported in imported modules', () => {
@NgModule({
declarations: [CompUsingModuleDirectiveAndPipe, SomeDirective, SomePipe],
entryComponents: [CompUsingModuleDirectiveAndPipe]
})
class SomeImportedModule {
}
@NgModule({imports: [SomeImportedModule]})
class SomeModule {
}
const compFixture = createComp(CompUsingModuleDirectiveAndPipe, SomeModule);
compFixture.detectChanges();
expect(compFixture.debugElement.children[0].properties['title'])
.toBe('transformed someValue');
});
it('should be supported in nested components', () => {
@Component({
selector: 'parent',
template: '<comp></comp>',
})
class ParentCompUsingModuleDirectiveAndPipe {
}
@NgModule({
declarations: [
ParentCompUsingModuleDirectiveAndPipe, CompUsingModuleDirectiveAndPipe, SomeDirective,
SomePipe
],
entryComponents: [ParentCompUsingModuleDirectiveAndPipe]
})
class SomeModule {
}
const compFixture = createComp(ParentCompUsingModuleDirectiveAndPipe, SomeModule);
compFixture.detectChanges();
expect(compFixture.debugElement.children[0].children[0].properties['title'])
.toBe('transformed someValue');
});
});
describe('import/export', () => {
it('should support exported directives and pipes', () => {
@NgModule({declarations: [SomeDirective, SomePipe], exports: [SomeDirective, SomePipe]})
class SomeImportedModule {
}
@NgModule({
declarations: [CompUsingModuleDirectiveAndPipe],
imports: [SomeImportedModule],
entryComponents: [CompUsingModuleDirectiveAndPipe]
})
class SomeModule {
}
const compFixture = createComp(CompUsingModuleDirectiveAndPipe, SomeModule);
compFixture.detectChanges();
expect(compFixture.debugElement.children[0].properties['title'])
.toBe('transformed someValue');
});
it('should support exported directives and pipes if the module is wrapped into an `ModuleWithProviders`',
() => {
@NgModule(
{declarations: [SomeDirective, SomePipe], exports: [SomeDirective, SomePipe]})
class SomeImportedModule {
}
@NgModule({
declarations: [CompUsingModuleDirectiveAndPipe],
imports: [{ngModule: SomeImportedModule}],
entryComponents: [CompUsingModuleDirectiveAndPipe]
})
class SomeModule {
}
const compFixture = createComp(CompUsingModuleDirectiveAndPipe, SomeModule);
compFixture.detectChanges();
expect(compFixture.debugElement.children[0].properties['title'])
.toBe('transformed someValue');
});
it('should support reexported modules', () => {
@NgModule({declarations: [SomeDirective, SomePipe], exports: [SomeDirective, SomePipe]})
class SomeReexportedModule {
}
@NgModule({exports: [SomeReexportedModule]})
class SomeImportedModule {
}
@NgModule({
declarations: [CompUsingModuleDirectiveAndPipe],
imports: [SomeImportedModule],
entryComponents: [CompUsingModuleDirectiveAndPipe]
})
class SomeModule {
}
const compFixture = createComp(CompUsingModuleDirectiveAndPipe, SomeModule);
compFixture.detectChanges();
expect(compFixture.debugElement.children[0].properties['title'])
.toBe('transformed someValue');
});
it('should support exporting individual directives of an imported module', () => {
@NgModule({declarations: [SomeDirective, SomePipe], exports: [SomeDirective, SomePipe]})
class SomeReexportedModule {
}
@NgModule({imports: [SomeReexportedModule], exports: [SomeDirective, SomePipe]})
class SomeImportedModule {
}
@NgModule({
declarations: [CompUsingModuleDirectiveAndPipe],
imports: [SomeImportedModule],
entryComponents: [CompUsingModuleDirectiveAndPipe]
})
class SomeModule {
}
const compFixture = createComp(CompUsingModuleDirectiveAndPipe, SomeModule);
compFixture.detectChanges();
expect(compFixture.debugElement.children[0].properties['title'])
.toBe('transformed someValue');
});
it('should not use non exported pipes of an imported module', () => {
@NgModule({
declarations: [SomePipe],
})
class SomeImportedModule {
}
@NgModule({
declarations: [CompUsingModuleDirectiveAndPipe],
imports: [SomeImportedModule],
entryComponents: [CompUsingModuleDirectiveAndPipe]
})
class SomeModule {
}
expect(() => createComp(SomeComp, SomeModule))
.toThrowError(/The pipe 'somePipe' could not be found/);
});
it('should not use non exported directives of an imported module', () => {
@NgModule({
declarations: [SomeDirective],
})
class SomeImportedModule {
}
@NgModule({
declarations: [CompUsingModuleDirectiveAndPipe, SomePipe],
imports: [SomeImportedModule],
entryComponents: [CompUsingModuleDirectiveAndPipe]
})
class SomeModule {
}
expect(() => createComp(SomeComp, SomeModule)).toThrowError(/Can't bind to 'someDir'/);
});
});
});
describe('providers', function() {
let moduleType: any = null;
function createInjector(providers: Provider[], parent?: Injector | null): Injector {
@NgModule({providers: providers})
class SomeModule {
}
moduleType = SomeModule;
return createModule(SomeModule, parent).injector;
}
it('should provide the module',
() => { expect(createInjector([]).get(moduleType)).toBeAnInstanceOf(moduleType); });
it('should instantiate a class without dependencies', () => {
const injector = createInjector([Engine]);
const engine = injector.get(Engine);
expect(engine).toBeAnInstanceOf(Engine);
});
it('should resolve dependencies based on type information', () => {
const injector = createInjector([Engine, Car]);
const car = injector.get(Car);
expect(car).toBeAnInstanceOf(Car);
expect(car.engine).toBeAnInstanceOf(Engine);
});
it('should resolve dependencies based on @Inject annotation', () => {
const injector = createInjector([TurboEngine, Engine, CarWithInject]);
const car = injector.get(CarWithInject);
expect(car).toBeAnInstanceOf(CarWithInject);
expect(car.engine).toBeAnInstanceOf(TurboEngine);
});
it('should throw when no type and not @Inject (class case)', () => {
expect(() => createInjector([NoAnnotations]))
.toThrowError('Can\'t resolve all parameters for NoAnnotations: (?).');
});
it('should throw when no type and not @Inject (factory case)', () => {
expect(() => createInjector([{provide: 'someToken', useFactory: factoryFn}]))
.toThrowError('Can\'t resolve all parameters for factoryFn: (?).');
});
it('should cache instances', () => {
const injector = createInjector([Engine]);
const e1 = injector.get(Engine);
const e2 = injector.get(Engine);
expect(e1).toBe(e2);
});
it('should provide to a value', () => {
const injector = createInjector([{provide: Engine, useValue: 'fake engine'}]);
const engine = injector.get(Engine);
expect(engine).toEqual('fake engine');
});
it('should provide to a factory', () => {
function sportsCarFactory(e: Engine) { return new SportsCar(e); }
const injector =
createInjector([Engine, {provide: Car, useFactory: sportsCarFactory, deps: [Engine]}]);
const car = injector.get(Car);
expect(car).toBeAnInstanceOf(SportsCar);
expect(car.engine).toBeAnInstanceOf(Engine);
});
it('should supporting provider to null', () => {
const injector = createInjector([{provide: Engine, useValue: null}]);
const engine = injector.get(Engine);
expect(engine).toBeNull();
});
it('should provide to an alias', () => {
const injector = createInjector([
Engine, {provide: SportsCar, useClass: SportsCar},
{provide: Car, useExisting: SportsCar}
]);
const car = injector.get(Car);
const sportsCar = injector.get(SportsCar);
expect(car).toBeAnInstanceOf(SportsCar);
expect(car).toBe(sportsCar);
});
it('should support multiProviders', () => {
const injector = createInjector([
Engine, {provide: CARS, useClass: SportsCar, multi: true},
{provide: CARS, useClass: CarWithOptionalEngine, multi: true}
]);
const cars = injector.get(CARS);
expect(cars.length).toEqual(2);
expect(cars[0]).toBeAnInstanceOf(SportsCar);
expect(cars[1]).toBeAnInstanceOf(CarWithOptionalEngine);
});
it('should support multiProviders that are created using useExisting', () => {
const injector = createInjector(
[Engine, SportsCar, {provide: CARS, useExisting: SportsCar, multi: true}]);
const cars = injector.get(CARS);
expect(cars.length).toEqual(1);
expect(cars[0]).toBe(injector.get(SportsCar));
});
it('should throw when the aliased provider does not exist', () => {
const injector = createInjector([{provide: 'car', useExisting: SportsCar}]);
const e = `No provider for ${stringify(SportsCar)}!`;
expect(() => injector.get('car')).toThrowError(e);
});
it('should handle forwardRef in useExisting', () => {
const injector = createInjector([
{provide: 'originalEngine', useClass: forwardRef(() => Engine)},
{provide: 'aliasedEngine', useExisting: <any>forwardRef(() => 'originalEngine')}
]);
expect(injector.get('aliasedEngine')).toBeAnInstanceOf(Engine);
});
it('should support overriding factory dependencies', () => {
const injector = createInjector(
[Engine, {provide: Car, useFactory: (e: Engine) => new SportsCar(e), deps: [Engine]}]);
const car = injector.get(Car);
expect(car).toBeAnInstanceOf(SportsCar);
expect(car.engine).toBeAnInstanceOf(Engine);
});
it('should support optional dependencies', () => {
const injector = createInjector([CarWithOptionalEngine]);
const car = injector.get(CarWithOptionalEngine);
expect(car.engine).toEqual(null);
});
it('should flatten passed-in providers', () => {
const injector = createInjector([[[Engine, Car]]]);
const car = injector.get(Car);
expect(car).toBeAnInstanceOf(Car);
});
it('should use the last provider when there are multiple providers for same token', () => {
const injector = createInjector(
[{provide: Engine, useClass: Engine}, {provide: Engine, useClass: TurboEngine}]);
expect(injector.get(Engine)).toBeAnInstanceOf(TurboEngine);
});
it('should use non-type tokens', () => {
const injector = createInjector([{provide: 'token', useValue: 'value'}]);
expect(injector.get('token')).toEqual('value');
});
it('should throw when given invalid providers', () => {
expect(() => createInjector(<any>['blah']))
.toThrowError(
`Invalid provider for the NgModule 'SomeModule' - only instances of Provider and Type are allowed, got: [?blah?]`);
});
it('should throw when given blank providers', () => {
expect(() => createInjector(<any>[null, {provide: 'token', useValue: 'value'}]))
.toThrowError(
`Invalid provider for the NgModule 'SomeModule' - only instances of Provider and Type are allowed, got: [?null?, ...]`);
});
it('should provide itself', () => {
const parent = createInjector([]);
const child = createInjector([], parent);
expect(child.get(Injector)).toBe(child);
});
it('should throw when no provider defined', () => {
const injector = createInjector([]);
expect(() => injector.get('NonExisting')).toThrowError('No provider for NonExisting!');
});
it('should throw when trying to instantiate a cyclic dependency', () => {
expect(() => createInjector([Car, {provide: Engine, useClass: CyclicEngine}]))
.toThrowError(/Cannot instantiate cyclic dependency! Car/g);
});
it('should support null values', () => {
const injector = createInjector([{provide: 'null', useValue: null}]);
expect(injector.get('null')).toBe(null);
});
describe('child', () => {
it('should load instances from parent injector', () => {
const parent = createInjector([Engine]);
const child = createInjector([], parent);
const engineFromParent = parent.get(Engine);
const engineFromChild = child.get(Engine);
expect(engineFromChild).toBe(engineFromParent);
});
it('should not use the child providers when resolving the dependencies of a parent provider',
() => {
const parent = createInjector([Car, Engine]);
const child = createInjector([{provide: Engine, useClass: TurboEngine}], parent);
const carFromChild = child.get(Car);
expect(carFromChild.engine).toBeAnInstanceOf(Engine);
});
it('should create new instance in a child injector', () => {
const parent = createInjector([Engine]);
const child = createInjector([{provide: Engine, useClass: TurboEngine}], parent);
const engineFromParent = parent.get(Engine);
const engineFromChild = child.get(Engine);
expect(engineFromParent).not.toBe(engineFromChild);
expect(engineFromChild).toBeAnInstanceOf(TurboEngine);
});
});
describe('depedency resolution', () => {
describe('@Self()', () => {
it('should return a dependency from self', () => {
const inj = createInjector([
Engine,
{provide: Car, useFactory: (e: Engine) => new Car(e), deps: [[Engine, new Self()]]}
]);
expect(inj.get(Car)).toBeAnInstanceOf(Car);
});
it('should throw when not requested provider on self', () => {
expect(() => createInjector([{
provide: Car,
useFactory: (e: Engine) => new Car(e),
deps: [[Engine, new Self()]]
}]))
.toThrowError(/No provider for Engine/g);
});
});
describe('default', () => {
it('should not skip self', () => {
const parent = createInjector([Engine]);
const child = createInjector(
[
{provide: Engine, useClass: TurboEngine},
{provide: Car, useFactory: (e: Engine) => new Car(e), deps: [Engine]}
],
parent);
expect(child.get(Car).engine).toBeAnInstanceOf(TurboEngine);
});
});
});
describe('lifecycle', () => {
it('should instantiate modules eagerly', () => {
let created = false;
@NgModule()
class ImportedModule {
constructor() { created = true; }
}
@NgModule({imports: [ImportedModule]})
class SomeModule {
}
createModule(SomeModule);
expect(created).toBe(true);
});
it('should instantiate providers that are not used by a module lazily', () => {
let created = false;
createInjector([{
provide: 'someToken',
useFactory: () => {
created = true;
return true;
}
}]);
expect(created).toBe(false);
});
it('should support ngOnDestroy on any provider', () => {
let destroyed = false;
class SomeInjectable {
ngOnDestroy() { destroyed = true; }
}
@NgModule({providers: [SomeInjectable]})
class SomeModule {
// Inject SomeInjectable to make it eager...
constructor(i: SomeInjectable) {}
}
const moduleRef = createModule(SomeModule);
expect(destroyed).toBe(false);
moduleRef.destroy();
expect(destroyed).toBe(true);
});
it('should support ngOnDestroy for lazy providers', () => {
let created = false;
let destroyed = false;
class SomeInjectable {
constructor() { created = true; }
ngOnDestroy() { destroyed = true; }
}
@NgModule({providers: [SomeInjectable]})
class SomeModule {
}
let moduleRef = createModule(SomeModule);
expect(created).toBe(false);
expect(destroyed).toBe(false);
// no error if the provider was not yet created
moduleRef.destroy();
expect(created).toBe(false);
expect(destroyed).toBe(false);
moduleRef = createModule(SomeModule);
moduleRef.injector.get(SomeInjectable);
expect(created).toBe(true);
moduleRef.destroy();
expect(destroyed).toBe(true);
});
});
describe('imported and exported modules', () => {
it('should add the providers of imported modules', () => {
@NgModule({providers: [{provide: 'token1', useValue: 'imported'}]})
class ImportedModule {
}
@NgModule({imports: [ImportedModule]})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get(SomeModule)).toBeAnInstanceOf(SomeModule);
expect(injector.get(ImportedModule)).toBeAnInstanceOf(ImportedModule);
expect(injector.get('token1')).toBe('imported');
});
it('should add the providers of imported ModuleWithProviders', () => {
@NgModule()
class ImportedModule {
}
@NgModule({
imports: [
{ngModule: ImportedModule, providers: [{provide: 'token1', useValue: 'imported'}]}
]
})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get(SomeModule)).toBeAnInstanceOf(SomeModule);
expect(injector.get(ImportedModule)).toBeAnInstanceOf(ImportedModule);
expect(injector.get('token1')).toBe('imported');
});
it('should overwrite the providers of imported modules', () => {
@NgModule({providers: [{provide: 'token1', useValue: 'imported'}]})
class ImportedModule {
}
@NgModule(
{providers: [{provide: 'token1', useValue: 'direct'}], imports: [ImportedModule]})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get('token1')).toBe('direct');
});
it('should overwrite the providers of imported ModuleWithProviders', () => {
@NgModule()
class ImportedModule {
}
@NgModule({
providers: [{provide: 'token1', useValue: 'direct'}],
imports: [
{ngModule: ImportedModule, providers: [{provide: 'token1', useValue: 'imported'}]}
]
})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get('token1')).toBe('direct');
});
it('should overwrite the providers of imported modules on the second import level', () => {
@NgModule({providers: [{provide: 'token1', useValue: 'imported'}]})
class ImportedModuleLevel2 {
}
@NgModule({
providers: [{provide: 'token1', useValue: 'direct'}],
imports: [ImportedModuleLevel2]
})
class ImportedModuleLevel1 {
}
@NgModule({imports: [ImportedModuleLevel1]})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get('token1')).toBe('direct');
});
it('should add the providers of exported modules', () => {
@NgModule({providers: [{provide: 'token1', useValue: 'exported'}]})
class ExportedValue {
}
@NgModule({exports: [ExportedValue]})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get(SomeModule)).toBeAnInstanceOf(SomeModule);
expect(injector.get(ExportedValue)).toBeAnInstanceOf(ExportedValue);
expect(injector.get('token1')).toBe('exported');
});
it('should overwrite the providers of exported modules', () => {
@NgModule({providers: [{provide: 'token1', useValue: 'exported'}]})
class ExportedModule {
}
@NgModule(
{providers: [{provide: 'token1', useValue: 'direct'}], exports: [ExportedModule]})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get('token1')).toBe('direct');
});
it('should overwrite the providers of imported modules by following imported modules',
() => {
@NgModule({providers: [{provide: 'token1', useValue: 'imported1'}]})
class ImportedModule1 {
}
@NgModule({providers: [{provide: 'token1', useValue: 'imported2'}]})
class ImportedModule2 {
}
@NgModule({imports: [ImportedModule1, ImportedModule2]})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get('token1')).toBe('imported2');
});
it('should overwrite the providers of exported modules by following exported modules',
() => {
@NgModule({providers: [{provide: 'token1', useValue: 'exported1'}]})
class ExportedModule1 {
}
@NgModule({providers: [{provide: 'token1', useValue: 'exported2'}]})
class ExportedModule2 {
}
@NgModule({exports: [ExportedModule1, ExportedModule2]})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get('token1')).toBe('exported2');
});
it('should overwrite the providers of imported modules by exported modules', () => {
@NgModule({providers: [{provide: 'token1', useValue: 'imported'}]})
class ImportedModule {
}
@NgModule({providers: [{provide: 'token1', useValue: 'exported'}]})
class ExportedModule {
}
@NgModule({imports: [ImportedModule], exports: [ExportedModule]})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get('token1')).toBe('exported');
});
it('should not overwrite the providers if a module was already used on the same level',
() => {
@NgModule({providers: [{provide: 'token1', useValue: 'imported1'}]})
class ImportedModule1 {
}
@NgModule({providers: [{provide: 'token1', useValue: 'imported2'}]})
class ImportedModule2 {
}
@NgModule({imports: [ImportedModule1, ImportedModule2, ImportedModule1]})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get('token1')).toBe('imported2');
});
it('should not overwrite the providers if a module was already used on a child level',
() => {
@NgModule({providers: [{provide: 'token1', useValue: 'imported1'}]})
class ImportedModule1 {
}
@NgModule({imports: [ImportedModule1]})
class ImportedModule3 {
}
@NgModule({providers: [{provide: 'token1', useValue: 'imported2'}]})
class ImportedModule2 {
}
@NgModule({imports: [ImportedModule3, ImportedModule2, ImportedModule1]})
class SomeModule {
}
const injector = createModule(SomeModule).injector;
expect(injector.get('token1')).toBe('imported2');
});
it('should throw when given invalid providers in an imported ModuleWithProviders', () => {
@NgModule()
class ImportedModule1 {
}
@NgModule({imports: [{ngModule: ImportedModule1, providers: [<any>'broken']}]})
class SomeModule {
}
expect(() => createModule(SomeModule).injector)
.toThrowError(
`Invalid provider for the NgModule 'ImportedModule1' - only instances of Provider and Type are allowed, got: [?broken?]`);
});
});
});
});
}