From 71ec99856aaf4e6ea35cae68573b48981dc05e0a Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Wed, 27 Mar 2019 14:56:08 -0700 Subject: [PATCH] fix(ivy): allow TestBed to recompile AOT-compiled components in case of template overrides (#29555) Prior to this change, recompilation of AOT-compiled components in TestBed may fail when template override is requested. That was happening due to the `styleUrls` field defined for a Component, thus switching its state to "requires resolution" (i.e. having external resources) at compile time. This change avoids this issue by storing styles and resetting `styleUrls` field before recompilation. Once compilation is done, saved styles are patched back onto Component def. PR Close #29555 --- packages/core/test/test_bed_spec.ts | 57 ++++++++++++++++++- .../core/testing/src/r3_test_bed_compiler.ts | 39 +++++++++++-- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index 7766ebde0b..6ef190b3f8 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -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 {Component, Directive, ErrorHandler, Inject, InjectionToken, NgModule, Optional, Pipe, ɵdefineComponent as defineComponent, ɵsetClassMetadata as setClassMetadata, ɵtext as text} 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'; @@ -270,6 +269,60 @@ describe('TestBed', () => { expect(TestBed.get(ErrorHandler)).toEqual(jasmine.any(CustomErrorHandler)); }); + onlyInIvy('TestBed should handle AOT pre-compiled Components') + .describe('AOT pre-compiled components', () => { + /** + * Function returns a class that represents AOT-compiled version of the following Component: + * + * @Component({ + * selector: 'comp', + * templateUrl: './template.ng.html', + * styleUrls: ['./style.css'] + * }) + * class ComponentClass {} + * + * This is needed to closer match the behavior of AOT pre-compiled components (compiled + * outside of TestBed) without changing TestBed state and/or Component metadata to compile + * them via TestBed with external resources. + */ + const getAOTCompiledComponent = () => { + class ComponentClass { + static ngComponentDef = defineComponent({ + type: ComponentClass, + selectors: [['comp']], + factory: () => new ComponentClass(), + consts: 1, + vars: 0, + template: (rf: any, ctx: any) => { + if (rf & 1) { + text(0, 'Some template'); + } + }, + styles: ['body { margin: 0; }'] + }); + } + setClassMetadata( + ComponentClass, [{ + type: Component, + args: [{ + selector: 'comp', + templateUrl: './template.ng.html', + styleUrls: ['./style.css'], + }] + }], + null, null); + return ComponentClass; + }; + + it('should have an ability to override template', () => { + const SomeComponent = getAOTCompiledComponent(); + TestBed.configureTestingModule({declarations: [SomeComponent]}); + TestBed.overrideTemplateUsingTestingModule(SomeComponent, 'Template override'); + const fixture = TestBed.createComponent(SomeComponent); + expect(fixture.nativeElement.innerHTML).toBe('Template override'); + }); + }); + onlyInIvy('patched ng defs should be removed after resetting TestingModule') .describe('resetting ng defs', () => { it('should restore ng defs to their initial states', () => { diff --git a/packages/core/testing/src/r3_test_bed_compiler.ts b/packages/core/testing/src/r3_test_bed_compiler.ts index aeae8e6ee4..c2e38deb3d 100644 --- a/packages/core/testing/src/r3_test_bed_compiler.ts +++ b/packages/core/testing/src/r3_test_bed_compiler.ts @@ -87,6 +87,10 @@ export class R3TestBedCompiler { private seenComponents = new Set>(); private seenDirectives = new Set>(); + // Store resolved styles for Components that have template overrides present and `styleUrls` + // defined at the same time. + private existingComponentStyles = new Map, string[]>(); + private resolvers: Resolvers = initResolvers(); private componentToModuleScope = new Map, Type|TESTING_MODULE>(); @@ -194,9 +198,26 @@ export class R3TestBedCompiler { } 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}}); + const def = (type as any)[NG_COMPONENT_DEF]; + const hasStyleUrls = (): boolean => { + const metadata = this.resolvers.component.resolve(type) !as Component; + return !!metadata.styleUrls && metadata.styleUrls.length > 0; + }; + const overrideStyleUrls = !!def && !isComponentDefPendingResolution(type) && hasStyleUrls(); + + // In Ivy, compiling a component does not require knowing the module providing the + // component's scope, so overrideTemplateUsingTestingModule can be implemented purely via + // overrideComponent. Important: overriding template requires full Component re-compilation, + // which may fail in case styleUrls are also present (thus Component is considered as required + // resolution). In order to avoid this, we preemptively set styleUrls to an empty array, + // preserve current styles available on Component def and restore styles back once compilation + // is complete. + const override = overrideStyleUrls ? {template, styles: [], styleUrls: []} : {template}; + this.overrideComponent(type, {set: override}); + + if (overrideStyleUrls && def.styles && def.styles.length > 0) { + this.existingComponentStyles.set(type, def.styles); + } // Set the component's scope to be the testing module. this.componentToModuleScope.set(type, TESTING_MODULE); @@ -231,6 +252,10 @@ export class R3TestBedCompiler { this.applyProviderOverrides(); + // Patch previously stored `styles` Component values (taken from ngComponentDef), in case these + // Components have `styleUrls` fields defined and template override was requested. + this.patchComponentsWithExistingStyles(); + // Clear the componentToModuleScope map, so that future compilations don't reset the scope of // every component. this.componentToModuleScope.clear(); @@ -347,7 +372,7 @@ export class R3TestBedCompiler { 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) { @@ -369,6 +394,12 @@ export class R3TestBedCompiler { } } + private patchComponentsWithExistingStyles(): void { + this.existingComponentStyles.forEach( + (styles, type) => (type as any)[NG_COMPONENT_DEF].styles = styles); + this.existingComponentStyles.clear(); + } + private queueTypeArray(arr: any[], moduleType: Type|TESTING_MODULE): void { for (const value of arr) { if (Array.isArray(value)) {