mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(core): update TestBed to recognize Standalone Components (#45809)
This commit updates an internal logic of the TestBed to recognize Standalone Components to be able to apply the necessary overrides correctly. PR Close #45809
This commit is contained in:
parent
bb8d7091c6
commit
401dec46eb
3 changed files with 288 additions and 6 deletions
|
|
@ -168,6 +168,254 @@ describe('TestBed', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('TestBed with Standalone types', () => {
|
||||
beforeEach(() => {
|
||||
getTestBed().resetTestingModule();
|
||||
});
|
||||
|
||||
it('should override providers on standalone component itself', () => {
|
||||
const A = new InjectionToken('A');
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '{{ a }}',
|
||||
providers: [{provide: A, useValue: 'A'}],
|
||||
})
|
||||
class MyStandaloneComp {
|
||||
constructor(@Inject(A) public a: string) {}
|
||||
}
|
||||
|
||||
// NOTE: the `TestBed.configureTestingModule` is load-bearing here: it instructs
|
||||
// TestBed to examine and override providers in dependencies.
|
||||
TestBed.configureTestingModule({imports: [MyStandaloneComp]});
|
||||
TestBed.overrideProvider(A, {useValue: 'Overridden A'});
|
||||
|
||||
const fixture = TestBed.createComponent(MyStandaloneComp);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.innerHTML).toBe('Overridden A');
|
||||
});
|
||||
|
||||
it('should override providers in standalone component dependencies via overrideProvider', () => {
|
||||
const A = new InjectionToken('A');
|
||||
@NgModule({
|
||||
providers: [{provide: A, useValue: 'A'}],
|
||||
})
|
||||
class ComponentDependenciesModule {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '{{ a }}',
|
||||
imports: [ComponentDependenciesModule],
|
||||
})
|
||||
class MyStandaloneComp {
|
||||
constructor(@Inject(A) public a: string) {}
|
||||
}
|
||||
|
||||
// NOTE: the `TestBed.configureTestingModule` is load-bearing here: it instructs
|
||||
// TestBed to examine and override providers in dependencies.
|
||||
TestBed.configureTestingModule({imports: [MyStandaloneComp]});
|
||||
TestBed.overrideProvider(A, {useValue: 'Overridden A'});
|
||||
|
||||
const fixture = TestBed.createComponent(MyStandaloneComp);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.innerHTML).toBe('Overridden A');
|
||||
});
|
||||
|
||||
it('should override providers in standalone component dependencies via overrideModule', () => {
|
||||
const A = new InjectionToken('A');
|
||||
@NgModule({
|
||||
providers: [{provide: A, useValue: 'A'}],
|
||||
})
|
||||
class ComponentDependenciesModule {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '{{ a }}',
|
||||
imports: [ComponentDependenciesModule],
|
||||
})
|
||||
class MyStandaloneComp {
|
||||
constructor(@Inject(A) public a: string) {}
|
||||
}
|
||||
|
||||
// NOTE: the `TestBed.configureTestingModule` is *not* needed here, since the TestBed
|
||||
// knows which NgModule was overridden and needs re-compilation.
|
||||
TestBed.overrideModule(
|
||||
ComponentDependenciesModule, {set: {providers: [{provide: A, useValue: 'Overridden A'}]}});
|
||||
|
||||
const fixture = TestBed.createComponent(MyStandaloneComp);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.innerHTML).toBe('Overridden A');
|
||||
});
|
||||
|
||||
it('should allow overriding a template of a standalone component', () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: 'Original',
|
||||
})
|
||||
class MyStandaloneComp {
|
||||
}
|
||||
|
||||
// NOTE: the `TestBed.configureTestingModule` call is *not* required here, since TestBed already
|
||||
// knows that the `MyStandaloneComp` should be overridden/recompiled.
|
||||
TestBed.overrideComponent(MyStandaloneComp, {set: {template: 'Overridden'}});
|
||||
|
||||
const fixture = TestBed.createComponent(MyStandaloneComp);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.innerHTML).toBe('Overridden');
|
||||
});
|
||||
|
||||
it('should allow overriding the set of directives and pipes used in a standalone component',
|
||||
() => {
|
||||
@Directive({
|
||||
selector: '[dir]',
|
||||
standalone: true,
|
||||
host: {'[id]': 'id'},
|
||||
})
|
||||
class MyStandaloneDirectiveA {
|
||||
id = 'A';
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[dir]',
|
||||
standalone: true,
|
||||
host: {'[id]': 'id'},
|
||||
})
|
||||
class MyStandaloneDirectiveB {
|
||||
id = 'B';
|
||||
}
|
||||
|
||||
@Pipe({name: 'pipe', standalone: true})
|
||||
class MyStandalonePipeA {
|
||||
transform(value: string): string {
|
||||
return `transformed ${value} (A)`;
|
||||
}
|
||||
}
|
||||
@Pipe({name: 'pipe', standalone: true})
|
||||
class MyStandalonePipeB {
|
||||
transform(value: string): string {
|
||||
return `transformed ${value} (B)`;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<div dir>{{ name | pipe }}</div>',
|
||||
imports: [MyStandalonePipeA, MyStandaloneDirectiveA],
|
||||
})
|
||||
class MyStandaloneComp {
|
||||
name = 'MyStandaloneComp';
|
||||
}
|
||||
|
||||
// NOTE: the `TestBed.configureTestingModule` call is *not* required here, since TestBed
|
||||
// already knows that the `MyStandaloneComp` should be overridden/recompiled.
|
||||
TestBed.overrideComponent(
|
||||
MyStandaloneComp, {set: {imports: [MyStandalonePipeB, MyStandaloneDirectiveB]}});
|
||||
|
||||
const fixture = TestBed.createComponent(MyStandaloneComp);
|
||||
fixture.detectChanges();
|
||||
|
||||
const rootElement = fixture.nativeElement.firstChild;
|
||||
expect(rootElement.id).toBe('B');
|
||||
expect(rootElement.innerHTML).toBe('transformed MyStandaloneComp (B)');
|
||||
});
|
||||
|
||||
it('should reflect overrides on imported standalone directive', () => {
|
||||
@Directive({
|
||||
selector: '[dir]',
|
||||
standalone: true,
|
||||
host: {'[id]': 'id'},
|
||||
})
|
||||
class DepStandaloneDirective {
|
||||
id = 'A';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'standalone-cmp',
|
||||
standalone: true,
|
||||
template: 'Original MyStandaloneComponent',
|
||||
})
|
||||
class DepStandaloneComponent {
|
||||
id = 'A';
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '<standalone-cmp dir>Hello world!</standalone-cmp>',
|
||||
imports: [DepStandaloneDirective, DepStandaloneComponent],
|
||||
})
|
||||
class RootStandaloneComp {
|
||||
}
|
||||
|
||||
// NOTE: the `TestBed.configureTestingModule` call is *not* required here, since TestBed
|
||||
// already knows which Components/Directives are overridden and should be recompiled.
|
||||
TestBed.overrideComponent(
|
||||
DepStandaloneComponent, {set: {template: 'Overridden MyStandaloneComponent'}});
|
||||
TestBed.overrideDirective(DepStandaloneDirective, {set: {host: {'[id]': '\'Overridden\''}}});
|
||||
|
||||
const fixture = TestBed.createComponent(RootStandaloneComp);
|
||||
fixture.detectChanges();
|
||||
|
||||
const rootElement = fixture.nativeElement.firstChild;
|
||||
|
||||
expect(rootElement.id).toBe('Overridden');
|
||||
expect(rootElement.innerHTML).toBe('Overridden MyStandaloneComponent');
|
||||
});
|
||||
|
||||
describe('NgModules as dependencies', () => {
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
template: '...',
|
||||
})
|
||||
class TestComponent {
|
||||
testField = 'default';
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
template: '...',
|
||||
})
|
||||
class MockTestComponent {
|
||||
testField = 'overridden';
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [TestComponent],
|
||||
exports: [TestComponent],
|
||||
})
|
||||
class TestModule {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-root',
|
||||
template: `<test-cmp #testCmpCtrl></test-cmp>`,
|
||||
imports: [TestModule],
|
||||
})
|
||||
class AppComponent {
|
||||
@ViewChild('testCmpCtrl', {static: true}) testCmpCtrl!: TestComponent;
|
||||
}
|
||||
|
||||
it('should allow declarations and exports overrides on an imported NgModule', () => {
|
||||
// replace TestComponent with MockTestComponent
|
||||
TestBed.overrideModule(TestModule, {
|
||||
remove: {declarations: [TestComponent], exports: [TestComponent]},
|
||||
add: {declarations: [MockTestComponent], exports: [MockTestComponent]}
|
||||
});
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.testCmpCtrl.testField).toBe('overridden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestBed', () => {
|
||||
beforeEach(() => {
|
||||
getTestBed().resetTestingModule();
|
||||
|
|
|
|||
|
|
@ -418,8 +418,7 @@ export class TestBedRender3 implements TestBed {
|
|||
const componentDef = (type as any).ɵcmp;
|
||||
|
||||
if (!componentDef) {
|
||||
throw new Error(
|
||||
`It looks like '${stringify(type)}' has not been IVY compiled - it has no 'ɵcmp' field`);
|
||||
throw new Error(`It looks like '${stringify(type)}' has not been compiled.`);
|
||||
}
|
||||
|
||||
// TODO: Don't cast as `InjectionToken<boolean>`, proper type is boolean[]
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {ResourceLoader} from '@angular/compiler';
|
|||
import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, resolveForwardRef, Type, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDirectiveDef as DirectiveDef, ɵgetInjectableDef as getInjectableDef, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵɵInjectableDeclaration as InjectableDeclaration} from '@angular/core';
|
||||
|
||||
import {clearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution, resolveComponentResources, restoreComponentResolutionQueue} from '../../src/metadata/resource_loading';
|
||||
import {ComponentDef, ComponentType} from '../../src/render3';
|
||||
|
||||
import {MetadataOverride} from './metadata_override';
|
||||
import {ComponentResolver, DirectiveResolver, NgModuleResolver, PipeResolver, Resolver} from './resolvers';
|
||||
|
|
@ -424,8 +425,24 @@ export class R3TestBedCompiler {
|
|||
}
|
||||
this.moduleProvidersOverridden.add(moduleType);
|
||||
|
||||
// NOTE: the line below triggers JIT compilation of the module injector,
|
||||
// which also invokes verification of the NgModule semantics, which produces
|
||||
// detailed error messages. The fact that the code relies on this line being
|
||||
// present here is suspicious and should be refactored in a way that the line
|
||||
// below can be moved (for ex. after an early exit check below).
|
||||
const injectorDef: any = (moduleType as any)[NG_INJ_DEF];
|
||||
if (this.providerOverridesByToken.size > 0) {
|
||||
|
||||
// No provider overrides, exit early.
|
||||
if (this.providerOverridesByToken.size === 0) return;
|
||||
|
||||
if (isStandaloneComponent(moduleType)) {
|
||||
// Visit all component dependencies and override providers there.
|
||||
const def = getComponentDef(moduleType);
|
||||
const dependencies = maybeUnwrapFn(def.dependencies ?? []);
|
||||
for (const dependency of dependencies) {
|
||||
this.applyProviderOverridesToModule(dependency);
|
||||
}
|
||||
} else {
|
||||
const providers = [
|
||||
...injectorDef.providers,
|
||||
...(this.providerOverridesByModule.get(moduleType as InjectorType<any>) || [])
|
||||
|
|
@ -482,7 +499,7 @@ export class R3TestBedCompiler {
|
|||
compileNgModuleDefs(ngModule as NgModuleType<any>, metadata);
|
||||
}
|
||||
|
||||
private queueType(type: Type<any>, moduleType: Type<any>|TestingModuleOverride): void {
|
||||
private queueType(type: Type<any>, moduleType: Type<any>|TestingModuleOverride|null): void {
|
||||
const component = this.resolvers.component.resolve(type);
|
||||
if (component) {
|
||||
// Check whether a give Type has respective NG def (ɵcmp) and compile if def is
|
||||
|
|
@ -508,8 +525,11 @@ export class R3TestBedCompiler {
|
|||
// real module, which was imported. This pattern is understood to mean that the component
|
||||
// should use its original scope, but that the testing module should also contain the
|
||||
// component in its scope.
|
||||
if (!this.componentToModuleScope.has(type) ||
|
||||
this.componentToModuleScope.get(type) === TestingModuleOverride.DECLARATION) {
|
||||
//
|
||||
// Note: standalone components have no associated NgModule, so the `moduleType` can be `null`.
|
||||
if (moduleType !== null &&
|
||||
(!this.componentToModuleScope.has(type) ||
|
||||
this.componentToModuleScope.get(type) === TestingModuleOverride.DECLARATION)) {
|
||||
this.componentToModuleScope.set(type, moduleType);
|
||||
}
|
||||
return;
|
||||
|
|
@ -553,6 +573,10 @@ export class R3TestBedCompiler {
|
|||
queueTypesFromModulesArrayRecur(maybeUnwrapFn(def.exports));
|
||||
} else if (isModuleWithProviders(value)) {
|
||||
queueTypesFromModulesArrayRecur([value.ngModule]);
|
||||
} else if (isStandaloneComponent(value)) {
|
||||
this.queueType(value, null);
|
||||
const def = getComponentDef(value);
|
||||
queueTypesFromModulesArrayRecur(maybeUnwrapFn(def.dependencies ?? []));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -790,6 +814,17 @@ function initResolvers(): Resolvers {
|
|||
};
|
||||
}
|
||||
|
||||
function isStandaloneComponent<T>(value: Type<T>): value is ComponentType<T> {
|
||||
const def = getComponentDef(value);
|
||||
return !!def?.standalone;
|
||||
}
|
||||
|
||||
function getComponentDef(value: ComponentType<unknown>): ComponentDef<unknown>;
|
||||
function getComponentDef(value: Type<unknown>): ComponentDef<unknown>|null;
|
||||
function getComponentDef(value: Type<unknown>): ComponentDef<unknown>|null {
|
||||
return (value as any).ɵcmp ?? null;
|
||||
}
|
||||
|
||||
function hasNgModuleDef<T>(value: Type<T>): value is NgModuleType<T> {
|
||||
return value.hasOwnProperty('ɵmod');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue