diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts
index d47991a2883..b88a4b7399b 100644
--- a/packages/core/test/test_bed_spec.ts
+++ b/packages/core/test/test_bed_spec.ts
@@ -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: '
{{ name | pipe }}
',
+ 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: 'Hello world!',
+ 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: ``,
+ 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();
diff --git a/packages/core/testing/src/r3_test_bed.ts b/packages/core/testing/src/r3_test_bed.ts
index 77176c2146b..9d8daa16a5f 100644
--- a/packages/core/testing/src/r3_test_bed.ts
+++ b/packages/core/testing/src/r3_test_bed.ts
@@ -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`, proper type is boolean[]
diff --git a/packages/core/testing/src/r3_test_bed_compiler.ts b/packages/core/testing/src/r3_test_bed_compiler.ts
index 624aaa53693..1f5dcce7685 100644
--- a/packages/core/testing/src/r3_test_bed_compiler.ts
+++ b/packages/core/testing/src/r3_test_bed_compiler.ts
@@ -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) || [])
@@ -482,7 +499,7 @@ export class R3TestBedCompiler {
compileNgModuleDefs(ngModule as NgModuleType, metadata);
}
- private queueType(type: Type, moduleType: Type|TestingModuleOverride): void {
+ private queueType(type: Type, moduleType: Type|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(value: Type): value is ComponentType {
+ const def = getComponentDef(value);
+ return !!def?.standalone;
+}
+
+function getComponentDef(value: ComponentType): ComponentDef;
+function getComponentDef(value: Type): ComponentDef|null;
+function getComponentDef(value: Type): ComponentDef|null {
+ return (value as any).ɵcmp ?? null;
+}
+
function hasNgModuleDef(value: Type): value is NgModuleType {
return value.hasOwnProperty('ɵmod');
}