/** * @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 {ApplicationInitStatus, CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, SchemaMetadata, SkipSelf, StaticProvider, Type, ɵDepFlags as DepFlags, ɵNodeFlags as NodeFlags, ɵclearOverrides as clearOverrides, ɵgetComponentViewDefinitionFactory as getComponentViewDefinitionFactory, ɵoverrideComponentView as overrideComponentView, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core'; import {AsyncTestCompleter} from './async_test_completer'; import {ComponentFixture} from './component_fixture'; import {MetadataOverride} from './metadata_override'; import {TestingCompiler, TestingCompilerFactory} from './test_compiler'; const UNDEFINED = new Object(); /** * An abstract class for inserting the root test component element in a platform independent way. * * @experimental */ export class TestComponentRenderer { insertRootElement(rootElementId: string) {} } let _nextRootElementId = 0; /** * @experimental */ export const ComponentFixtureAutoDetect = new InjectionToken('ComponentFixtureAutoDetect'); /** * @experimental */ export const ComponentFixtureNoNgZone = new InjectionToken('ComponentFixtureNoNgZone'); /** * @experimental */ export type TestModuleMetadata = { providers?: any[], declarations?: any[], imports?: any[], schemas?: Array, aotSummaries?: () => any[], }; /** * @whatItDoes Configures and initializes environment for unit testing and provides methods for * creating components and services in unit tests. * @description * * TestBed is the primary api for writing unit tests for Angular applications and libraries. * * @stable */ export class TestBed implements Injector { /** * 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 = getTestBed(); testBed.initTestEnvironment(ngModule, platform, aotSummaries); return testBed; } /** * Reset the providers for the test injector. * * @experimental */ static resetTestEnvironment() { getTestBed().resetTestEnvironment(); } static resetTestingModule(): typeof TestBed { getTestBed().resetTestingModule(); return TestBed; } /** * Allows overriding default compiler providers and settings * which are defined in test_injector.js */ static configureCompiler(config: {providers?: any[]; useJit?: boolean;}): typeof TestBed { getTestBed().configureCompiler(config); return TestBed; } /** * Allows overriding default providers, directives, pipes, modules of the test injector, * which are defined in test_injector.js */ static configureTestingModule(moduleDef: TestModuleMetadata): typeof TestBed { getTestBed().configureTestingModule(moduleDef); return TestBed; } /** * 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 getTestBed().compileComponents(); } static overrideModule(ngModule: Type, override: MetadataOverride): typeof TestBed { getTestBed().overrideModule(ngModule, override); return TestBed; } static overrideComponent(component: Type, override: MetadataOverride): typeof TestBed { getTestBed().overrideComponent(component, override); return TestBed; } static overrideDirective(directive: Type, override: MetadataOverride): typeof TestBed { getTestBed().overrideDirective(directive, override); return TestBed; } static overridePipe(pipe: Type, override: MetadataOverride): typeof TestBed { getTestBed().overridePipe(pipe, override); return TestBed; } static overrideTemplate(component: Type, template: string): typeof TestBed { getTestBed().overrideComponent(component, {set: {template, templateUrl: null !}}); return TestBed; } /** * 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): typeof TestBed { getTestBed().overrideTemplateUsingTestingModule(component, template); return TestBed; } /** * Overwrites all providers for the given token with the given provider definition. * * Note: This works for JIT and AOTed components as well. */ static overrideProvider(token: any, provider: { useFactory: Function, deps: any[], }): typeof TestBed; static overrideProvider(token: any, provider: {useValue: any;}): typeof TestBed; static overrideProvider(token: any, provider: { useFactory?: Function, useValue?: any, deps?: any[], }): typeof TestBed { getTestBed().overrideProvider(token, provider as any); return TestBed; } /** * 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[], }): typeof TestBed { getTestBed().deprecatedOverrideProvider(token, provider as any); return TestBed; } static get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND) { return getTestBed().get(token, notFoundValue); } static createComponent(component: Type): ComponentFixture { return getTestBed().createComponent(component); } private _instantiated: boolean = false; private _compiler: TestingCompiler = null !; private _moduleRef: NgModuleRef = null !; private _moduleFactory: NgModuleFactory = null !; private _compilerOptions: CompilerOptions[] = []; private _moduleOverrides: [Type, MetadataOverride][] = []; private _componentOverrides: [Type, MetadataOverride][] = []; private _directiveOverrides: [Type, MetadataOverride][] = []; private _pipeOverrides: [Type, MetadataOverride][] = []; private _providers: Provider[] = []; private _declarations: Array|any[]|any> = []; private _imports: Array|any[]|any> = []; private _schemas: Array = []; private _activeFixtures: ComponentFixture[] = []; private _testEnvAotSummaries: () => any[] = () => []; private _aotSummaries: Array<() => any[]> = []; private _templateOverrides: Array<{component: Type, templateOf: Type}> = []; platform: PlatformRef = null !; ngModule: Type|Type[] = null !; /** * 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[]) { if (this.platform || this.ngModule) { throw new Error('Cannot set base providers because it has already been called'); } this.platform = platform; this.ngModule = ngModule; if (aotSummaries) { this._testEnvAotSummaries = aotSummaries; } } /** * Reset the providers for the test injector. * * @experimental */ resetTestEnvironment() { this.resetTestingModule(); this.platform = null !; this.ngModule = null !; this._testEnvAotSummaries = () => []; } resetTestingModule() { clearOverrides(); this._aotSummaries = []; this._templateOverrides = []; this._compiler = null !; this._moduleOverrides = []; this._componentOverrides = []; this._directiveOverrides = []; this._pipeOverrides = []; this._moduleRef = null !; this._moduleFactory = null !; this._compilerOptions = []; this._providers = []; this._declarations = []; this._imports = []; this._schemas = []; 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}) { this._assertNotInstantiated('TestBed.configureCompiler', 'configure the compiler'); this._compilerOptions.push(config); } configureTestingModule(moduleDef: TestModuleMetadata) { this._assertNotInstantiated('TestBed.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); } if (moduleDef.aotSummaries) { this._aotSummaries.push(moduleDef.aotSummaries); } } compileComponents(): Promise { if (this._moduleFactory || this._instantiated) { return Promise.resolve(null); } const moduleType = this._createCompilerAndModule(); return this._compiler.compileModuleAndAllComponentsAsync(moduleType) .then((moduleAndComponentFactories) => { this._moduleFactory = moduleAndComponentFactories.ngModuleFactory; }); } private _initIfNeeded() { if (this._instantiated) { return; } if (!this._moduleFactory) { try { const moduleType = this._createCompilerAndModule(); this._moduleFactory = this._compiler.compileModuleAndAllComponentsSync(moduleType).ngModuleFactory; } catch (e) { const errorCompType = this._compiler.getComponentFromError(e); if (errorCompType) { throw new Error( `This test module uses the component ${stringify(errorCompType)} which is using a "templateUrl" or "styleUrls", but they were never compiled. ` + `Please call "TestBed.compileComponents" before your test.`); } else { throw e; } } } for (const {component, templateOf} of this._templateOverrides) { const compFactory = this._compiler.getComponentFactory(templateOf); overrideComponentView(component, compFactory); } const ngZone = new NgZone({enableLongStackTrace: true}); const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}]; const ngZoneInjector = Injector.create({ providers: providers, parent: this.platform.injector, name: this._moduleFactory.moduleType.name }); this._moduleRef = this._moduleFactory.create(ngZoneInjector); // ApplicationInitStatus.runInitializers() is marked @internal to core. So casting to any // before accessing it. (this._moduleRef.injector.get(ApplicationInitStatus) as any).runInitializers(); this._instantiated = true; } private _createCompilerAndModule(): Type { const providers = this._providers.concat([{provide: TestBed, useValue: this}]); const declarations = [...this._declarations, ...this._templateOverrides.map(entry => entry.templateOf)]; const imports = [this.ngModule, this._imports]; const schemas = this._schemas; @NgModule({providers, declarations, imports, schemas}) class DynamicTestModule { } const compilerFactory: TestingCompilerFactory = this.platform.injector.get(TestingCompilerFactory); this._compiler = compilerFactory.createTestingCompiler(this._compilerOptions); for (const summary of [this._testEnvAotSummaries, ...this._aotSummaries]) { this._compiler.loadAotSummaries(summary); } this._moduleOverrides.forEach((entry) => this._compiler.overrideModule(entry[0], entry[1])); this._componentOverrides.forEach( (entry) => this._compiler.overrideComponent(entry[0], entry[1])); this._directiveOverrides.forEach( (entry) => this._compiler.overrideDirective(entry[0], entry[1])); this._pipeOverrides.forEach((entry) => this._compiler.overridePipe(entry[0], entry[1])); return DynamicTestModule; } 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}\`.`); } } get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND) { this._initIfNeeded(); if (token === TestBed) { return this; } // Tests can inject things from the ng module and from the compiler, // but the ng module can't inject things from the compiler and vice versa. const result = this._moduleRef.injector.get(token, UNDEFINED); return result === UNDEFINED ? this._compiler.injector.get(token, notFoundValue) : result; } 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, deps: any[], }): void; overrideProvider(token: any, provider: {useValue: any;}): void; overrideProvider(token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): void { this.overrideProviderImpl(token, provider); } /** * 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 { this.overrideProviderImpl(token, provider, /* deprecated */ true); } private overrideProviderImpl( token: any, provider: { useFactory?: Function, useValue?: any, deps?: any[], }, deprecated = false): void { let flags: NodeFlags = 0; let value: any; if (provider.useFactory) { flags |= NodeFlags.TypeFactoryProvider; value = provider.useFactory; } else { flags |= NodeFlags.TypeValueProvider; value = provider.useValue; } const deps = (provider.deps || []).map((dep) => { let depFlags: DepFlags = DepFlags.None; let depToken: any; if (Array.isArray(dep)) { dep.forEach((entry: any) => { if (entry instanceof Optional) { depFlags |= DepFlags.Optional; } else if (entry instanceof SkipSelf) { depFlags |= DepFlags.SkipSelf; } else { depToken = entry; } }); } else { depToken = dep; } return [depFlags, depToken]; }); overrideProvider({token, flags, deps, value, deprecatedBehavior: deprecated}); } overrideTemplateUsingTestingModule(component: Type, template: string) { this._assertNotInstantiated('overrideTemplateUsingTestingModule', 'override template'); @Component({selector: 'empty', template}) class OverrideComponent { } this._templateOverrides.push({component, templateOf: OverrideComponent}); } createComponent(component: Type): ComponentFixture { this._initIfNeeded(); const componentFactory = this._compiler.getComponentFactory(component); if (!componentFactory) { throw new Error( `Cannot create the component ${stringify(component)} as it was not imported into the testing module!`); } const noNgZone = this.get(ComponentFixtureNoNgZone, false); const autoDetect: boolean = this.get(ComponentFixtureAutoDetect, false); const ngZone: NgZone = noNgZone ? null : this.get(NgZone, null); const testComponentRenderer: TestComponentRenderer = this.get(TestComponentRenderer); const rootElId = `root${_nextRootElementId++}`; testComponentRenderer.insertRootElement(rootElId); const initComponent = () => { const componentRef = componentFactory.create(Injector.NULL, [], `#${rootElId}`, this._moduleRef); return new ComponentFixture(componentRef, ngZone, autoDetect); }; const fixture = !ngZone ? initComponent() : ngZone.run(initComponent); this._activeFixtures.push(fixture); return fixture; } } let _testBed: TestBed = null !; /** * @experimental */ export function getTestBed() { return _testBed = _testBed || new TestBed(); } /** * Allows injecting dependencies in `beforeEach()` and `it()`. * * Example: * * ``` * beforeEach(inject([Dependency, AClass], (dep, object) => { * // some code that uses `dep` and `object` * // ... * })); * * it('...', inject([AClass], (object) => { * object.doSomething(); * expect(...); * }) * ``` * * Notes: * - inject is currently a function because of some Traceur limitation the syntax should * eventually * becomes `it('...', @Inject (object: AClass, async: AsyncTestCompleter) => { ... });` * * @stable */ export function inject(tokens: any[], fn: Function): () => any { const testBed = getTestBed(); if (tokens.indexOf(AsyncTestCompleter) >= 0) { // Not using an arrow function to preserve context passed from call site return function() { // Return an async test method that returns a Promise if AsyncTestCompleter is one of // the injected tokens. return testBed.compileComponents().then(() => { const completer: AsyncTestCompleter = testBed.get(AsyncTestCompleter); testBed.execute(tokens, fn, this); return completer.promise; }); }; } else { // Not using an arrow function to preserve context passed from call site return function() { return testBed.execute(tokens, fn, this); }; } } /** * @experimental */ export class InjectSetupWrapper { constructor(private _moduleDef: () => TestModuleMetadata) {} private _addModule() { const moduleDef = this._moduleDef(); if (moduleDef) { getTestBed().configureTestingModule(moduleDef); } } inject(tokens: any[], fn: Function): () => any { const self = this; // Not using an arrow function to preserve context passed from call site return function() { self._addModule(); return inject(tokens, fn).call(this); }; } } /** * @experimental */ export function withModule(moduleDef: TestModuleMetadata): InjectSetupWrapper; export function withModule(moduleDef: TestModuleMetadata, fn: Function): () => any; export function withModule(moduleDef: TestModuleMetadata, fn?: Function | null): (() => any)| InjectSetupWrapper { if (fn) { // Not using an arrow function to preserve context passed from call site return function() { const testBed = getTestBed(); if (moduleDef) { testBed.configureTestingModule(moduleDef); } return fn.apply(this); }; } return new InjectSetupWrapper(() => moduleDef); }