From a4600669721494bc8a9647317a25dea5843a0bd1 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 27 Oct 2017 17:04:11 -0700 Subject: [PATCH] feat(compiler): introduce `TestBed.overrideTemplateUsingTestingModule` This allows to overwrite templates for JIT and AOT components alike. In contrast to `TestBed.overrideTemplate`, the template is compiled in the context of the testing module, allowing to use other testing directives. Closes #19815 --- packages/core/src/core_private_export.ts | 2 +- packages/core/src/view/entrypoint.ts | 12 +++-- packages/core/src/view/index.ts | 2 +- packages/core/src/view/services.ts | 35 ++++++++---- packages/core/src/view/types.ts | 8 ++- .../linker/jit_summaries_integration_spec.ts | 25 +++++++++ packages/core/testing/src/test_bed.ts | 38 +++++++++++-- .../test/testing_public_spec.ts | 54 +++++++++++++++++++ tools/public_api_guard/core/testing.d.ts | 15 +++--- 9 files changed, 166 insertions(+), 25 deletions(-) diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 33d36d7adb..bf1aace410 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -20,5 +20,5 @@ export {DirectRenderer as ɵDirectRenderer, RenderDebugInfo as ɵRenderDebugInfo export {global as ɵglobal, looseIdentical as ɵlooseIdentical, stringify as ɵstringify} from './util'; export {makeDecorator as ɵmakeDecorator} from './util/decorators'; export {isObservable as ɵisObservable, isPromise as ɵisPromise} from './util/lang'; -export {clearProviderOverrides as ɵclearProviderOverrides, overrideProvider as ɵoverrideProvider} from './view/index'; +export {clearOverrides as ɵclearOverrides, overrideComponentView as ɵoverrideComponentView, overrideProvider as ɵoverrideProvider} from './view/index'; export {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from './view/provider'; diff --git a/packages/core/src/view/entrypoint.ts b/packages/core/src/view/entrypoint.ts index f0355dba4e..7bd118e6cf 100644 --- a/packages/core/src/view/entrypoint.ts +++ b/packages/core/src/view/entrypoint.ts @@ -7,11 +7,12 @@ */ import {Injector} from '../di/injector'; +import {ComponentFactory} from '../linker/component_factory'; import {NgModuleFactory, NgModuleRef} from '../linker/ng_module_factory'; import {Type} from '../type'; import {initServicesIfNeeded} from './services'; -import {NgModuleDefinitionFactory, ProviderOverride, Services} from './types'; +import {NgModuleDefinitionFactory, ProviderOverride, Services, ViewDefinition} from './types'; import {resolveDefinition} from './util'; export function overrideProvider(override: ProviderOverride) { @@ -19,9 +20,14 @@ export function overrideProvider(override: ProviderOverride) { return Services.overrideProvider(override); } -export function clearProviderOverrides() { +export function overrideComponentView(comp: Type, componentFactory: ComponentFactory) { initServicesIfNeeded(); - return Services.clearProviderOverrides(); + return Services.overrideComponentView(comp, componentFactory); +} + +export function clearOverrides() { + initServicesIfNeeded(); + return Services.clearOverrides(); } // Attention: this function is called as top level function. diff --git a/packages/core/src/view/index.ts b/packages/core/src/view/index.ts index 4275d4490a..28b0947d26 100644 --- a/packages/core/src/view/index.ts +++ b/packages/core/src/view/index.ts @@ -7,7 +7,7 @@ */ export {anchorDef, elementDef} from './element'; -export {clearProviderOverrides, createNgModuleFactory, overrideProvider} from './entrypoint'; +export {clearOverrides, createNgModuleFactory, overrideComponentView, overrideProvider} from './entrypoint'; export {ngContentDef} from './ng_content'; export {moduleDef, moduleProvideDef} from './ng_module'; export {directiveDef, pipeDef, providerDef} from './provider'; diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index 6599e808f5..8ae5e30bc1 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -10,6 +10,7 @@ import {isDevMode} from '../application_ref'; import {DebugElement, DebugNode, EventListener, getDebugNode, indexDebugNode, removeDebugNodeFromIndex} from '../debug/debug_node'; import {Injector} from '../di'; import {ErrorHandler} from '../error_handler'; +import {ComponentFactory} from '../linker/component_factory'; import {NgModuleRef} from '../linker/ng_module_factory'; import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api'; import {Sanitizer} from '../security'; @@ -18,9 +19,9 @@ import {Type} from '../type'; import {isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors'; import {resolveDep} from './provider'; import {dirtyParentQueries, getQueryValue} from './query'; -import {createInjector, createNgModuleRef} from './refs'; +import {createInjector, createNgModuleRef, getComponentViewDefinitionFactory} from './refs'; import {ArgumentType, BindingFlags, CheckType, DebugContext, DepDef, ElementData, NgModuleDefinition, NgModuleProviderDef, NodeDef, NodeFlags, NodeLogger, ProviderOverride, RootData, Services, ViewData, ViewDefinition, ViewState, asElementData, asPureExpressionData} from './types'; -import {NOOP, isComponentView, renderNode, splitDepsDsl, viewParentEl} from './util'; +import {NOOP, isComponentView, renderNode, resolveDefinition, splitDepsDsl, viewParentEl} from './util'; import {checkAndUpdateNode, checkAndUpdateView, checkNoChangesNode, checkNoChangesView, createComponentView, createEmbeddedView, createRootView, destroyView} from './view'; @@ -38,7 +39,8 @@ export function initServicesIfNeeded() { Services.createComponentView = services.createComponentView; Services.createNgModuleRef = services.createNgModuleRef; Services.overrideProvider = services.overrideProvider; - Services.clearProviderOverrides = services.clearProviderOverrides; + Services.overrideComponentView = services.overrideComponentView; + Services.clearOverrides = services.clearOverrides; Services.checkAndUpdateView = services.checkAndUpdateView; Services.checkNoChangesView = services.checkNoChangesView; Services.destroyView = services.destroyView; @@ -58,7 +60,8 @@ function createProdServices() { createComponentView: createComponentView, createNgModuleRef: createNgModuleRef, overrideProvider: NOOP, - clearProviderOverrides: NOOP, + overrideComponentView: NOOP, + clearOverrides: NOOP, checkAndUpdateView: checkAndUpdateView, checkNoChangesView: checkNoChangesView, destroyView: destroyView, @@ -84,7 +87,8 @@ function createDebugServices() { createComponentView: debugCreateComponentView, createNgModuleRef: debugCreateNgModuleRef, overrideProvider: debugOverrideProvider, - clearProviderOverrides: debugClearProviderOverrides, + overrideComponentView: debugOverrideComponentView, + clearOverrides: debugClearOverrides, checkAndUpdateView: debugCheckAndUpdateView, checkNoChangesView: debugCheckNoChangesView, destroyView: debugDestroyView, @@ -139,10 +143,15 @@ function debugCreateEmbeddedView( function debugCreateComponentView( parentView: ViewData, nodeDef: NodeDef, viewDef: ViewDefinition, hostElement: any): ViewData { - const defWithOverride = applyProviderOverridesToView(viewDef); + const overrideComponentView = + viewDefOverrides.get(nodeDef.element !.componentProvider !.provider !.token); + if (overrideComponentView) { + viewDef = overrideComponentView; + } else { + viewDef = applyProviderOverridesToView(viewDef); + } return callWithDebugContext( - DebugAction.create, createComponentView, null, - [parentView, nodeDef, defWithOverride, hostElement]); + DebugAction.create, createComponentView, null, [parentView, nodeDef, viewDef, hostElement]); } function debugCreateNgModuleRef( @@ -153,13 +162,21 @@ function debugCreateNgModuleRef( } const providerOverrides = new Map(); +const viewDefOverrides = new Map(); function debugOverrideProvider(override: ProviderOverride) { providerOverrides.set(override.token, override); } -function debugClearProviderOverrides() { +function debugOverrideComponentView(comp: any, compFactory: ComponentFactory) { + const hostViewDef = resolveDefinition(getComponentViewDefinitionFactory(compFactory)); + const compViewDef = resolveDefinition(hostViewDef.nodes[0].element !.componentView !); + viewDefOverrides.set(comp, compViewDef); +} + +function debugClearOverrides() { providerOverrides.clear(); + viewDefOverrides.clear(); } // Notes about the algorithm: diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index 0a2a2ef030..1340815c44 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -8,6 +8,7 @@ import {Injector} from '../di'; import {ErrorHandler} from '../error_handler'; +import {ComponentFactory} from '../linker/component_factory'; import {NgModuleRef} from '../linker/ng_module_factory'; import {QueryList} from '../linker/query_list'; import {TemplateRef} from '../linker/template_ref'; @@ -16,6 +17,7 @@ import {Renderer2, RendererFactory2, RendererType2} from '../render/api'; import {Sanitizer, SecurityContext} from '../security'; import {Type} from '../type'; + // ------------------------------------- // Defs // ------------------------------------- @@ -522,7 +524,8 @@ export interface Services { moduleType: Type, parent: Injector, bootstrapComponents: Type[], def: NgModuleDefinition): NgModuleRef; overrideProvider(override: ProviderOverride): void; - clearProviderOverrides(): void; + overrideComponentView(compType: Type, compFactory: ComponentFactory): void; + clearOverrides(): void; checkAndUpdateView(view: ViewData): void; checkNoChangesView(view: ViewData): void; destroyView(view: ViewData): void; @@ -547,7 +550,8 @@ export const Services: Services = { createComponentView: undefined !, createNgModuleRef: undefined !, overrideProvider: undefined !, - clearProviderOverrides: undefined !, + overrideComponentView: undefined !, + clearOverrides: undefined !, checkAndUpdateView: undefined !, checkNoChangesView: undefined !, destroyView: undefined !, diff --git a/packages/core/test/linker/jit_summaries_integration_spec.ts b/packages/core/test/linker/jit_summaries_integration_spec.ts index 71c306246d..f3a85f6185 100644 --- a/packages/core/test/linker/jit_summaries_integration_spec.ts +++ b/packages/core/test/linker/jit_summaries_integration_spec.ts @@ -11,6 +11,7 @@ import {CompileMetadataResolver} from '@angular/compiler/src/metadata_resolver'; import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock'; import {Component, Directive, Injectable, NgModule, Pipe, Type} from '@angular/core'; import {TestBed, async, getTestBed} from '@angular/core/testing'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; export function main() { describe('Jit Summaries', () => { @@ -222,5 +223,29 @@ export function main() { .createComponent(TestComp); expectInstanceCreated(SomeDirective); }); + + it('should allow to override a provider', () => { + resetTestEnvironmentWithSummaries(summaries); + + const overwrittenValue = {}; + + TestBed.overrideProvider(SomeDep, {useFactory: () => overwrittenValue, deps: []}); + + const fixture = TestBed.configureTestingModule({providers: [SomeDep], imports: [SomeModule]}) + .createComponent(SomePublicComponent); + expect(fixture.componentInstance.dep).toBe(overwrittenValue); + }); + + it('should allow to override a template', () => { + resetTestEnvironmentWithSummaries(summaries); + + TestBed.overrideTemplateUsingTestingModule(SomePublicComponent, 'overwritten'); + + const fixture = TestBed.configureTestingModule({providers: [SomeDep], imports: [SomeModule]}) + .createComponent(SomePublicComponent); + expectInstanceCreated(SomePublicComponent); + + expect(fixture.nativeElement).toHaveText('overwritten'); + }); }); } diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 104c71227d..006d74c26d 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationInitStatus, CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, SchemaMetadata, SkipSelf, Type, ɵDepFlags as DepFlags, ɵNodeFlags as NodeFlags, ɵclearProviderOverrides as clearProviderOverrides, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core'; +import {ApplicationInitStatus, CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, SchemaMetadata, SkipSelf, StaticProvider, Type, ɵDepFlags as DepFlags, ɵNodeFlags as NodeFlags, ɵclearOverrides as clearOverrides, ɵgetComponentViewDefinitionFactory as getComponentViewDefinitionFactory, ɵoverrideComponentView as overrideComponentView, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core'; import {AsyncTestCompleter} from './async_test_completer'; import {ComponentFixture} from './component_fixture'; @@ -142,9 +142,23 @@ export class TestBed implements Injector { return TestBed; } + /** + * Overrides the template of the given component, compiling the template + * in the context of the TestingModule. + * + * Note: This works for JIT and AOTed components as well. + */ + static overrideTemplateUsingTestingModule(component: Type, template: string): + typeof TestBed { + getTestBed().overrideTemplateUsingTestingModule(component, template); + return TestBed; + } + /** * Overwrites all providers for the given token with the given provider definition. + * + * Note: This works for JIT and AOTed components as well. */ static overrideProvider(token: any, provider: { useFactory: Function, @@ -208,6 +222,7 @@ export class TestBed implements Injector { private _testEnvAotSummaries: () => any[] = () => []; private _aotSummaries: Array<() => any[]> = []; + private _templateOverrides: Array<{component: Type, templateOf: Type}> = []; platform: PlatformRef = null !; @@ -251,8 +266,9 @@ export class TestBed implements Injector { } resetTestingModule() { - clearProviderOverrides(); + clearOverrides(); this._aotSummaries = []; + this._templateOverrides = []; this._compiler = null !; this._moduleOverrides = []; this._componentOverrides = []; @@ -333,6 +349,11 @@ export class TestBed implements Injector { } } } + for (const {component, templateOf} of this._templateOverrides) { + const compFactory = this._compiler.getComponentFactory(templateOf); + overrideComponentView(component, compFactory); + } + const ngZone = new NgZone({enableLongStackTrace: true}); const ngZoneInjector = Injector.create([{provide: NgZone, useValue: ngZone}], this.platform.injector); @@ -345,7 +366,8 @@ export class TestBed implements Injector { private _createCompilerAndModule(): Type { const providers = this._providers.concat([{provide: TestBed, useValue: this}]); - const declarations = this._declarations; + const declarations = + [...this._declarations, ...this._templateOverrides.map(entry => entry.templateOf)]; const imports = [this.ngModule, this._imports]; const schemas = this._schemas; @@ -478,6 +500,16 @@ export class TestBed implements Injector { overrideProvider({token, flags, deps, value, deprecatedBehavior: deprecated}); } + overrideTemplateUsingTestingModule(component: Type, template: string) { + this._assertNotInstantiated('overrideTemplateUsingTestingModule', 'override template'); + + @Component({selector: 'empty', template: template}) + class OverrideComponent { + } + + this._templateOverrides.push({component, templateOf: OverrideComponent}); + } + createComponent(component: Type): ComponentFixture { this._initIfNeeded(); const componentFactory = this._compiler.getComponentFactory(component); diff --git a/packages/platform-browser/test/testing_public_spec.ts b/packages/platform-browser/test/testing_public_spec.ts index 609f659bbc..e0abb0319f 100644 --- a/packages/platform-browser/test/testing_public_spec.ts +++ b/packages/platform-browser/test/testing_public_spec.ts @@ -712,6 +712,60 @@ export function main() { }); }); + describe('overrideTemplateUsingTestingModule', () => { + it('should compile the template in the context of the testing module', () => { + @Component({selector: 'comp', template: 'a'}) + class MyComponent { + prop = 'some prop'; + } + + let testDir: TestDir|undefined; + + @Directive({selector: '[test]'}) + class TestDir { + constructor() { testDir = this; } + + @Input('test') + test: string; + } + + TestBed.overrideTemplateUsingTestingModule( + MyComponent, '
Hello world!
'); + + const fixture = TestBed.configureTestingModule({declarations: [MyComponent, TestDir]}) + .createComponent(MyComponent); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveText('Hello world!'); + expect(testDir).toBeAnInstanceOf(TestDir); + expect(testDir !.test).toBe('some prop'); + }); + + it('should throw if the TestBed is already created', () => { + @Component({selector: 'comp', template: 'a'}) + class MyComponent { + } + + TestBed.get(Injector); + + expect(() => TestBed.overrideTemplateUsingTestingModule(MyComponent, 'b')) + .toThrowError( + /Cannot override template when the test module has already been instantiated/); + }); + + it('should reset overrides when the testing modules is resetted', () => { + @Component({selector: 'comp', template: 'a'}) + class MyComponent { + } + + TestBed.overrideTemplateUsingTestingModule(MyComponent, 'b'); + TestBed.resetTestingModule(); + + const fixture = TestBed.configureTestingModule({declarations: [MyComponent]}) + .createComponent(MyComponent); + expect(fixture.nativeElement).toHaveText('a'); + }); + }); + describe('setting up the compiler', () => { describe('providers', () => { diff --git a/tools/public_api_guard/core/testing.d.ts b/tools/public_api_guard/core/testing.d.ts index 3532e0f888..68b635427f 100644 --- a/tools/public_api_guard/core/testing.d.ts +++ b/tools/public_api_guard/core/testing.d.ts @@ -71,13 +71,13 @@ export declare class TestBed implements Injector { }): void; configureTestingModule(moduleDef: TestModuleMetadata): void; createComponent(component: Type): ComponentFixture; + deprecatedOverrideProvider(token: any, provider: { + useValue: any; + }): void; /** @deprecated */ deprecatedOverrideProvider(token: any, provider: { useFactory: Function; deps: any[]; }): void; - deprecatedOverrideProvider(token: any, provider: { - useValue: any; - }): void; execute(tokens: any[], fn: Function, context?: any): any; get(token: any, notFoundValue?: any): any; /** @experimental */ initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, aotSummaries?: () => any[]): void; @@ -92,6 +92,7 @@ export declare class TestBed implements Injector { overrideProvider(token: any, provider: { useValue: any; }): void; + overrideTemplateUsingTestingModule(component: Type, template: string): void; /** @experimental */ resetTestEnvironment(): void; resetTestingModule(): void; static compileComponents(): Promise; @@ -101,13 +102,13 @@ export declare class TestBed implements Injector { }): typeof TestBed; static configureTestingModule(moduleDef: TestModuleMetadata): typeof TestBed; static createComponent(component: Type): ComponentFixture; + static deprecatedOverrideProvider(token: any, provider: { + useValue: any; + }): void; /** @deprecated */ static deprecatedOverrideProvider(token: any, provider: { useFactory: Function; deps: any[]; }): void; - static deprecatedOverrideProvider(token: any, provider: { - useValue: any; - }): void; static get(token: any, notFoundValue?: any): any; /** @experimental */ static initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, aotSummaries?: () => any[]): TestBed; static overrideComponent(component: Type, override: MetadataOverride): typeof TestBed; @@ -122,6 +123,7 @@ export declare class TestBed implements Injector { useValue: any; }): void; static overrideTemplate(component: Type, template: string): typeof TestBed; + static overrideTemplateUsingTestingModule(component: Type, template: string): typeof TestBed; /** @experimental */ static resetTestEnvironment(): void; static resetTestingModule(): typeof TestBed; } @@ -137,6 +139,7 @@ export declare type TestModuleMetadata = { declarations?: any[]; imports?: any[]; schemas?: Array; + aotSummaries?: () => any[]; }; /** @experimental */