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
This commit is contained in:
		
							parent
							
								
									e958447100
								
							
						
					
					
						commit
						71ec99856a
					
				| @ -6,8 +6,7 @@ | |||||||
|  * found in the LICENSE file at https://angular.io/license
 |  * found in the LICENSE file at https://angular.io/license
 | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import {ResourceLoader} from '@angular/compiler'; | import {Component, Directive, ErrorHandler, Inject, InjectionToken, NgModule, Optional, Pipe, ɵdefineComponent as defineComponent, ɵsetClassMetadata as setClassMetadata, ɵtext as text} from '@angular/core'; | ||||||
| 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 {TestBed, getTestBed} from '@angular/core/testing/src/test_bed'; | ||||||
| import {By} from '@angular/platform-browser'; | import {By} from '@angular/platform-browser'; | ||||||
| import {expect} from '@angular/platform-browser/testing/src/matchers'; | import {expect} from '@angular/platform-browser/testing/src/matchers'; | ||||||
| @ -270,6 +269,60 @@ describe('TestBed', () => { | |||||||
|     expect(TestBed.get(ErrorHandler)).toEqual(jasmine.any(CustomErrorHandler)); |     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') |   onlyInIvy('patched ng defs should be removed after resetting TestingModule') | ||||||
|       .describe('resetting ng defs', () => { |       .describe('resetting ng defs', () => { | ||||||
|         it('should restore ng defs to their initial states', () => { |         it('should restore ng defs to their initial states', () => { | ||||||
|  | |||||||
| @ -87,6 +87,10 @@ export class R3TestBedCompiler { | |||||||
|   private seenComponents = new Set<Type<any>>(); |   private seenComponents = new Set<Type<any>>(); | ||||||
|   private seenDirectives = new Set<Type<any>>(); |   private seenDirectives = new Set<Type<any>>(); | ||||||
| 
 | 
 | ||||||
|  |   // Store resolved styles for Components that have template overrides present and `styleUrls`
 | ||||||
|  |   // defined at the same time.
 | ||||||
|  |   private existingComponentStyles = new Map<Type<any>, string[]>(); | ||||||
|  | 
 | ||||||
|   private resolvers: Resolvers = initResolvers(); |   private resolvers: Resolvers = initResolvers(); | ||||||
| 
 | 
 | ||||||
|   private componentToModuleScope = new Map<Type<any>, Type<any>|TESTING_MODULE>(); |   private componentToModuleScope = new Map<Type<any>, Type<any>|TESTING_MODULE>(); | ||||||
| @ -194,9 +198,26 @@ export class R3TestBedCompiler { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   overrideTemplateUsingTestingModule(type: Type<any>, template: string): void { |   overrideTemplateUsingTestingModule(type: Type<any>, template: string): void { | ||||||
|     // In Ivy, compiling a component does not require knowing the module providing the component's
 |     const def = (type as any)[NG_COMPONENT_DEF]; | ||||||
|     // scope, so overrideTemplateUsingTestingModule can be implemented purely via overrideComponent.
 |     const hasStyleUrls = (): boolean => { | ||||||
|     this.overrideComponent(type, {set: {template}}); |       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.
 |     // Set the component's scope to be the testing module.
 | ||||||
|     this.componentToModuleScope.set(type, TESTING_MODULE); |     this.componentToModuleScope.set(type, TESTING_MODULE); | ||||||
| @ -231,6 +252,10 @@ export class R3TestBedCompiler { | |||||||
| 
 | 
 | ||||||
|     this.applyProviderOverrides(); |     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
 |     // Clear the componentToModuleScope map, so that future compilations don't reset the scope of
 | ||||||
|     // every component.
 |     // every component.
 | ||||||
|     this.componentToModuleScope.clear(); |     this.componentToModuleScope.clear(); | ||||||
| @ -347,7 +372,7 @@ export class R3TestBedCompiler { | |||||||
|     this.seenComponents.clear(); |     this.seenComponents.clear(); | ||||||
|     this.seenDirectives.clear(); |     this.seenDirectives.clear(); | ||||||
|   } |   } | ||||||
|   // ...
 | 
 | ||||||
|   private applyProviderOverridesToModule(moduleType: Type<any>): void { |   private applyProviderOverridesToModule(moduleType: Type<any>): void { | ||||||
|     const injectorDef: any = (moduleType as any)[NG_INJECTOR_DEF]; |     const injectorDef: any = (moduleType as any)[NG_INJECTOR_DEF]; | ||||||
|     if (this.providerOverridesByToken.size > 0) { |     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<any>|TESTING_MODULE): void { |   private queueTypeArray(arr: any[], moduleType: Type<any>|TESTING_MODULE): void { | ||||||
|     for (const value of arr) { |     for (const value of arr) { | ||||||
|       if (Array.isArray(value)) { |       if (Array.isArray(value)) { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user