fix(ivy): TestBed rewrite to avoid unnecessary recompilations (#29483)
Prior to this change, Ivy version of TestBed was not designed to support the logic to avoid recompilations - most of the Components/Directives/Pipes were recompiled for each test, even if there were no overrides defined for a given Type. Additional checks to avoid recompilation were introduced in one of the previous commits (0244a2433e
), but there were still some corner cases that required attention. In order to support the necessary logic better, Ivy TestBed was rewritten/refactored. Main results of this rewrite are:
* no recompilation for Components/Directives/Pipes without overrides
* the logic to restore state between tests (isolate tests) was improved
* transitive scopes calculation no longer performs recompilation (it works with compiled defs)
As a result of these changes we see reduction in memory consumption (3.5-4x improvement) and pefromance increase (4-4.5x improvement).
PR Close #29483
This commit is contained in:
parent
fea2a0f2ac
commit
309ffe7e16
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<any>[] = [];
|
||||
|
||||
const UNDEFINED: Symbol = Symbol('UNDEFINED');
|
||||
|
||||
// Resolvers for Angular decorators
|
||||
type Resolvers = {
|
||||
module: Resolver<NgModule>,
|
||||
component: Resolver<Directive>,
|
||||
directive: Resolver<Component>,
|
||||
pipe: Resolver<Pipe>,
|
||||
};
|
||||
|
||||
/**
|
||||
* @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<any>, 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<any>|Type<any>[] = null !;
|
||||
|
||||
// metadata overrides
|
||||
private _moduleOverrides: [Type<any>, MetadataOverride<NgModule>][] = [];
|
||||
private _componentOverrides: [Type<any>, MetadataOverride<Component>][] = [];
|
||||
private _directiveOverrides: [Type<any>, MetadataOverride<Directive>][] = [];
|
||||
private _pipeOverrides: [Type<any>, MetadataOverride<Pipe>][] = [];
|
||||
private _providerOverrides: Provider[] = [];
|
||||
private _compilerProviders: StaticProvider[] = [];
|
||||
private _rootProviderOverrides: Provider[] = [];
|
||||
private _providerOverridesByToken: Map<any, Provider[]> = new Map();
|
||||
private _templateOverrides: Map<Type<any>, string> = new Map();
|
||||
private _resolvers: Resolvers = null !;
|
||||
|
||||
// test module configuration
|
||||
private _providers: Provider[] = [];
|
||||
private _compilerOptions: CompilerOptions[] = [];
|
||||
private _declarations: Array<Type<any>|any[]|any> = [];
|
||||
private _imports: Array<Type<any>|any[]|any> = [];
|
||||
private _schemas: Array<SchemaMetadata|any[]> = [];
|
||||
private _compiler: R3TestBedCompiler|null = null;
|
||||
private _testModuleRef: NgModuleRef<any>|null = null;
|
||||
|
||||
private _activeFixtures: ComponentFixture<any>[] = [];
|
||||
|
||||
private _compilerInjector: Injector = null !;
|
||||
private _moduleRef: NgModuleRef<any> = null !;
|
||||
private _testModuleType: NgModuleType<any> = null !;
|
||||
|
||||
private _instantiated: boolean = false;
|
||||
private _globalCompilationChecked = false;
|
||||
|
||||
private _originalComponentResolutionQueue: Map<Type<any>, 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<Type<any>, [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,
|
||||
});
|
||||
if (this._compiler !== null) {
|
||||
this.compiler.restoreOriginalState();
|
||||
}
|
||||
});
|
||||
this._activeFixtures = [];
|
||||
|
||||
// restore initial component/directive/pipe defs
|
||||
this._initialNgDefs.forEach((value: [string, PropertyDescriptor], type: Type<any>) => {
|
||||
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();
|
||||
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<any> {
|
||||
this._clearComponentResolutionQueue();
|
||||
|
||||
const resolvers = this._getResolvers();
|
||||
const declarations: Type<any>[] = flatten(this._declarations || EMPTY_ARRAY, resolveForwardRef);
|
||||
|
||||
const componentOverrides: [Type<any>, 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<any>, 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<any> { 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<any>, override: MetadataOverride<NgModule>): 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<any>, override: MetadataOverride<Component>): 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<any>, 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<any>, override: MetadataOverride<Directive>): 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<any>, override: MetadataOverride<Pipe>): 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<any>|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<T>(type: Type<T>): ComponentFixture<T> {
|
||||
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<any>(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`);
|
||||
}
|
||||
return this._compiler;
|
||||
}
|
||||
|
||||
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;
|
||||
private get testModuleRef(): NgModuleRef<any> {
|
||||
if (this._testModuleRef === null) {
|
||||
this._testModuleRef = this.compiler.finalize();
|
||||
}
|
||||
return this._testModuleRef;
|
||||
}
|
||||
|
||||
private _storeNgDef(prop: string, type: Type<any>) {
|
||||
if (!this._initialNgDefs.has(type)) {
|
||||
const currentDef = Object.getOwnPropertyDescriptor(type, prop);
|
||||
this._initialNgDefs.set(type, [prop, currentDef]);
|
||||
}
|
||||
}
|
||||
|
||||
// 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<any>, overrides: [Type<any>, MetadataOverride<any>][]) {
|
||||
return overrides.some((override: [Type<any>, MetadataOverride<any>]) => override[0] === type);
|
||||
}
|
||||
|
||||
private _hasTemplateOverrides(type: Type<any>) { return this._templateOverrides.has(type); }
|
||||
|
||||
private _getMetaWithOverrides(meta: Component|Directive|NgModule, type?: Type<any>) {
|
||||
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<any>, 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<any>) => 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<any>[] =
|
||||
flatten(ngModule.declarations || EMPTY_ARRAY, resolveForwardRef);
|
||||
const declaredComponents: Type<any>[] = [];
|
||||
|
||||
// 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<any>[] {
|
||||
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<any>[]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T>(values: any[], mapFn?: (value: T) => any): T[] {
|
||||
const out: T[] = [];
|
||||
values.forEach(value => {
|
||||
if (Array.isArray(value)) {
|
||||
out.push(...flatten<T>(value, mapFn));
|
||||
} else {
|
||||
out.push(mapFn ? mapFn(value) : value);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function isNgModule<T>(value: Type<T>): value is Type<T>&{ngModuleDef: NgModuleDef<T>} {
|
||||
return (value as{ngModuleDef?: NgModuleDef<T>}).ngModuleDef !== undefined;
|
||||
}
|
||||
|
||||
class R3TestCompiler implements Compiler {
|
||||
constructor(private testBed: TestBedRender3) {}
|
||||
|
||||
compileModuleSync<T>(moduleType: Type<T>): NgModuleFactory<T> {
|
||||
this.testBed._compileNgModule(moduleType as NgModuleType<T>);
|
||||
return new R3NgModuleFactory(moduleType);
|
||||
}
|
||||
|
||||
compileModuleAsync<T>(moduleType: Type<T>): Promise<NgModuleFactory<T>> {
|
||||
return Promise.resolve(this.compileModuleSync(moduleType));
|
||||
}
|
||||
|
||||
compileModuleAndAllComponentsSync<T>(moduleType: Type<T>): ModuleWithComponentFactories<T> {
|
||||
const ngModuleFactory = this.compileModuleSync(moduleType);
|
||||
const componentFactories = this.testBed._getComponentFactories(moduleType as NgModuleType<T>);
|
||||
return new ModuleWithComponentFactories(ngModuleFactory, componentFactories);
|
||||
}
|
||||
|
||||
compileModuleAndAllComponentsAsync<T>(moduleType: Type<T>):
|
||||
Promise<ModuleWithComponentFactories<T>> {
|
||||
return Promise.resolve(this.compileModuleAndAllComponentsSync(moduleType));
|
||||
}
|
||||
|
||||
clearCache(): void {}
|
||||
|
||||
clearCacheFor(type: Type<any>): void {}
|
||||
|
||||
getModuleId(moduleType: Type<any>): 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<T>(value: T | (() => T)): T {
|
||||
if (value instanceof Function) {
|
||||
return value();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<NgModule>,
|
||||
component: Resolver<Directive>,
|
||||
directive: Resolver<Component>,
|
||||
pipe: Resolver<Pipe>,
|
||||
};
|
||||
|
||||
interface CleanupOperation {
|
||||
field: string;
|
||||
def: any;
|
||||
original: unknown;
|
||||
}
|
||||
|
||||
export class R3TestBedCompiler {
|
||||
private originalComponentResolutionQueue: Map<Type<any>, Component>|null = null;
|
||||
|
||||
// Testing module configuration
|
||||
private declarations: Type<any>[] = [];
|
||||
private imports: Type<any>[] = [];
|
||||
private providers: Provider[] = [];
|
||||
private schemas: any[] = [];
|
||||
|
||||
// Queues of components/directives/pipes that should be recompiled.
|
||||
private pendingComponents = new Set<Type<any>>();
|
||||
private pendingDirectives = new Set<Type<any>>();
|
||||
private pendingPipes = new Set<Type<any>>();
|
||||
|
||||
// Keep track of all components and directives, so we can patch Providers onto defs later.
|
||||
private seenComponents = new Set<Type<any>>();
|
||||
private seenDirectives = new Set<Type<any>>();
|
||||
|
||||
private resolvers: Resolvers = initResolvers();
|
||||
|
||||
private componentToModuleScope = new Map<Type<any>, Type<any>|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<Type<any>, [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<any, Provider[]>();
|
||||
|
||||
private testModuleType: NgModuleType<any>;
|
||||
private testModuleRef: NgModuleRef<any>|null = null;
|
||||
|
||||
constructor(private platform: PlatformRef, private additionalModuleTypes: Type<any>|Type<any>[]) {
|
||||
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<any>, override: MetadataOverride<NgModule>): 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<any>, override: MetadataOverride<Component>): void {
|
||||
this.resolvers.component.addOverride(component, override);
|
||||
this.pendingComponents.add(component);
|
||||
}
|
||||
|
||||
overrideDirective(directive: Type<any>, override: MetadataOverride<Directive>): void {
|
||||
this.resolvers.directive.addOverride(directive, override);
|
||||
this.pendingDirectives.add(directive);
|
||||
}
|
||||
|
||||
overridePipe(pipe: Type<any>, override: MetadataOverride<Pipe>): 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<any>|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<any>, 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<void> {
|
||||
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<string> => {
|
||||
if (!resourceLoader) {
|
||||
resourceLoader = this.injector.get(ResourceLoader);
|
||||
}
|
||||
return Promise.resolve(resourceLoader.get(url));
|
||||
};
|
||||
await resolveComponentResources(resolver);
|
||||
}
|
||||
}
|
||||
|
||||
finalize(): NgModuleRef<any> {
|
||||
// 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<any>): void {
|
||||
this.queueTypesFromModulesArray([moduleType]);
|
||||
this.compileTypesSync();
|
||||
this.applyProviderOverrides();
|
||||
this.applyProviderOverridesToModule(moduleType);
|
||||
this.applyTransitiveScopes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _compileNgModuleAsync(moduleType: Type<any>): Promise<void> {
|
||||
this.queueTypesFromModulesArray([moduleType]);
|
||||
await this.compileComponents();
|
||||
this.applyProviderOverrides();
|
||||
this.applyProviderOverridesToModule(moduleType);
|
||||
this.applyTransitiveScopes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getModuleResolver(): Resolver<NgModule> { return this.resolvers.module; }
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_getComponentFactories(moduleType: NgModuleType): ComponentFactory<any>[] {
|
||||
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<any>[]);
|
||||
}
|
||||
|
||||
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<Type<any>|TESTING_MODULE, NgModuleTransitiveScopes>();
|
||||
const getScopeOfModule = (moduleType: Type<any>| 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<any>) => {
|
||||
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<any>): 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<any>|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<any>): 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<any>, metadata);
|
||||
}
|
||||
|
||||
private queueType(type: Type<any>, moduleType: Type<any>|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<any>) {
|
||||
if (!this.initialNgDefs.has(type)) {
|
||||
const currentDef = Object.getOwnPropertyDescriptor(type, prop);
|
||||
this.initialNgDefs.set(type, [prop, currentDef]);
|
||||
}
|
||||
}
|
||||
|
||||
private storeFieldOfDefOnType(type: Type<any>, 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<any>) => {
|
||||
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<any>, 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<any>) => resolver(ngDef, processProvidersFn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initResolvers(): Resolvers {
|
||||
return {
|
||||
module: new NgModuleResolver(),
|
||||
component: new ComponentResolver(),
|
||||
directive: new DirectiveResolver(),
|
||||
pipe: new PipeResolver()
|
||||
};
|
||||
}
|
||||
|
||||
function hasNgModuleDef<T>(value: Type<T>): value is NgModuleType<T> {
|
||||
return value.hasOwnProperty('ngModuleDef');
|
||||
}
|
||||
|
||||
function maybeUnwrapFn<T>(maybeFn: (() => T) | T): T {
|
||||
return maybeFn instanceof Function ? maybeFn() : maybeFn;
|
||||
}
|
||||
|
||||
function flatten<T>(values: any[], mapFn?: (value: T) => any): T[] {
|
||||
const out: T[] = [];
|
||||
values.forEach(value => {
|
||||
if (Array.isArray(value)) {
|
||||
out.push(...flatten<T>(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<T>(moduleType: Type<T>): NgModuleFactory<T> {
|
||||
this.testBed._compileNgModuleSync(moduleType);
|
||||
return new R3NgModuleFactory(moduleType);
|
||||
}
|
||||
|
||||
async compileModuleAsync<T>(moduleType: Type<T>): Promise<NgModuleFactory<T>> {
|
||||
await this.testBed._compileNgModuleAsync(moduleType);
|
||||
return new R3NgModuleFactory(moduleType);
|
||||
}
|
||||
|
||||
compileModuleAndAllComponentsSync<T>(moduleType: Type<T>): ModuleWithComponentFactories<T> {
|
||||
const ngModuleFactory = this.compileModuleSync(moduleType);
|
||||
const componentFactories = this.testBed._getComponentFactories(moduleType as NgModuleType<T>);
|
||||
return new ModuleWithComponentFactories(ngModuleFactory, componentFactories);
|
||||
}
|
||||
|
||||
async compileModuleAndAllComponentsAsync<T>(moduleType: Type<T>):
|
||||
Promise<ModuleWithComponentFactories<T>> {
|
||||
const ngModuleFactory = await this.compileModuleAsync(moduleType);
|
||||
const componentFactories = this.testBed._getComponentFactories(moduleType as NgModuleType<T>);
|
||||
return new ModuleWithComponentFactories(ngModuleFactory, componentFactories);
|
||||
}
|
||||
|
||||
clearCache(): void {}
|
||||
|
||||
clearCacheFor(type: Type<any>): void {}
|
||||
|
||||
getModuleId(moduleType: Type<any>): string|undefined {
|
||||
const meta = this.testBed._getModuleResolver().resolve(moduleType);
|
||||
return meta && meta.id || undefined;
|
||||
}
|
||||
}
|
|
@ -16,7 +16,11 @@ const reflection = new ReflectionCapabilities();
|
|||
/**
|
||||
* Base interface to resolve `@Component`, `@Directive`, `@Pipe` and `@NgModule`.
|
||||
*/
|
||||
export interface Resolver<T> { resolve(type: Type<any>): T|null; }
|
||||
export interface Resolver<T> {
|
||||
addOverride(type: Type<any>, override: MetadataOverride<T>): void;
|
||||
setOverrides(overrides: Array<[Type<any>, MetadataOverride<T>]>): void;
|
||||
resolve(type: Type<any>): T|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to override ivy metadata for tests (via the `TestBed`).
|
||||
|
@ -27,13 +31,16 @@ abstract class OverrideResolver<T> implements Resolver<T> {
|
|||
|
||||
abstract get type(): any;
|
||||
|
||||
setOverrides(overrides: Array<[Type<any>, MetadataOverride<T>]>) {
|
||||
this.overrides.clear();
|
||||
overrides.forEach(([type, override]) => {
|
||||
addOverride(type: Type<any>, override: MetadataOverride<T>) {
|
||||
const overrides = this.overrides.get(type) || [];
|
||||
overrides.push(override);
|
||||
this.overrides.set(type, overrides);
|
||||
});
|
||||
this.resolved.delete(type);
|
||||
}
|
||||
|
||||
setOverrides(overrides: Array<[Type<any>, MetadataOverride<T>]>) {
|
||||
this.overrides.clear();
|
||||
overrides.forEach(([type, override]) => { this.addOverride(type, override); });
|
||||
}
|
||||
|
||||
getAnnotation(type: Type<any>): T|null {
|
||||
|
|
|
@ -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!');
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue