/** * @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 {Component, Directive, Injector, NgModule, Pipe, PlatformRef, Provider, RendererFactory2, SchemaMetadata, Type, ɵNgModuleDefInternal as NgModuleDefInternal, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵRender3ComponentFactory as ComponentFactory, ɵRender3DebugRendererFactory2 as Render3DebugRendererFactory2, ɵRender3NgModuleRef as NgModuleRef, ɵWRAP_RENDERER_FACTORY2 as WRAP_RENDERER_FACTORY2, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵstringify as stringify} from '@angular/core'; import {ComponentFixture} from './component_fixture'; import {MetadataOverride} from './metadata_override'; import {ComponentResolver, DirectiveResolver, NgModuleResolver, PipeResolver, Resolver} from './resolvers'; import {TestBed} from './test_bed'; import {ComponentFixtureAutoDetect, TestBedStatic, TestComponentRenderer, TestModuleMetadata} from './test_bed_common'; let _nextRootElementId = 0; /** * @description * Configures and initializes environment for unit testing and provides methods for * creating components and services in unit tests. * * TestBed is the primary api for writing unit tests for Angular applications and libraries. * * Note: Use `TestBed` in tests. It will be set to either `TestBedViewEngine` or `TestBedRender3` * according to the compiler used. */ export class TestBedRender3 implements Injector, TestBed { /** * Initialize the environment for testing with a compiler factory, a PlatformRef, and an * angular module. These are common to every test in the suite. * * This may only be called once, to set up the common providers for the current test * suite on the current platform. If you absolutely need to change the providers, * first use `resetTestEnvironment`. * * Test modules and platforms for individual platforms are available from * '@angular//testing'. * * @experimental */ static initTestEnvironment( ngModule: Type|Type[], platform: PlatformRef, aotSummaries?: () => any[]): TestBed { const testBed = _getTestBedRender3(); testBed.initTestEnvironment(ngModule, platform, aotSummaries); return testBed; } /** * Reset the providers for the test injector. * * @experimental */ static resetTestEnvironment(): void { _getTestBedRender3().resetTestEnvironment(); } static configureCompiler(config: {providers?: any[]; useJit?: boolean;}): TestBedStatic { _getTestBedRender3().configureCompiler(config); return TestBedRender3 as any as TestBedStatic; } /** * Allows overriding default providers, directives, pipes, modules of the test injector, * which are defined in test_injector.js */ static configureTestingModule(moduleDef: TestModuleMetadata): TestBedStatic { _getTestBedRender3().configureTestingModule(moduleDef); return TestBedRender3 as any as TestBedStatic; } /** * Compile components with a `templateUrl` for the test's NgModule. * It is necessary to call this function * as fetching urls is asynchronous. */ static compileComponents(): Promise { return _getTestBedRender3().compileComponents(); } static overrideModule(ngModule: Type, override: MetadataOverride): TestBedStatic { _getTestBedRender3().overrideModule(ngModule, override); return TestBedRender3 as any as TestBedStatic; } static overrideComponent(component: Type, override: MetadataOverride): TestBedStatic { _getTestBedRender3().overrideComponent(component, override); return TestBedRender3 as any as TestBedStatic; } static overrideDirective(directive: Type, override: MetadataOverride): TestBedStatic { _getTestBedRender3().overrideDirective(directive, override); return TestBedRender3 as any as TestBedStatic; } static overridePipe(pipe: Type, override: MetadataOverride): TestBedStatic { _getTestBedRender3().overridePipe(pipe, override); return TestBedRender3 as any as TestBedStatic; } static overrideTemplate(component: Type, template: string): TestBedStatic { _getTestBedRender3().overrideComponent(component, {set: {template, templateUrl: null !}}); return TestBedRender3 as any as TestBedStatic; } /** * Overrides the template of the given component, compiling the template * in the context of the TestingModule. * * Note: This works for JIT and AOTed components as well. */ static overrideTemplateUsingTestingModule(component: Type, template: string): TestBedStatic { _getTestBedRender3().overrideTemplateUsingTestingModule(component, template); return TestBedRender3 as any as TestBedStatic; } overrideTemplateUsingTestingModule(component: Type, template: string): void { throw new Error('Render3TestBed.overrideTemplateUsingTestingModule is not implemented yet'); } static overrideProvider(token: any, provider: { useFactory: Function, deps: any[], }): TestBedStatic; static overrideProvider(token: any, provider: {useValue: any;}): TestBedStatic; static overrideProvider(token: any, provider: { useFactory?: Function, useValue?: any, deps?: any[], }): TestBedStatic { _getTestBedRender3().overrideProvider(token, provider); return TestBedRender3 as any as TestBedStatic; } /** * Overwrites all providers for the given token with the given provider definition. * * @deprecated as it makes all NgModules lazy. Introduced only for migrating off of it. */ static deprecatedOverrideProvider(token: any, provider: { useFactory: Function, deps: any[], }): void; static deprecatedOverrideProvider(token: any, provider: {useValue: any;}): void; static deprecatedOverrideProvider(token: any, provider: { useFactory?: Function, useValue?: any, deps?: any[], }): TestBedStatic { throw new Error('Render3TestBed.deprecatedOverrideProvider is not implemented'); } static get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { return _getTestBedRender3().get(token, notFoundValue); } static createComponent(component: Type): ComponentFixture { return _getTestBedRender3().createComponent(component); } static resetTestingModule(): TestBedStatic { _getTestBedRender3().resetTestingModule(); return TestBedRender3 as any as TestBedStatic; } // Properties platform: PlatformRef = null !; ngModule: Type|Type[] = null !; // metadata overrides private _moduleOverrides: [Type, MetadataOverride][] = []; private _componentOverrides: [Type, MetadataOverride][] = []; private _directiveOverrides: [Type, MetadataOverride][] = []; private _pipeOverrides: [Type, MetadataOverride][] = []; private _providerOverrides: Provider[] = []; private _rootProviderOverrides: Provider[] = []; // test module configuration private _providers: Provider[] = []; private _declarations: Array|any[]|any> = []; private _imports: Array|any[]|any> = []; private _schemas: Array = []; private _activeFixtures: ComponentFixture[] = []; private _moduleRef: NgModuleRef = null !; private _instantiated: boolean = false; /** * Initialize the environment for testing with a compiler factory, a PlatformRef, and an * angular module. These are common to every test in the suite. * * This may only be called once, to set up the common providers for the current test * suite on the current platform. If you absolutely need to change the providers, * first use `resetTestEnvironment`. * * Test modules and platforms for individual platforms are available from * '@angular//testing'. * * @experimental */ initTestEnvironment( ngModule: Type|Type[], platform: PlatformRef, aotSummaries?: () => any[]): void { if (this.platform || this.ngModule) { throw new Error('Cannot set base providers because it has already been called'); } this.platform = platform; this.ngModule = ngModule; } /** * Reset the providers for the test injector. * * @experimental */ resetTestEnvironment(): void { this.resetTestingModule(); this.platform = null !; this.ngModule = null !; } resetTestingModule(): void { // reset metadata overrides this._moduleOverrides = []; this._componentOverrides = []; this._directiveOverrides = []; this._pipeOverrides = []; this._providerOverrides = []; this._rootProviderOverrides = []; // reset test module config this._providers = []; this._declarations = []; this._imports = []; this._schemas = []; this._moduleRef = null !; this._instantiated = false; this._activeFixtures.forEach((fixture) => { try { fixture.destroy(); } catch (e) { console.error('Error during cleanup of component', { component: fixture.componentInstance, stacktrace: e, }); } }); this._activeFixtures = []; } configureCompiler(config: {providers?: any[]; useJit?: boolean;}): void { throw new Error('the Render3 compiler is not configurable !'); } configureTestingModule(moduleDef: TestModuleMetadata): void { this._assertNotInstantiated('R3TestBed.configureTestingModule', 'configure the test module'); if (moduleDef.providers) { this._providers.push(...moduleDef.providers); } if (moduleDef.declarations) { this._declarations.push(...moduleDef.declarations); } if (moduleDef.imports) { this._imports.push(...moduleDef.imports); } if (moduleDef.schemas) { this._schemas.push(...moduleDef.schemas); } } // TODO(vicb): implement compileComponents(): Promise { throw new Error('Render3TestBed.compileComponents is not implemented yet'); } get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { this._initIfNeeded(); if (token === TestBedRender3) { return this; } return this._moduleRef.injector.get(token, notFoundValue); } execute(tokens: any[], fn: Function, context?: any): any { this._initIfNeeded(); const params = tokens.map(t => this.get(t)); return fn.apply(context, params); } overrideModule(ngModule: Type, override: MetadataOverride): void { this._assertNotInstantiated('overrideModule', 'override module metadata'); this._moduleOverrides.push([ngModule, override]); } overrideComponent(component: Type, override: MetadataOverride): void { this._assertNotInstantiated('overrideComponent', 'override component metadata'); this._componentOverrides.push([component, override]); } overrideDirective(directive: Type, override: MetadataOverride): void { this._assertNotInstantiated('overrideDirective', 'override directive metadata'); this._directiveOverrides.push([directive, override]); } overridePipe(pipe: Type, override: MetadataOverride): void { this._assertNotInstantiated('overridePipe', 'override pipe metadata'); this._pipeOverrides.push([pipe, override]); } /** * Overwrites all providers for the given token with the given provider definition. */ overrideProvider(token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): void { const isRoot = (typeof token !== 'string' && token.ngInjectableDef && token.ngInjectableDef.providedIn === 'root'); const overrides = isRoot ? this._rootProviderOverrides : this._providerOverrides; if (provider.useFactory) { overrides.push({provide: token, useFactory: provider.useFactory, deps: provider.deps || []}); } else { overrides.push({provide: token, useValue: provider.useValue}); } } /** * Overwrites all providers for the given token with the given provider definition. * * @deprecated as it makes all NgModules lazy. Introduced only for migrating off of it. */ deprecatedOverrideProvider(token: any, provider: { useFactory: Function, deps: any[], }): void; deprecatedOverrideProvider(token: any, provider: {useValue: any;}): void; deprecatedOverrideProvider( token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): void { throw new Error('No implemented in IVY'); } createComponent(type: Type): ComponentFixture { this._initIfNeeded(); const testComponentRenderer: TestComponentRenderer = this.get(TestComponentRenderer); const rootElId = `root${_nextRootElementId++}`; testComponentRenderer.insertRootElement(rootElId); const componentDef = (type as any).ngComponentDef; if (!componentDef) { throw new Error( `It looks like '${stringify(type)}' has not been IVY compiled - it has no 'ngComponentDef' field`); } const componentFactory = new ComponentFactory(componentDef); const componentRef = componentFactory.create(Injector.NULL, [], `#${rootElId}`, this._moduleRef); const autoDetect: boolean = this.get(ComponentFixtureAutoDetect, false); const fixture = new ComponentFixture(componentRef, null, autoDetect); this._activeFixtures.push(fixture); return fixture; } // internal methods private _initIfNeeded(): void { if (this._instantiated) { return; } const resolvers = this._getResolvers(); const testModuleType = this._createTestModule(); compileNgModule(testModuleType, resolvers); const parentInjector = this.platform.injector; this._moduleRef = new NgModuleRef(testModuleType, parentInjector); this._instantiated = true; } // creates resolvers taking overrides into account private _getResolvers() { const module = new NgModuleResolver(); module.setOverrides(this._moduleOverrides); const component = new ComponentResolver(); component.setOverrides(this._componentOverrides); const directive = new DirectiveResolver(); directive.setOverrides(this._directiveOverrides); const pipe = new PipeResolver(); pipe.setOverrides(this._pipeOverrides); return {module, component, directive, pipe}; } private _assertNotInstantiated(methodName: string, methodDescription: string) { if (this._instantiated) { throw new Error( `Cannot ${methodDescription} when the test module has already been instantiated. ` + `Make sure you are not using \`inject\` before \`${methodName}\`.`); } } private _createTestModule(): Type { const rootProviderOverrides = this._rootProviderOverrides; const rendererFactoryWrapper = { provide: WRAP_RENDERER_FACTORY2, useFactory: () => (rf: RendererFactory2) => new Render3DebugRendererFactory2(rf), }; @NgModule({ providers: [...rootProviderOverrides, rendererFactoryWrapper], jit: true, }) class RootScopeModule { } const providers = [...this._providers, ...this._providerOverrides]; const declarations = this._declarations; const imports = [RootScopeModule, this.ngModule, this._imports]; const schemas = this._schemas; @NgModule({providers, declarations, imports, schemas, jit: true}) class DynamicTestModule { } return DynamicTestModule; } } let testBed: TestBedRender3; export function _getTestBedRender3(): TestBedRender3 { return testBed = testBed || new TestBedRender3(); } // Module compiler const EMPTY_ARRAY: Type[] = []; // Resolvers for Angular decorators type Resolvers = { module: Resolver, component: Resolver, directive: Resolver, pipe: Resolver, }; function compileNgModule(moduleType: Type, resolvers: Resolvers): void { const ngModule = resolvers.module.resolve(moduleType); if (ngModule === null) { throw new Error(`${stringify(moduleType)} has not @NgModule annotation`); } compileNgModuleDefs(moduleType, ngModule); const declarations: Type[] = flatten(ngModule.declarations || EMPTY_ARRAY); const compiledComponents: Type[] = []; // Compile the components, directives and pipes declared by this module declarations.forEach(declaration => { const component = resolvers.component.resolve(declaration); if (component) { compileComponent(declaration, component); compiledComponents.push(declaration); return; } const directive = resolvers.directive.resolve(declaration); if (directive) { compileDirective(declaration, directive); return; } const pipe = resolvers.pipe.resolve(declaration); if (pipe) { compilePipe(declaration, pipe); return; } }); // Compile transitive modules, components, directives and pipes const transitiveScope = transitiveScopesFor(moduleType, resolvers); compiledComponents.forEach( cmp => patchComponentDefWithScope((cmp as any).ngComponentDef, transitiveScope)); } /** * Compute the pair of transitive scopes (compilation scope and exported scope) for a given module. * * This operation is memoized and the result is cached on the module's definition. It can be called * on modules with components that have not fully compiled yet, but the result should not be used * until they have. */ function transitiveScopesFor( moduleType: Type, resolvers: Resolvers): NgModuleTransitiveScopes { if (!isNgModule(moduleType)) { throw new Error(`${moduleType.name} does not have an ngModuleDef`); } const def = moduleType.ngModuleDef; if (def.transitiveCompileScopes !== null) { return def.transitiveCompileScopes; } const scopes: NgModuleTransitiveScopes = { compilation: { directives: new Set(), pipes: new Set(), }, exported: { directives: new Set(), pipes: new Set(), }, }; def.declarations.forEach(declared => { const declaredWithDefs = declared as Type& { ngPipeDef?: any; }; if (declaredWithDefs.ngPipeDef !== undefined) { scopes.compilation.pipes.add(declared); } else { scopes.compilation.directives.add(declared); } }); def.imports.forEach((imported: Type) => { const ngModule = resolvers.module.resolve(imported); if (ngModule === null) { throw new Error(`Importing ${imported.name} which does not have an @ngModule`); } else { compileNgModule(imported, resolvers); } // When this module imports another, the imported module's exported directives and pipes are // added to the compilation scope of this module. const importedScope = transitiveScopesFor(imported, resolvers); importedScope.exported.directives.forEach(entry => scopes.compilation.directives.add(entry)); importedScope.exported.pipes.forEach(entry => scopes.compilation.pipes.add(entry)); }); def.exports.forEach((exported: Type) => { const exportedTyped = exported as Type& { // Components, Directives, NgModules, and Pipes can all be exported. ngComponentDef?: any; ngDirectiveDef?: any; ngModuleDef?: NgModuleDefInternal; ngPipeDef?: any; }; // Either the type is a module, a pipe, or a component/directive (which may not have an // ngComponentDef as it might be compiled asynchronously). if (isNgModule(exportedTyped)) { // When this module exports another, the exported module's exported directives and pipes are // added to both the compilation and exported scopes of this module. const exportedScope = transitiveScopesFor(exportedTyped, resolvers); exportedScope.exported.directives.forEach(entry => { scopes.compilation.directives.add(entry); scopes.exported.directives.add(entry); }); exportedScope.exported.pipes.forEach(entry => { scopes.compilation.pipes.add(entry); scopes.exported.pipes.add(entry); }); } else if (exportedTyped.ngPipeDef !== undefined) { scopes.exported.pipes.add(exportedTyped); } else { scopes.exported.directives.add(exportedTyped); } }); def.transitiveCompileScopes = scopes; return scopes; } function flatten(values: any[]): T[] { const out: T[] = []; values.forEach(value => { if (Array.isArray(value)) { out.push(...flatten(value)); } else { out.push(value); } }); return out; } function isNgModule(value: Type): value is Type&{ngModuleDef: NgModuleDefInternal} { return (value as{ngModuleDef?: NgModuleDefInternal}).ngModuleDef !== undefined; }