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…
Reference in New Issue