diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index e118e4ff015ec..1012c4833ca95 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -398,6 +398,7 @@ function lookupTokenUsingModuleInjector( export function getOrCreateInjectable( tNode: TDirectiveHostNode|null, lView: LView, token: ProviderToken, flags: InjectFlags = InjectFlags.Default, notFoundValue?: any): T|null { + debugger; if (tNode !== null) { // If the view or any of its ancestors have an embedded // view injector, we have to look it up there first. diff --git a/packages/core/src/render3/features/standalone_feature.ts b/packages/core/src/render3/features/standalone_feature.ts index 1c2d813cdd274..9c057ef4ee325 100644 --- a/packages/core/src/render3/features/standalone_feature.ts +++ b/packages/core/src/render3/features/standalone_feature.ts @@ -42,6 +42,7 @@ class StandaloneService implements OnDestroy { } ngOnDestroy() { + debugger; try { for (const injector of this.cachedInjectors.values()) { if (injector !== null) { diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index bf72fb5141486..b5942e86a10c7 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -92,6 +92,7 @@ export function createLView( tHostNode: TNode|null, environment: LViewEnvironment|null, renderer: Renderer|null, injector: Injector|null, embeddedViewInjector: Injector|null, hydrationInfo: DehydratedView|null): LView { + debugger; const lView = tView.blueprint.slice() as LView; lView[HOST] = host; lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.FirstLViewPass; diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index 9fef0b46f5375..de4d2da6ded3d 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -198,6 +198,176 @@ describe('TestBed with Standalone types', () => { expect(fixture.nativeElement.innerHTML).toBe('Overridden A'); }); + xit('should override providers on components used as standalone component dependency', () => { + @Injectable() + class Service { + id = 'CmpDependency(original)'; + } + + @Injectable() + class MockService { + id = 'CmpDependency(mock)'; + } + + @Component({ + selector: 'dep', + standalone: true, + template: '{{ svc.id }}', + providers: [Service], + }) + class Dep { + svc = inject(Service); + } + + @Component({ + standalone: true, + template: '', + imports: [Dep], + }) + class MyStandaloneComp { + } + + // NOTE: the `TestBed.configureTestingModule` is load-bearing here: it instructs + // TestBed to examine and override providers in dependencies. + TestBed.configureTestingModule({imports: [MyStandaloneComp]}); + TestBed.overrideProvider(Service, {useFactory: () => new MockService()}); + + const fixture = TestBed.createComponent(MyStandaloneComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toBe('CmpDependency(mock)'); + + debugger; + + // Emulate an end of a test. + TestBed.resetTestingModule(); + + // Emulate the start of a next test, make sure previous overrides + // are not persisted across tests. + TestBed.configureTestingModule({imports: [MyStandaloneComp]}); + + const newFixture = TestBed.createComponent(MyStandaloneComp); + newFixture.detectChanges(); + + debugger; + + expect(newFixture.nativeElement.innerHTML).toBe('CmpDependency(original)'); + }); + + fit('should override providers on components used as standalone component dependency', () => { + @Component({ + selector: 'dep', + standalone: true, + template: 'main dep', + }) + class MainDep { + } + + @Component({ + selector: 'dep', + standalone: true, + template: 'mock dep', + }) + class MockDep { + } + + @Component({ + selector: 'app-root', + standalone: true, + imports: [MainDep], + template: '', + }) + class AppComponent { + } + + // NOTE: the `TestBed.configureTestingModule` is load-bearing here: it instructs + // TestBed to examine and override providers in dependencies. + TestBed.configureTestingModule({imports: [AppComponent]}); + + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toBe('CmpDependency(mock)'); + + debugger; + + // Emulate an end of a test. + TestBed.resetTestingModule(); + + // Emulate the start of a next test, make sure previous overrides + // are not persisted across tests. + TestBed.configureTestingModule({imports: [AppComponent]}); + TestBed.overrideComponent(AppComponent, { + set: { + imports: [MockDep], + } + }) + + const newFixture = TestBed.createComponent(AppComponent); + newFixture.detectChanges(); + + debugger; + + expect(newFixture.nativeElement.innerHTML).toBe('CmpDependency(original)'); + }); + + xit('should override providers on components used as standalone component dependency', () => { + @Injectable() + class Service { + id = 'CmpDependency(original)'; + } + + @Injectable() + class MockService { + id = 'CmpDependency(mock)'; + } + + @Component({ + selector: 'dep', + template: '{{ svc.id }}', + providers: [Service], + }) + class Dep { + svc = inject(Service); + } + + @Component({ + template: '', + }) + class MyStandaloneComp { + } + + @NgModule({declarations: [MyStandaloneComp, Dep]}) + class MyMod { + } + + // NOTE: the `TestBed.configureTestingModule` is load-bearing here: it instructs + // TestBed to examine and override providers in dependencies. + TestBed.configureTestingModule({imports: [MyMod]}); + TestBed.overrideProvider(Service, {useFactory: () => new MockService()}); + + const fixture = TestBed.createComponent(MyStandaloneComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toBe('CmpDependency(mock)'); + + debugger; + + // Emulate an end of a test. + TestBed.resetTestingModule(); + + // Emulate the start of a next test, make sure previous overrides + // are not persisted across tests. + TestBed.configureTestingModule({imports: [MyMod]}); + + const newFixture = TestBed.createComponent(MyStandaloneComp); + newFixture.detectChanges(); + + debugger; + + expect(newFixture.nativeElement.innerHTML).toBe('CmpDependency(original)'); + }); + it('should override providers in standalone component dependencies via overrideProvider', () => { const A = new InjectionToken('A'); @NgModule({ diff --git a/packages/core/testing/src/test_bed_compiler.ts b/packages/core/testing/src/test_bed_compiler.ts index 1039a77822165..af2d1377df8b3 100644 --- a/packages/core/testing/src/test_bed_compiler.ts +++ b/packages/core/testing/src/test_bed_compiler.ts @@ -65,6 +65,8 @@ export class TestBedCompiler { private pendingDirectives = new Set>(); private pendingPipes = new Set>(); + private parentComponents = new WeakMap, Array>>(); + // Keep track of all components and directives, so we can patch Providers onto defs later. private seenComponents = new Set>(); private seenDirectives = new Set>(); @@ -488,7 +490,8 @@ export class TestBedCompiler { * Applies provider overrides to a given type (either an NgModule or a standalone component) * and all imported NgModules and standalone components recursively. */ - private applyProviderOverridesInScope(type: Type): void { + private applyProviderOverridesInScope( + type: Type, parentStandaloneComponentType?: Type): void { const hasScope = isStandaloneComponent(type) || isNgModule(type); // The function can be re-entered recursively while inspecting dependencies @@ -515,14 +518,19 @@ export class TestBedCompiler { const def = getComponentDef(type); const dependencies = maybeUnwrapFn(def.dependencies ?? []); for (const dependency of dependencies) { - this.applyProviderOverridesInScope(dependency); + this.applyProviderOverridesInScope(dependency, type); } } else { const providers: Array = [ ...injectorDef.providers, ...(this.providerOverridesByModule.get(type as InjectorType) || []) ]; + debugger; if (this.hasProviderOverrides(providers)) { + // TODO: add docs. + if (parentStandaloneComponentType) { + this.storeFieldOfDefOnType(parentStandaloneComponentType, NG_COMP_DEF, 'tView'); + } this.maybeStoreNgDef(NG_INJ_DEF, type); this.storeFieldOfDefOnType(type, NG_INJ_DEF, 'providers'); @@ -659,6 +667,12 @@ export class TestBedCompiler { } processedDefs.add(def); + // TODO: add docs + this.storeFieldOfDefOnType(value, NG_COMP_DEF, 'tView'); + this.storeFieldOfDefOnType(value, NG_COMP_DEF, 'directiveDefs'); + this.storeFieldOfDefOnType(value, NG_COMP_DEF, 'pipeDefs'); + this.storeFieldOfDefOnType(value, NG_COMP_DEF, 'dependencies'); + const dependencies = maybeUnwrapFn(def.dependencies ?? []); dependencies.forEach((dependency) => { // Note: in AOT, the `dependencies` might also contain regular @@ -670,6 +684,13 @@ export class TestBedCompiler { } else { this.queueType(dependency, null); } + // TODO: add docs + if (getComponentDef(value)) { + this.storeFieldOfDefOnType(dependency, NG_COMP_DEF, 'tView'); + this.storeFieldOfDefOnType(dependency, NG_COMP_DEF, 'directiveDefs'); + this.storeFieldOfDefOnType(dependency, NG_COMP_DEF, 'pipeDefs'); + this.storeFieldOfDefOnType(dependency, NG_COMP_DEF, 'dependencies'); + } }); } } @@ -766,6 +787,8 @@ export class TestBedCompiler { } restoreOriginalState(): void { + debugger; + // Process cleanup ops in reverse order so the field's original value is restored correctly (in // case there were multiple overrides for the same field). forEachRight(this.defCleanupOps, (op: CleanupOperation) => { @@ -867,6 +890,8 @@ export class TestBedCompiler { Provider[] { if (!providers || !providers.length || this.providerOverridesByToken.size === 0) return []; + debugger; + const flattenedProviders = flattenProviders(providers); const overrides = this.getProviderOverrides(flattenedProviders); const overriddenProviders = [...flattenedProviders, ...overrides]; @@ -900,6 +925,7 @@ export class TestBedCompiler { private patchDefWithProviderOverrides(declaration: Type, field: string): void { const def = (declaration as any)[field]; + debugger; if (def && def.providersResolver) { this.maybeStoreNgDef(field, declaration);