diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index ccbc5b40c2..7766ebde0b 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, ErrorHandler, Inject, InjectionToken, NgModule, Optional, Pipe} from '@angular/core'; +import {ResourceLoader} from '@angular/compiler'; +import {Component, Directive, ErrorHandler, Inject, InjectionToken, NgModule, Optional, Pipe, ɵNG_COMPONENT_DEF as NG_COMPONENT_DEF} from '@angular/core'; import {TestBed, getTestBed} from '@angular/core/testing/src/test_bed'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -319,18 +320,45 @@ describe('TestBed', () => { class ComponentWithNoAnnotations extends SomeComponent {} - TestBed.configureTestingModule({declarations: [ComponentWithNoAnnotations]}); + @Directive({selector: 'some-directive'}) + class SomeDirective { + } + + class DirectiveWithNoAnnotations extends SomeDirective {} + + @Pipe({name: 'some-pipe'}) + class SomePipe { + } + + class PipeWithNoAnnotations extends SomePipe {} + + TestBed.configureTestingModule({ + declarations: [ + ComponentWithNoAnnotations, DirectiveWithNoAnnotations, PipeWithNoAnnotations + ] + }); TestBed.createComponent(ComponentWithNoAnnotations); expect(ComponentWithNoAnnotations.hasOwnProperty('ngComponentDef')).toBeTruthy(); expect(SomeComponent.hasOwnProperty('ngComponentDef')).toBeTruthy(); + expect(DirectiveWithNoAnnotations.hasOwnProperty('ngDirectiveDef')).toBeTruthy(); + expect(SomeDirective.hasOwnProperty('ngDirectiveDef')).toBeTruthy(); + + expect(PipeWithNoAnnotations.hasOwnProperty('ngPipeDef')).toBeTruthy(); + expect(SomePipe.hasOwnProperty('ngPipeDef')).toBeTruthy(); + TestBed.resetTestingModule(); + // ng defs should be removed from classes with no annotations expect(ComponentWithNoAnnotations.hasOwnProperty('ngComponentDef')).toBeFalsy(); + expect(DirectiveWithNoAnnotations.hasOwnProperty('ngDirectiveDef')).toBeFalsy(); + expect(PipeWithNoAnnotations.hasOwnProperty('ngPipeDef')).toBeFalsy(); - // ngComponentDef should be preserved on super component + // ng defs should be preserved on super types expect(SomeComponent.hasOwnProperty('ngComponentDef')).toBeTruthy(); + expect(SomeDirective.hasOwnProperty('ngDirectiveDef')).toBeTruthy(); + expect(SomePipe.hasOwnProperty('ngPipeDef')).toBeTruthy(); }); }); }); diff --git a/packages/core/testing/src/r3_test_bed.ts b/packages/core/testing/src/r3_test_bed.ts index 740629b87b..5c987de4c3 100644 --- a/packages/core/testing/src/r3_test_bed.ts +++ b/packages/core/testing/src/r3_test_bed.ts @@ -11,72 +11,32 @@ // this statement only. // clang-format off import { - ApplicationInitStatus, - Compiler, Component, Directive, - ErrorHandler, Injector, - ModuleWithComponentFactories, NgModule, - NgModuleFactory, NgZone, Pipe, PlatformRef, - Provider, - SchemaMetadata, Type, - resolveForwardRef, - ɵInjectableDef as InjectableDef, - ɵNG_COMPONENT_DEF as NG_COMPONENT_DEF, - ɵNG_DIRECTIVE_DEF as NG_DIRECTIVE_DEF, - ɵNG_INJECTOR_DEF as NG_INJECTOR_DEF, - ɵNG_MODULE_DEF as NG_MODULE_DEF, - ɵNG_PIPE_DEF as NG_PIPE_DEF, - ɵNgModuleDef as NgModuleDef, - ɵNgModuleFactory as R3NgModuleFactory, - ɵNgModuleType as NgModuleType, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, - ɵcompileComponent as compileComponent, - ɵcompileDirective as compileDirective, - ɵcompileNgModuleDefs as compileNgModuleDefs, - ɵcompilePipe as compilePipe, - ɵgetInjectableDef as getInjectableDef, ɵflushModuleScopingQueueAsMuchAsPossible as flushModuleScopingQueueAsMuchAsPossible, - ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵresetCompiledComponents as resetCompiledComponents, ɵstringify as stringify, - ɵtransitiveScopesFor as transitiveScopesFor, - CompilerOptions, - StaticProvider, - COMPILER_OPTIONS, - ɵDirectiveDef as DirectiveDef, } from '@angular/core'; // clang-format on -import {ResourceLoader} from '@angular/compiler'; -import {clearResolutionOfComponentResourcesQueue, componentNeedsResolution, resolveComponentResources, maybeQueueResolutionOfComponentResources, isComponentDefPendingResolution, restoreComponentResolutionQueue} from '../../src/metadata/resource_loading'; 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, ComponentFixtureNoNgZone, TestBedStatic, TestComponentRenderer, TestModuleMetadata} from './test_bed_common'; +import {R3TestBedCompiler} from './r3_test_bed_compiler'; let _nextRootElementId = 0; -const EMPTY_ARRAY: Type[] = []; - const UNDEFINED: Symbol = Symbol('UNDEFINED'); -// Resolvers for Angular decorators -type Resolvers = { - module: Resolver, - component: Resolver, - directive: Resolver, - pipe: Resolver, -}; - /** * @description * Configures and initializes environment for unit testing and provides methods for @@ -174,14 +134,6 @@ export class TestBedRender3 implements Injector, TestBed { return TestBedRender3 as any as TestBedStatic; } - overrideTemplateUsingTestingModule(component: Type, template: string): void { - if (this._instantiated) { - throw new Error( - 'Cannot override template when the test module has already been instantiated'); - } - this._templateOverrides.set(component, template); - } - static overrideProvider(token: any, provider: { useFactory: Function, deps: any[], @@ -233,41 +185,12 @@ export class TestBedRender3 implements Injector, TestBed { 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 _compilerProviders: StaticProvider[] = []; - private _rootProviderOverrides: Provider[] = []; - private _providerOverridesByToken: Map = new Map(); - private _templateOverrides: Map, string> = new Map(); - private _resolvers: Resolvers = null !; - - // test module configuration - private _providers: Provider[] = []; - private _compilerOptions: CompilerOptions[] = []; - private _declarations: Array|any[]|any> = []; - private _imports: Array|any[]|any> = []; - private _schemas: Array = []; + private _compiler: R3TestBedCompiler|null = null; + private _testModuleRef: NgModuleRef|null = null; private _activeFixtures: ComponentFixture[] = []; - - private _compilerInjector: Injector = null !; - private _moduleRef: NgModuleRef = null !; - private _testModuleType: NgModuleType = null !; - - private _instantiated: boolean = false; private _globalCompilationChecked = false; - private _originalComponentResolutionQueue: Map, Component>|null = null; - - // Map that keeps initial version of component/directive/pipe defs in case - // we compile a Type again, thus overriding respective static fields. This is - // required to make sure we restore defs to their initial states between test runs - private _initialNgDefs: Map, [string, PropertyDescriptor|undefined]> = new Map(); - /** * Initialize the environment for testing with a compiler factory, a PlatformRef, and an * angular module. These are common to every test in the suite. @@ -288,6 +211,7 @@ export class TestBedRender3 implements Injector, TestBed { } this.platform = platform; this.ngModule = ngModule; + this._compiler = new R3TestBedCompiler(this.platform, this.ngModule); } /** @@ -297,65 +221,20 @@ export class TestBedRender3 implements Injector, TestBed { */ resetTestEnvironment(): void { this.resetTestingModule(); + this._compiler = null; this.platform = null !; this.ngModule = null !; } resetTestingModule(): void { - this._checkGlobalCompilationFinished(); + this.checkGlobalCompilationFinished(); resetCompiledComponents(); - // reset metadata overrides - this._moduleOverrides = []; - this._componentOverrides = []; - this._directiveOverrides = []; - this._pipeOverrides = []; - this._providerOverrides = []; - this._rootProviderOverrides = []; - this._providerOverridesByToken.clear(); - this._templateOverrides.clear(); - this._resolvers = null !; - - // reset test module config - this._providers = []; - this._compilerOptions = []; - this._compilerProviders = []; - this._declarations = []; - this._imports = []; - this._schemas = []; - this._moduleRef = null !; - this._testModuleType = null !; - - this._compilerInjector = 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 = []; - - // restore initial component/directive/pipe defs - this._initialNgDefs.forEach((value: [string, PropertyDescriptor], type: Type) => { - const [prop, descriptor] = value; - if (!descriptor) { - // Delete operations are generally undesirable since they have performance implications on - // objects they were applied to. In this particular case, situations where this code is - // invoked should be quite rare to cause any noticable impact, since it's applied only to - // some test cases (for example when class with no annotations extends some @Component) when - // we need to clear 'ngComponentDef' field on a given class to restore its original state - // (before applying overrides and running tests). - delete (type as any)[prop]; - } else { - Object.defineProperty(type, prop, descriptor); - } - }); - this._initialNgDefs.clear(); - this._restoreComponentResolutionQueue(); + if (this._compiler !== null) { + this.compiler.restoreOriginalState(); + } + this._compiler = new R3TestBedCompiler(this.platform, this.ngModule); + this._testModuleRef = null; + this.destroyActiveFixtures(); } configureCompiler(config: {providers?: any[]; useJit?: boolean;}): void { @@ -363,126 +242,56 @@ export class TestBedRender3 implements Injector, TestBed { throw new Error('the Render3 compiler JiT mode is not configurable !'); } - if (config.providers) { - this._providerOverrides.push(...config.providers); - this._compilerProviders.push(...config.providers); + if (config.providers !== undefined) { + this.compiler.setCompilerProviders(config.providers); } } 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); - } + this.assertNotInstantiated('R3TestBed.configureTestingModule', 'configure the test module'); + this.compiler.configureTestingModule(moduleDef); } - compileComponents(): Promise { - this._clearComponentResolutionQueue(); - - const resolvers = this._getResolvers(); - const declarations: Type[] = flatten(this._declarations || EMPTY_ARRAY, resolveForwardRef); - - const componentOverrides: [Type, Component][] = []; - const providerOverrides: (() => void)[] = []; - let hasAsyncResources = false; - - // Compile the components declared by this module - // TODO(FW-1178): `compileComponents` should not duplicate `_compileNgModule` logic - declarations.forEach(declaration => { - const component = resolvers.component.resolve(declaration); - if (component) { - if (!declaration.hasOwnProperty(NG_COMPONENT_DEF) || - isComponentDefPendingResolution(declaration) || // - // Compiler provider overrides (like ResourceLoader) might affect the outcome of - // compilation, so we trigger `compileComponent` in case we have compilers overrides. - this._compilerProviders.length > 0 || - this._hasTypeOverrides(declaration, this._componentOverrides) || - this._hasTemplateOverrides(declaration)) { - this._storeNgDef(NG_COMPONENT_DEF, declaration); - // We make a copy of the metadata to ensure that we don't mutate the original metadata - const metadata = {...component}; - compileComponent(declaration, metadata); - componentOverrides.push([declaration, metadata]); - hasAsyncResources = hasAsyncResources || componentNeedsResolution(component); - } else if (this._hasProviderOverrides(component.providers)) { - // Queue provider override operations, since fetching ngComponentDef (to patch it) might - // trigger re-compilation, which will fail because component resources are not yet fully - // resolved at this moment. The queue is drained once all resources are resolved. - providerOverrides.push( - () => this._patchDefWithProviderOverrides(declaration, NG_COMPONENT_DEF)); - } - } - }); - - const overrideComponents = () => { - componentOverrides.forEach((override: [Type, Component]) => { - // Override the existing metadata, ensuring that the resolved resources - // are only available until the next TestBed reset (when `resetTestingModule` is called) - this.overrideComponent(override[0], {set: override[1]}); - }); - providerOverrides.forEach((overrideFn: () => void) => overrideFn()); - }; - - // If the component has no async resources (templateUrl, styleUrls), we can finish - // synchronously. This is important so that users who mistakenly treat `compileComponents` - // as synchronous don't encounter an error, as ViewEngine was tolerant of this. - if (!hasAsyncResources) { - overrideComponents(); - return Promise.resolve(); - } else { - let resourceLoader: ResourceLoader; - return resolveComponentResources(url => { - if (!resourceLoader) { - resourceLoader = this.compilerInjector.get(ResourceLoader); - } - return Promise.resolve(resourceLoader.get(url)); - }) - .then(overrideComponents); - } - } + compileComponents(): Promise { return this.compiler.compileComponents(); } get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { - this._initIfNeeded(); if (token === TestBedRender3) { return this; } - const result = this._moduleRef.injector.get(token, UNDEFINED); - return result === UNDEFINED ? this.compilerInjector.get(token, notFoundValue) : result; + const result = this.testModuleRef.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]); + this.assertNotInstantiated('overrideModule', 'override module metadata'); + this.compiler.overrideModule(ngModule, override); } overrideComponent(component: Type, override: MetadataOverride): void { - this._assertNotInstantiated('overrideComponent', 'override component metadata'); - this._componentOverrides.push([component, override]); + this.assertNotInstantiated('overrideComponent', 'override component metadata'); + this.compiler.overrideComponent(component, override); + } + + overrideTemplateUsingTestingModule(component: Type, template: string): void { + this.assertNotInstantiated( + 'R3TestBed.overrideTemplateUsingTestingModule', + 'Cannot override template when the test module has already been instantiated'); + this.compiler.overrideTemplateUsingTestingModule(component, template); } overrideDirective(directive: Type, override: MetadataOverride): void { - this._assertNotInstantiated('overrideDirective', 'override directive metadata'); - this._directiveOverrides.push([directive, override]); + this.assertNotInstantiated('overrideDirective', 'override directive metadata'); + this.compiler.overrideDirective(directive, override); } overridePipe(pipe: Type, override: MetadataOverride): void { - this._assertNotInstantiated('overridePipe', 'override pipe metadata'); - this._pipeOverrides.push([pipe, override]); + this.assertNotInstantiated('overridePipe', 'override pipe metadata'); + this.compiler.overridePipe(pipe, override); } /** @@ -490,21 +299,7 @@ export class TestBedRender3 implements Injector, TestBed { */ overrideProvider(token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): void { - const providerDef = provider.useFactory ? - {provide: token, useFactory: provider.useFactory, deps: provider.deps || []} : - {provide: token, useValue: provider.useValue}; - - let injectableDef: InjectableDef|null; - const isRoot = - (typeof token !== 'string' && (injectableDef = getInjectableDef(token)) && - injectableDef.providedIn === 'root'); - const overridesBucket = isRoot ? this._rootProviderOverrides : this._providerOverrides; - overridesBucket.push(providerDef); - - // keep all overrides grouped by token as well for fast lookups using token - const overridesForToken = this._providerOverridesByToken.get(token) || []; - overridesForToken.push(providerDef); - this._providerOverridesByToken.set(token, overridesForToken); + this.compiler.overrideProvider(token, provider); } /** @@ -532,8 +327,6 @@ export class TestBedRender3 implements Injector, TestBed { } createComponent(type: Type): ComponentFixture { - this._initIfNeeded(); - const testComponentRenderer: TestComponentRenderer = this.get(TestComponentRenderer); const rootElId = `root${_nextRootElementId++}`; testComponentRenderer.insertRootElement(rootElId); @@ -551,7 +344,7 @@ export class TestBedRender3 implements Injector, TestBed { const componentFactory = new ComponentFactory(componentDef); const initComponent = () => { const componentRef = - componentFactory.create(Injector.NULL, [], `#${rootElId}`, this._moduleRef); + componentFactory.create(Injector.NULL, [], `#${rootElId}`, this.testModuleRef); return new ComponentFixture(componentRef, ngZone, autoDetect); }; const fixture = ngZone ? ngZone.run(initComponent) : initComponent(); @@ -559,292 +352,28 @@ export class TestBedRender3 implements Injector, TestBed { return fixture; } - // internal methods - - private _initIfNeeded(): void { - this._checkGlobalCompilationFinished(); - if (this._instantiated) { - return; + private get compiler(): R3TestBedCompiler { + if (this._compiler === null) { + throw new Error(`Need to call TestBed.initTestEnvironment() first`); } - - this._resolvers = this._getResolvers(); - this._testModuleType = this._createTestModule(); - this._compileNgModule(this._testModuleType); - - const parentInjector = this.platform.injector; - this._moduleRef = new NgModuleRef(this._testModuleType, parentInjector); - - // ApplicationInitStatus.runInitializers() is marked @internal - // to core. Cast it to any before accessing it. - (this._moduleRef.injector.get(ApplicationInitStatus) as any).runInitializers(); - this._instantiated = true; + return this._compiler; } - private _storeNgDef(prop: string, type: Type) { - if (!this._initialNgDefs.has(type)) { - const currentDef = Object.getOwnPropertyDescriptor(type, prop); - this._initialNgDefs.set(type, [prop, currentDef]); + private get testModuleRef(): NgModuleRef { + if (this._testModuleRef === null) { + this._testModuleRef = this.compiler.finalize(); } + return this._testModuleRef; } - // get overrides for a specific provider (if any) - private _getProviderOverrides(provider: any) { - const token = provider && typeof provider === 'object' && provider.hasOwnProperty('provide') ? - provider.provide : - provider; - return this._providerOverridesByToken.get(token) || []; - } - - // 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) { + private assertNotInstantiated(methodName: string, methodDescription: string) { + if (this._testModuleRef !== null) { throw new Error( `Cannot ${methodDescription} when the test module has already been instantiated. ` + `Make sure you are not using \`inject\` before \`${methodName}\`.`); } } - private _createTestModule(): NgModuleType { - const rootProviderOverrides = this._rootProviderOverrides; - - @NgModule({ - providers: [...rootProviderOverrides], - jit: true, - }) - class RootScopeModule { - } - - @NgModule({providers: [{provide: ErrorHandler, useClass: R3TestErrorHandler}]}) - class R3ErrorHandlerModule { - } - - const ngZone = new NgZone({enableLongStackTrace: true}); - const providers = [ - {provide: NgZone, useValue: ngZone}, - {provide: Compiler, useFactory: () => new R3TestCompiler(this)}, - ...this._providers, - ...this._providerOverrides, - ]; - - // We need to provide the `R3ErrorHandlerModule` after the consumer's NgModule so that we can - // override the default ErrorHandler, if the consumer didn't pass in a custom one. - const imports = [RootScopeModule, this.ngModule, R3ErrorHandlerModule, this._imports]; - const declarations = this._declarations; - const schemas = this._schemas; - - @NgModule({providers, declarations, imports, schemas, jit: true}) - class DynamicTestModule { - } - - return DynamicTestModule as NgModuleType; - } - - get compilerInjector(): Injector { - if (this._compilerInjector !== null) { - return this._compilerInjector; - } - - const providers: StaticProvider[] = []; - const compilerOptions = this.platform.injector.get(COMPILER_OPTIONS); - compilerOptions.forEach(opts => { - if (opts.providers) { - providers.push(opts.providers); - } - }); - providers.push(...this._compilerProviders); - - // TODO(ocombe): make this work with an Injector directly instead of creating a module for it - @NgModule({providers}) - class CompilerModule { - } - - const CompilerModuleFactory = new R3NgModuleFactory(CompilerModule); - this._compilerInjector = CompilerModuleFactory.create(this.platform.injector).injector; - return this._compilerInjector; - } - - /** - * Clears current components resolution queue, but stores the state of the queue, so we can - * restore it later. Clearing the queue is required before we try to compile components (via - * `TestBed.compileComponents`), so that component defs are in sync with the resolution queue. - */ - private _clearComponentResolutionQueue() { - if (this._originalComponentResolutionQueue === null) { - this._originalComponentResolutionQueue = new Map(); - } - clearResolutionOfComponentResourcesQueue().forEach( - (value, key) => this._originalComponentResolutionQueue !.set(key, value)); - } - - /** - * Restores component resolution queue to the previously saved state. This operation is performed - * as a part of restoring the state after completion of the current set of tests (that might - * potentially mutate the state). - */ - private _restoreComponentResolutionQueue() { - if (this._originalComponentResolutionQueue !== null) { - restoreComponentResolutionQueue(this._originalComponentResolutionQueue); - this._originalComponentResolutionQueue = null; - } - } - - // TODO(FW-1179): define better types for all Provider-related operations, avoid using `any`. - private _getProvidersOverrides(providers: any): any[] { - if (!providers || !providers.length) return []; - // There are two flattening operations here. The inner flatten() operates on the metadata's - // providers and applies a mapping function which retrieves overrides for each incoming - // provider. The outer flatten() then flattens the produced overrides array. If this is not - // done, the array can contain other empty arrays (e.g. `[[], []]`) which leak into the - // providers array and contaminate any error messages that might be generated. - return flatten(flatten(providers, (provider: any) => this._getProviderOverrides(provider))); - } - - private _hasProviderOverrides(providers: any) { - return this._getProvidersOverrides(providers).length > 0; - } - - private _hasTypeOverrides(type: Type, overrides: [Type, MetadataOverride][]) { - return overrides.some((override: [Type, MetadataOverride]) => override[0] === type); - } - - private _hasTemplateOverrides(type: Type) { return this._templateOverrides.has(type); } - - private _getMetaWithOverrides(meta: Component|Directive|NgModule, type?: Type) { - const overrides: {providers?: any[], template?: string} = {}; - if (meta.providers && meta.providers.length) { - const providerOverrides = this._getProvidersOverrides(meta.providers); - if (providerOverrides.length) { - overrides.providers = [...meta.providers, ...providerOverrides]; - } - } - const hasTemplateOverride = !!type && this._templateOverrides.has(type); - if (hasTemplateOverride) { - overrides.template = this._templateOverrides.get(type !); - } - return Object.keys(overrides).length ? {...meta, ...overrides} : meta; - } - - private _patchDefWithProviderOverrides(declaration: Type, field: string) { - const def = (declaration as any)[field]; - if (def && def.providersResolver) { - this._storeNgDef(field, declaration); - const resolver = def.providersResolver; - const processProvidersFn = (providers: any[]) => { - const overrides = this._getProvidersOverrides(providers); - return [...providers, ...overrides]; - }; - def.providersResolver = (ngDef: DirectiveDef) => resolver(ngDef, processProvidersFn); - } - } - - /** - * @internal - */ - _getModuleResolver() { return this._resolvers.module; } - - /** - * @internal - */ - _compileNgModule(moduleType: NgModuleType): void { - const ngModule = this._resolvers.module.resolve(moduleType); - - if (ngModule === null) { - throw new Error(`${stringify(moduleType)} has no @NgModule annotation`); - } - - this._storeNgDef(NG_MODULE_DEF, moduleType); - this._storeNgDef(NG_INJECTOR_DEF, moduleType); - const metadata = this._getMetaWithOverrides(ngModule); - compileNgModuleDefs(moduleType, metadata); - - const declarations: Type[] = - flatten(ngModule.declarations || EMPTY_ARRAY, resolveForwardRef); - const declaredComponents: Type[] = []; - - // Compile the components, directives and pipes declared by this module - declarations.forEach(declaration => { - const component = this._resolvers.component.resolve(declaration); - if (component) { - if (!declaration.hasOwnProperty(NG_COMPONENT_DEF) || - this._hasTypeOverrides(declaration, this._componentOverrides) || - this._hasTemplateOverrides(declaration)) { - this._storeNgDef(NG_COMPONENT_DEF, declaration); - const metadata = this._getMetaWithOverrides(component, declaration); - compileComponent(declaration, metadata); - } else if (this._hasProviderOverrides(component.providers)) { - this._patchDefWithProviderOverrides(declaration, NG_COMPONENT_DEF); - } - declaredComponents.push(declaration); - return; - } - - const directive = this._resolvers.directive.resolve(declaration); - if (directive) { - if (!declaration.hasOwnProperty(NG_DIRECTIVE_DEF) || - this._hasTypeOverrides(declaration, this._directiveOverrides)) { - this._storeNgDef(NG_DIRECTIVE_DEF, declaration); - const metadata = this._getMetaWithOverrides(directive); - compileDirective(declaration, metadata); - } else if (this._hasProviderOverrides(directive.providers)) { - this._patchDefWithProviderOverrides(declaration, NG_DIRECTIVE_DEF); - } - return; - } - - const pipe = this._resolvers.pipe.resolve(declaration); - if (pipe) { - if (!declaration.hasOwnProperty(NG_PIPE_DEF) || - this._hasTypeOverrides(declaration, this._pipeOverrides)) { - this._storeNgDef(NG_PIPE_DEF, declaration); - compilePipe(declaration, pipe); - } - return; - } - }); - - // Compile transitive modules, components, directives and pipes - const calcTransitiveScopesFor = (moduleType: NgModuleType) => transitiveScopesFor( - moduleType, (ngModule: NgModuleType) => this._compileNgModule(ngModule)); - const transitiveScope = calcTransitiveScopesFor(moduleType); - declaredComponents.forEach(cmp => { - const scope = this._templateOverrides.has(cmp) ? - // if we have template override via `TestBed.overrideTemplateUsingTestingModule` - - // define Component scope as TestingModule scope, instead of the scope of NgModule - // where this Component was declared - // TODO: This is only a partial fix. Should be fixed completely with FW-1178 refactor. - transitiveScopesFor(this._testModuleType) : - transitiveScope; - patchComponentDefWithScope((cmp as any).ngComponentDef, scope); - }); - } - - /** - * @internal - */ - _getComponentFactories(moduleType: NgModuleType): ComponentFactory[] { - return maybeUnwrapFn(moduleType.ngModuleDef.declarations).reduce((factories, declaration) => { - const componentDef = (declaration as any).ngComponentDef; - componentDef && factories.push(new ComponentFactory(componentDef, this._moduleRef)); - return factories; - }, [] as ComponentFactory[]); - } - /** * Check whether the module scoping queue should be flushed, and flush it if needed. * @@ -857,14 +386,28 @@ export class TestBedRender3 implements Injector, TestBed { * is called whenever TestBed is initialized or reset. The _first_ time that this happens, prior * to any other operations, the scoping queue is flushed. */ - private _checkGlobalCompilationFinished(): void { - // !this._instantiated should not be necessary, but is left in as an additional guard that - // compilations queued in tests (after instantiation) are never flushed accidentally. - if (!this._globalCompilationChecked && !this._instantiated) { + private checkGlobalCompilationFinished(): void { + // Checking _testNgModuleRef is null should not be necessary, but is left in as an additional + // guard that compilations queued in tests (after instantiation) are never flushed accidentally. + if (!this._globalCompilationChecked && this._testModuleRef === null) { flushModuleScopingQueueAsMuchAsPossible(); } this._globalCompilationChecked = true; } + + private destroyActiveFixtures(): void { + this._activeFixtures.forEach((fixture) => { + try { + fixture.destroy(); + } catch (e) { + console.error('Error during cleanup of component', { + component: fixture.componentInstance, + stacktrace: e, + }); + } + }); + this._activeFixtures = []; + } } let testBed: TestBedRender3; @@ -872,68 +415,3 @@ let testBed: TestBedRender3; export function _getTestBedRender3(): TestBedRender3 { return testBed = testBed || new TestBedRender3(); } - -function flatten(values: any[], mapFn?: (value: T) => any): T[] { - const out: T[] = []; - values.forEach(value => { - if (Array.isArray(value)) { - out.push(...flatten(value, mapFn)); - } else { - out.push(mapFn ? mapFn(value) : value); - } - }); - return out; -} - -function isNgModule(value: Type): value is Type&{ngModuleDef: NgModuleDef} { - return (value as{ngModuleDef?: NgModuleDef}).ngModuleDef !== undefined; -} - -class R3TestCompiler implements Compiler { - constructor(private testBed: TestBedRender3) {} - - compileModuleSync(moduleType: Type): NgModuleFactory { - this.testBed._compileNgModule(moduleType as NgModuleType); - return new R3NgModuleFactory(moduleType); - } - - compileModuleAsync(moduleType: Type): Promise> { - return Promise.resolve(this.compileModuleSync(moduleType)); - } - - compileModuleAndAllComponentsSync(moduleType: Type): ModuleWithComponentFactories { - const ngModuleFactory = this.compileModuleSync(moduleType); - const componentFactories = this.testBed._getComponentFactories(moduleType as NgModuleType); - return new ModuleWithComponentFactories(ngModuleFactory, componentFactories); - } - - compileModuleAndAllComponentsAsync(moduleType: Type): - Promise> { - return Promise.resolve(this.compileModuleAndAllComponentsSync(moduleType)); - } - - clearCache(): void {} - - clearCacheFor(type: Type): void {} - - getModuleId(moduleType: Type): string|undefined { - const meta = this.testBed._getModuleResolver().resolve(moduleType); - return meta && meta.id || undefined; - } -} - -/** Error handler used for tests. Rethrows errors rather than logging them out. */ -class R3TestErrorHandler extends ErrorHandler { - handleError(error: any) { throw error; } -} - -/** - * Unwrap a value which might be behind a closure (for forward declaration reasons). - */ -function maybeUnwrapFn(value: T | (() => T)): T { - if (value instanceof Function) { - return value(); - } else { - return value; - } -} diff --git a/packages/core/testing/src/r3_test_bed_compiler.ts b/packages/core/testing/src/r3_test_bed_compiler.ts new file mode 100644 index 0000000000..aeae8e6ee4 --- /dev/null +++ b/packages/core/testing/src/r3_test_bed_compiler.ts @@ -0,0 +1,674 @@ +/** + * @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 + */ + +// clang-format off +import { + ApplicationInitStatus, + COMPILER_OPTIONS, + Compiler, + Component, + Directive, + ErrorHandler, + ModuleWithComponentFactories, + NgModule, + NgModuleFactory, + NgZone, + Injector, + Pipe, + PlatformRef, + Provider, + Type, + ɵcompileComponent as compileComponent, + ɵcompileDirective as compileDirective, + ɵcompileNgModuleDefs as compileNgModuleDefs, + ɵcompilePipe as compilePipe, + ɵgetInjectableDef as getInjectableDef, + ɵNG_COMPONENT_DEF as NG_COMPONENT_DEF, + ɵNG_DIRECTIVE_DEF as NG_DIRECTIVE_DEF, + ɵNG_INJECTOR_DEF as NG_INJECTOR_DEF, + ɵNG_MODULE_DEF as NG_MODULE_DEF, + ɵNG_PIPE_DEF as NG_PIPE_DEF, + ɵRender3ComponentFactory as ComponentFactory, + ɵRender3NgModuleRef as NgModuleRef, + ɵInjectableDef as InjectableDef, + ɵNgModuleFactory as R3NgModuleFactory, + ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, + ɵNgModuleType as NgModuleType, + ɵDirectiveDef as DirectiveDef, + ɵpatchComponentDefWithScope as patchComponentDefWithScope, + ɵtransitiveScopesFor as transitiveScopesFor, +} from '@angular/core'; +// clang-format on +import {ResourceLoader} from '@angular/compiler'; + +import {clearResolutionOfComponentResourcesQueue, restoreComponentResolutionQueue, resolveComponentResources, isComponentDefPendingResolution} from '../../src/metadata/resource_loading'; + +import {MetadataOverride} from './metadata_override'; +import {ComponentResolver, DirectiveResolver, NgModuleResolver, PipeResolver, Resolver} from './resolvers'; +import {TestModuleMetadata} from './test_bed_common'; + +const TESTING_MODULE = 'TestingModule'; +type TESTING_MODULE = typeof TESTING_MODULE; + +// Resolvers for Angular decorators +type Resolvers = { + module: Resolver, + component: Resolver, + directive: Resolver, + pipe: Resolver, +}; + +interface CleanupOperation { + field: string; + def: any; + original: unknown; +} + +export class R3TestBedCompiler { + private originalComponentResolutionQueue: Map, Component>|null = null; + + // Testing module configuration + private declarations: Type[] = []; + private imports: Type[] = []; + private providers: Provider[] = []; + private schemas: any[] = []; + + // Queues of components/directives/pipes that should be recompiled. + private pendingComponents = new Set>(); + private pendingDirectives = new Set>(); + private pendingPipes = new Set>(); + + // Keep track of all components and directives, so we can patch Providers onto defs later. + private seenComponents = new Set>(); + private seenDirectives = new Set>(); + + private resolvers: Resolvers = initResolvers(); + + private componentToModuleScope = new Map, Type|TESTING_MODULE>(); + + // Map that keeps initial version of component/directive/pipe defs in case + // we compile a Type again, thus overriding respective static fields. This is + // required to make sure we restore defs to their initial states between test runs + // TODO: we should support the case with multiple defs on a type + private initialNgDefs = new Map, [string, PropertyDescriptor|undefined]>(); + + // Array that keeps cleanup operations for initial versions of component/directive/pipe/module + // defs in case TestBed makes changes to the originals. + private defCleanupOps: CleanupOperation[] = []; + + private _injector: Injector|null = null; + private compilerProviders: Provider[]|null = null; + + private providerOverrides: Provider[] = []; + private rootProviderOverrides: Provider[] = []; + private providerOverridesByToken = new Map(); + + private testModuleType: NgModuleType; + private testModuleRef: NgModuleRef|null = null; + + constructor(private platform: PlatformRef, private additionalModuleTypes: Type|Type[]) { + class DynamicTestModule {} + this.testModuleType = DynamicTestModule as any; + } + + setCompilerProviders(providers: Provider[]|null): void { + this.compilerProviders = providers; + this._injector = null; + } + + configureTestingModule(moduleDef: TestModuleMetadata): void { + // Enqueue any compilation tasks for the directly declared component. + if (moduleDef.declarations !== undefined) { + this.queueTypeArray(moduleDef.declarations, TESTING_MODULE); + this.declarations.push(...moduleDef.declarations); + } + + // Enqueue any compilation tasks for imported modules. + if (moduleDef.imports !== undefined) { + this.queueTypesFromModulesArray(moduleDef.imports); + this.imports.push(...moduleDef.imports); + } + + if (moduleDef.providers !== undefined) { + this.providers.push(...moduleDef.providers); + } + + if (moduleDef.schemas !== undefined) { + this.schemas.push(...moduleDef.schemas); + } + } + + overrideModule(ngModule: Type, override: MetadataOverride): void { + // Compile the module right away. + this.resolvers.module.addOverride(ngModule, override); + const metadata = this.resolvers.module.resolve(ngModule); + if (metadata === null) { + throw new Error(`${ngModule.name} is not an @NgModule or is missing metadata`); + } + + this.recompileNgModule(ngModule); + + // At this point, the module has a valid .ngModuleDef, but the override may have introduced + // new declarations or imported modules. Ingest any possible new types and add them to the + // current queue. + this.queueTypesFromModulesArray([ngModule]); + } + + overrideComponent(component: Type, override: MetadataOverride): void { + this.resolvers.component.addOverride(component, override); + this.pendingComponents.add(component); + } + + overrideDirective(directive: Type, override: MetadataOverride): void { + this.resolvers.directive.addOverride(directive, override); + this.pendingDirectives.add(directive); + } + + overridePipe(pipe: Type, override: MetadataOverride): void { + this.resolvers.pipe.addOverride(pipe, override); + this.pendingPipes.add(pipe); + } + + overrideProvider(token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): + void { + const providerDef = provider.useFactory ? + {provide: token, useFactory: provider.useFactory, deps: provider.deps || []} : + {provide: token, useValue: provider.useValue}; + + let injectableDef: InjectableDef|null; + const isRoot = + (typeof token !== 'string' && (injectableDef = getInjectableDef(token)) && + injectableDef.providedIn === 'root'); + const overridesBucket = isRoot ? this.rootProviderOverrides : this.providerOverrides; + overridesBucket.push(providerDef); + + // Keep all overrides grouped by token as well for fast lookups using token + const overridesForToken = this.providerOverridesByToken.get(token) || []; + overridesForToken.push(providerDef); + this.providerOverridesByToken.set(token, overridesForToken); + } + + overrideTemplateUsingTestingModule(type: Type, template: string): void { + // In Ivy, compiling a component does not require knowing the module providing the component's + // scope, so overrideTemplateUsingTestingModule can be implemented purely via overrideComponent. + this.overrideComponent(type, {set: {template}}); + + // Set the component's scope to be the testing module. + this.componentToModuleScope.set(type, TESTING_MODULE); + } + + async compileComponents(): Promise { + this.clearComponentResolutionQueue(); + // Run compilers for all queued types. + let needsAsyncResources = this.compileTypesSync(); + + // compileComponents() should not be async unless it needs to be. + if (needsAsyncResources) { + let resourceLoader: ResourceLoader; + let resolver = (url: string): Promise => { + if (!resourceLoader) { + resourceLoader = this.injector.get(ResourceLoader); + } + return Promise.resolve(resourceLoader.get(url)); + }; + await resolveComponentResources(resolver); + } + } + + finalize(): NgModuleRef { + // One last compile + this.compileTypesSync(); + + // Create the testing module itself. + this.compileTestModule(); + + this.applyTransitiveScopes(); + + this.applyProviderOverrides(); + + // Clear the componentToModuleScope map, so that future compilations don't reset the scope of + // every component. + this.componentToModuleScope.clear(); + + const parentInjector = this.platform.injector; + this.testModuleRef = new NgModuleRef(this.testModuleType, parentInjector); + + + // ApplicationInitStatus.runInitializers() is marked @internal to core. + // Cast it to any before accessing it. + (this.testModuleRef.injector.get(ApplicationInitStatus) as any).runInitializers(); + + return this.testModuleRef; + } + + /** + * @internal + */ + _compileNgModuleSync(moduleType: Type): void { + this.queueTypesFromModulesArray([moduleType]); + this.compileTypesSync(); + this.applyProviderOverrides(); + this.applyProviderOverridesToModule(moduleType); + this.applyTransitiveScopes(); + } + + /** + * @internal + */ + async _compileNgModuleAsync(moduleType: Type): Promise { + this.queueTypesFromModulesArray([moduleType]); + await this.compileComponents(); + this.applyProviderOverrides(); + this.applyProviderOverridesToModule(moduleType); + this.applyTransitiveScopes(); + } + + /** + * @internal + */ + _getModuleResolver(): Resolver { return this.resolvers.module; } + + /** + * @internal + */ + _getComponentFactories(moduleType: NgModuleType): ComponentFactory[] { + return maybeUnwrapFn(moduleType.ngModuleDef.declarations).reduce((factories, declaration) => { + const componentDef = (declaration as any).ngComponentDef; + componentDef && factories.push(new ComponentFactory(componentDef, this.testModuleRef !)); + return factories; + }, [] as ComponentFactory[]); + } + + private compileTypesSync(): boolean { + // Compile all queued components, directives, pipes. + let needsAsyncResources = false; + this.pendingComponents.forEach(declaration => { + needsAsyncResources = needsAsyncResources || isComponentDefPendingResolution(declaration); + const metadata = this.resolvers.component.resolve(declaration) !; + this.maybeStoreNgDef(NG_COMPONENT_DEF, declaration); + compileComponent(declaration, metadata); + }); + this.pendingComponents.clear(); + + this.pendingDirectives.forEach(declaration => { + const metadata = this.resolvers.directive.resolve(declaration) !; + this.maybeStoreNgDef(NG_DIRECTIVE_DEF, declaration); + compileDirective(declaration, metadata); + }); + this.pendingDirectives.clear(); + + this.pendingPipes.forEach(declaration => { + const metadata = this.resolvers.pipe.resolve(declaration) !; + this.maybeStoreNgDef(NG_PIPE_DEF, declaration); + compilePipe(declaration, metadata); + }); + this.pendingPipes.clear(); + + return needsAsyncResources; + } + + private applyTransitiveScopes(): void { + const moduleToScope = new Map|TESTING_MODULE, NgModuleTransitiveScopes>(); + const getScopeOfModule = (moduleType: Type| TESTING_MODULE): NgModuleTransitiveScopes => { + if (!moduleToScope.has(moduleType)) { + const realType = moduleType === TESTING_MODULE ? this.testModuleType : moduleType; + moduleToScope.set(moduleType, transitiveScopesFor(realType)); + } + return moduleToScope.get(moduleType) !; + }; + + this.componentToModuleScope.forEach((moduleType, componentType) => { + const moduleScope = getScopeOfModule(moduleType); + this.storeFieldOfDefOnType(componentType, NG_COMPONENT_DEF, 'directiveDefs'); + this.storeFieldOfDefOnType(componentType, NG_COMPONENT_DEF, 'pipeDefs'); + patchComponentDefWithScope((componentType as any).ngComponentDef, moduleScope); + }); + + this.componentToModuleScope.clear(); + } + + private applyProviderOverrides(): void { + const maybeApplyOverrides = (field: string) => (type: Type) => { + const resolver = + field === NG_COMPONENT_DEF ? this.resolvers.component : this.resolvers.directive; + const metadata = resolver.resolve(type) !; + if (this.hasProviderOverrides(metadata.providers)) { + this.patchDefWithProviderOverrides(type, field); + } + }; + this.seenComponents.forEach(maybeApplyOverrides(NG_COMPONENT_DEF)); + this.seenDirectives.forEach(maybeApplyOverrides(NG_DIRECTIVE_DEF)); + + this.seenComponents.clear(); + this.seenDirectives.clear(); + } + // ... + private applyProviderOverridesToModule(moduleType: Type): void { + const injectorDef: any = (moduleType as any)[NG_INJECTOR_DEF]; + if (this.providerOverridesByToken.size > 0) { + if (this.hasProviderOverrides(injectorDef.providers)) { + this.maybeStoreNgDef(NG_INJECTOR_DEF, moduleType); + + this.storeFieldOfDefOnType(moduleType, NG_INJECTOR_DEF, 'providers'); + injectorDef.providers = [ + ...injectorDef.providers, // + ...this.getProviderOverrides(injectorDef.providers) + ]; + } + + // Apply provider overrides to imported modules recursively + const moduleDef: any = (moduleType as any)[NG_MODULE_DEF]; + for (const importType of moduleDef.imports) { + this.applyProviderOverridesToModule(importType); + } + } + } + + private queueTypeArray(arr: any[], moduleType: Type|TESTING_MODULE): void { + for (const value of arr) { + if (Array.isArray(value)) { + this.queueTypeArray(value, moduleType); + } else { + this.queueType(value, moduleType); + } + } + } + + private recompileNgModule(ngModule: Type): void { + const metadata = this.resolvers.module.resolve(ngModule); + if (metadata === null) { + throw new Error(`Unable to resolve metadata for NgModule: ${ngModule.name}`); + } + // Cache the initial ngModuleDef as it will be overwritten. + this.maybeStoreNgDef(NG_MODULE_DEF, ngModule); + this.maybeStoreNgDef(NG_INJECTOR_DEF, ngModule); + + compileNgModuleDefs(ngModule as NgModuleType, metadata); + } + + private queueType(type: Type, moduleType: Type|TESTING_MODULE): void { + const component = this.resolvers.component.resolve(type); + if (component) { + // Check whether a give Type has respective NG def (ngComponentDef) and compile if def is + // missing. That might happen in case a class without any Angular decorators extends another + // class where Component/Directive/Pipe decorator is defined. + if (isComponentDefPendingResolution(type) || !type.hasOwnProperty(NG_COMPONENT_DEF)) { + this.pendingComponents.add(type); + } + this.seenComponents.add(type); + + // Keep track of the module which declares this component, so later the component's scope + // can be set correctly. Only record this the first time, because it might be overridden by + // overrideTemplateUsingTestingModule. + if (!this.componentToModuleScope.has(type)) { + this.componentToModuleScope.set(type, moduleType); + } + return; + } + + const directive = this.resolvers.directive.resolve(type); + if (directive) { + if (!type.hasOwnProperty(NG_DIRECTIVE_DEF)) { + this.pendingDirectives.add(type); + } + this.seenDirectives.add(type); + return; + } + + const pipe = this.resolvers.pipe.resolve(type); + if (pipe && !type.hasOwnProperty(NG_PIPE_DEF)) { + this.pendingPipes.add(type); + return; + } + } + + private queueTypesFromModulesArray(arr: any[]): void { + for (const value of arr) { + if (Array.isArray(value)) { + this.queueTypesFromModulesArray(value); + } else if (hasNgModuleDef(value)) { + const def = value.ngModuleDef; + // Look through declarations, imports, and exports, and queue everything found there. + this.queueTypeArray(maybeUnwrapFn(def.declarations), value); + this.queueTypesFromModulesArray(maybeUnwrapFn(def.imports)); + this.queueTypesFromModulesArray(maybeUnwrapFn(def.exports)); + } + } + } + + private maybeStoreNgDef(prop: string, type: Type) { + if (!this.initialNgDefs.has(type)) { + const currentDef = Object.getOwnPropertyDescriptor(type, prop); + this.initialNgDefs.set(type, [prop, currentDef]); + } + } + + private storeFieldOfDefOnType(type: Type, defField: string, field: string): void { + const def: any = (type as any)[defField]; + const original: any = def[field]; + this.defCleanupOps.push({field, def, original}); + } + + /** + * Clears current components resolution queue, but stores the state of the queue, so we can + * restore it later. Clearing the queue is required before we try to compile components (via + * `TestBed.compileComponents`), so that component defs are in sync with the resolution queue. + */ + private clearComponentResolutionQueue() { + if (this.originalComponentResolutionQueue === null) { + this.originalComponentResolutionQueue = new Map(); + } + clearResolutionOfComponentResourcesQueue().forEach( + (value, key) => this.originalComponentResolutionQueue !.set(key, value)); + } + + /* + * Restores component resolution queue to the previously saved state. This operation is performed + * as a part of restoring the state after completion of the current set of tests (that might + * potentially mutate the state). + */ + private restoreComponentResolutionQueue() { + if (this.originalComponentResolutionQueue !== null) { + restoreComponentResolutionQueue(this.originalComponentResolutionQueue); + this.originalComponentResolutionQueue = null; + } + } + + restoreOriginalState(): void { + for (const op of this.defCleanupOps) { + op.def[op.field] = op.original; + } + // Restore initial component/directive/pipe defs + this.initialNgDefs.forEach((value: [string, PropertyDescriptor], type: Type) => { + const [prop, descriptor] = value; + if (!descriptor) { + // Delete operations are generally undesirable since they have performance implications + // on objects they were applied to. In this particular case, situations where this code is + // invoked should be quite rare to cause any noticable impact, since it's applied only to + // some test cases (for example when class with no annotations extends some @Component) + // when we need to clear 'ngComponentDef' field on a given class to restore its original + // state (before applying overrides and running tests). + delete (type as any)[prop]; + } else { + Object.defineProperty(type, prop, descriptor); + } + }); + this.initialNgDefs.clear(); + this.restoreComponentResolutionQueue(); + } + + private compileTestModule(): void { + const rootProviderOverrides = this.rootProviderOverrides; + + @NgModule({ + providers: [...rootProviderOverrides], + jit: true, + }) + class RootScopeModule { + } + + @NgModule({providers: [{provide: ErrorHandler, useClass: R3TestErrorHandler}]}) + class R3ErrorHandlerModule { + } + + const ngZone = new NgZone({enableLongStackTrace: true}); + const providers: Provider[] = [ + {provide: NgZone, useValue: ngZone}, + {provide: Compiler, useFactory: () => new R3TestCompiler(this)}, + ...this.providers, + ...this.providerOverrides, + ]; + const imports = + [RootScopeModule, this.additionalModuleTypes, R3ErrorHandlerModule, this.imports || []]; + + // clang-format off + compileNgModuleDefs(this.testModuleType, { + declarations: this.declarations, + imports, + schemas: this.schemas, + providers, + }); + // clang-format on + + this.applyProviderOverridesToModule(this.testModuleType); + } + + get injector(): Injector { + if (this._injector !== null) { + return this._injector; + } + + const providers: Provider[] = []; + const compilerOptions = this.platform.injector.get(COMPILER_OPTIONS); + compilerOptions.forEach(opts => { + if (opts.providers) { + providers.push(opts.providers); + } + }); + if (this.compilerProviders !== null) { + providers.push(...this.compilerProviders); + } + + // TODO(ocombe): make this work with an Injector directly instead of creating a module for it + @NgModule({providers}) + class CompilerModule { + } + + const CompilerModuleFactory = new R3NgModuleFactory(CompilerModule); + this._injector = CompilerModuleFactory.create(this.platform.injector).injector; + return this._injector; + } + + // get overrides for a specific provider (if any) + private getSingleProviderOverrides(provider: Provider&{provide?: any}): Provider[] { + const token = provider && typeof provider === 'object' && provider.hasOwnProperty('provide') ? + provider.provide : + provider; + return this.providerOverridesByToken.get(token) || []; + } + + private getProviderOverrides(providers?: Provider[]): Provider[] { + if (!providers || !providers.length || this.providerOverridesByToken.size === 0) return []; + // There are two flattening operations here. The inner flatten() operates on the metadata's + // providers and applies a mapping function which retrieves overrides for each incoming + // provider. The outer flatten() then flattens the produced overrides array. If this is not + // done, the array can contain other empty arrays (e.g. `[[], []]`) which leak into the + // providers array and contaminate any error messages that might be generated. + return flatten( + flatten(providers, (provider: Provider) => this.getSingleProviderOverrides(provider))); + } + + private hasProviderOverrides(providers?: Provider[]): boolean { + return this.getProviderOverrides(providers).length > 0; + } + + private patchDefWithProviderOverrides(declaration: Type, field: string): void { + const def = (declaration as any)[field]; + if (def && def.providersResolver) { + this.maybeStoreNgDef(field, declaration); + + const resolver = def.providersResolver; + const processProvidersFn = (providers: Provider[]) => { + const overrides = this.getProviderOverrides(providers); + return [...providers, ...overrides]; + }; + this.storeFieldOfDefOnType(declaration, field, 'providersResolver'); + def.providersResolver = (ngDef: DirectiveDef) => resolver(ngDef, processProvidersFn); + } + } +} + +function initResolvers(): Resolvers { + return { + module: new NgModuleResolver(), + component: new ComponentResolver(), + directive: new DirectiveResolver(), + pipe: new PipeResolver() + }; +} + +function hasNgModuleDef(value: Type): value is NgModuleType { + return value.hasOwnProperty('ngModuleDef'); +} + +function maybeUnwrapFn(maybeFn: (() => T) | T): T { + return maybeFn instanceof Function ? maybeFn() : maybeFn; +} + +function flatten(values: any[], mapFn?: (value: T) => any): T[] { + const out: T[] = []; + values.forEach(value => { + if (Array.isArray(value)) { + out.push(...flatten(value, mapFn)); + } else { + out.push(mapFn ? mapFn(value) : value); + } + }); + return out; +} + +/** Error handler used for tests. Rethrows errors rather than logging them out. */ +class R3TestErrorHandler extends ErrorHandler { + handleError(error: any) { throw error; } +} + +class R3TestCompiler implements Compiler { + constructor(private testBed: R3TestBedCompiler) {} + + compileModuleSync(moduleType: Type): NgModuleFactory { + this.testBed._compileNgModuleSync(moduleType); + return new R3NgModuleFactory(moduleType); + } + + async compileModuleAsync(moduleType: Type): Promise> { + await this.testBed._compileNgModuleAsync(moduleType); + return new R3NgModuleFactory(moduleType); + } + + compileModuleAndAllComponentsSync(moduleType: Type): ModuleWithComponentFactories { + const ngModuleFactory = this.compileModuleSync(moduleType); + const componentFactories = this.testBed._getComponentFactories(moduleType as NgModuleType); + return new ModuleWithComponentFactories(ngModuleFactory, componentFactories); + } + + async compileModuleAndAllComponentsAsync(moduleType: Type): + Promise> { + const ngModuleFactory = await this.compileModuleAsync(moduleType); + const componentFactories = this.testBed._getComponentFactories(moduleType as NgModuleType); + return new ModuleWithComponentFactories(ngModuleFactory, componentFactories); + } + + clearCache(): void {} + + clearCacheFor(type: Type): void {} + + getModuleId(moduleType: Type): string|undefined { + const meta = this.testBed._getModuleResolver().resolve(moduleType); + return meta && meta.id || undefined; + } +} \ No newline at end of file diff --git a/packages/core/testing/src/resolvers.ts b/packages/core/testing/src/resolvers.ts index 027929078e..6bf7456f92 100644 --- a/packages/core/testing/src/resolvers.ts +++ b/packages/core/testing/src/resolvers.ts @@ -16,7 +16,11 @@ const reflection = new ReflectionCapabilities(); /** * Base interface to resolve `@Component`, `@Directive`, `@Pipe` and `@NgModule`. */ -export interface Resolver { resolve(type: Type): T|null; } +export interface Resolver { + addOverride(type: Type, override: MetadataOverride): void; + setOverrides(overrides: Array<[Type, MetadataOverride]>): void; + resolve(type: Type): T|null; +} /** * Allows to override ivy metadata for tests (via the `TestBed`). @@ -27,13 +31,16 @@ abstract class OverrideResolver implements Resolver { abstract get type(): any; + addOverride(type: Type, override: MetadataOverride) { + const overrides = this.overrides.get(type) || []; + overrides.push(override); + this.overrides.set(type, overrides); + this.resolved.delete(type); + } + setOverrides(overrides: Array<[Type, MetadataOverride]>) { this.overrides.clear(); - overrides.forEach(([type, override]) => { - const overrides = this.overrides.get(type) || []; - overrides.push(override); - this.overrides.set(type, overrides); - }); + overrides.forEach(([type, override]) => { this.addOverride(type, override); }); } getAnnotation(type: Type): T|null { diff --git a/packages/platform-browser/test/testing_public_spec.ts b/packages/platform-browser/test/testing_public_spec.ts index 77abf5fba4..1b6de179ec 100644 --- a/packages/platform-browser/test/testing_public_spec.ts +++ b/packages/platform-browser/test/testing_public_spec.ts @@ -777,18 +777,28 @@ class CompWithUrlTemplate { describe('setting up the compiler', () => { describe('providers', () => { - beforeEach(() => { - const resourceLoaderGet = jasmine.createSpy('resourceLoaderGet') - .and.returnValue(Promise.resolve('Hello world!')); - TestBed.configureTestingModule({declarations: [CompWithUrlTemplate]}); - TestBed.configureCompiler( - {providers: [{provide: ResourceLoader, useValue: {get: resourceLoaderGet}}]}); - }); it('should use set up providers', fakeAsync(() => { + // Keeping this component inside the test is needed to make sure it's not resolved + // prior to this test, thus having ngComponentDef and a reference in resource + // resolution queue. This is done to check external resoution logic in isolation by + // configuring TestBed with the necessary ResourceLoader instance. + @Component({ + selector: 'comp', + templateUrl: '/base/angular/packages/platform-browser/test/static_assets/test.html' + }) + class InternalCompWithUrlTemplate { + } + + const resourceLoaderGet = jasmine.createSpy('resourceLoaderGet') + .and.returnValue(Promise.resolve('Hello world!')); + TestBed.configureTestingModule({declarations: [InternalCompWithUrlTemplate]}); + TestBed.configureCompiler( + {providers: [{provide: ResourceLoader, useValue: {get: resourceLoaderGet}}]}); + TestBed.compileComponents(); tick(); - const compFixture = TestBed.createComponent(CompWithUrlTemplate); + const compFixture = TestBed.createComponent(InternalCompWithUrlTemplate); expect(compFixture.nativeElement).toHaveText('Hello world!'); })); }); diff --git a/packages/platform-webworker/test/web_workers/worker/renderer_v2_integration_spec.ts b/packages/platform-webworker/test/web_workers/worker/renderer_v2_integration_spec.ts index 6d9ce656b4..a991d31d98 100644 --- a/packages/platform-webworker/test/web_workers/worker/renderer_v2_integration_spec.ts +++ b/packages/platform-webworker/test/web_workers/worker/renderer_v2_integration_spec.ts @@ -45,8 +45,7 @@ let lastCreatedRenderer: Renderer2; // UI side uiRenderStore = new RenderStore(); const uiInjector = new TestBed(); - uiInjector.platform = platformBrowserDynamicTesting(); - uiInjector.ngModule = BrowserTestingModule; + uiInjector.initTestEnvironment(BrowserTestingModule, platformBrowserDynamicTesting()); uiInjector.configureTestingModule({ providers: [ Serializer,