Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ivy): allow TestBed to recompile AOT-compiled components in case of template overrides #29555

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 55 additions & 2 deletions packages/core/test/test_bed_spec.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down
39 changes: 35 additions & 4 deletions packages/core/testing/src/r3_test_bed_compiler.ts
Expand Up @@ -87,6 +87,10 @@ export class R3TestBedCompiler {
private seenComponents = 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 componentToModuleScope = new Map<Type<any>, Type<any>|TESTING_MODULE>();
Expand Down Expand Up @@ -194,9 +198,26 @@ export class R3TestBedCompiler {
}

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}});
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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -347,7 +372,7 @@ export class R3TestBedCompiler {
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) {
Expand All @@ -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 {
for (const value of arr) {
if (Array.isArray(value)) {
Expand Down