Skip to content

Commit 58cf389

Browse files
fix(core): avoid stale provider info when TestBed.overrideProvider is used (#52918)
This commit updates the logic to preserve previous value of cached TView before applying overrides. This helps ensure that the next tests that uses the same component has correct provider info. PR Close #52918
1 parent c632628 commit 58cf389

File tree

2 files changed

+69
-9
lines changed

2 files changed

+69
-9
lines changed

packages/core/test/test_bed_spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,57 @@ describe('TestBed with Standalone types', () => {
248248
expect(fixture.nativeElement.innerHTML).toBe('Overridden A');
249249
});
250250

251+
it('should override providers on components used as standalone component dependency', () => {
252+
@Injectable()
253+
class Service {
254+
id = 'Service(original)';
255+
}
256+
257+
@Injectable()
258+
class MockService {
259+
id = 'Service(mock)';
260+
}
261+
262+
@Component({
263+
selector: 'dep',
264+
standalone: true,
265+
template: '{{ service.id }}',
266+
providers: [Service],
267+
})
268+
class Dep {
269+
service = inject(Service);
270+
}
271+
272+
@Component({
273+
standalone: true,
274+
template: '<dep />',
275+
imports: [Dep],
276+
})
277+
class MyStandaloneComp {
278+
}
279+
280+
TestBed.configureTestingModule({imports: [MyStandaloneComp]});
281+
TestBed.overrideProvider(Service, {useFactory: () => new MockService()});
282+
283+
let fixture = TestBed.createComponent(MyStandaloneComp);
284+
fixture.detectChanges();
285+
286+
expect(fixture.nativeElement.innerHTML).toBe('<dep>Service(mock)</dep>');
287+
288+
// Emulate an end of a test.
289+
TestBed.resetTestingModule();
290+
291+
// Emulate the start of a next test, make sure previous overrides
292+
// are not persisted across tests.
293+
TestBed.configureTestingModule({imports: [MyStandaloneComp]});
294+
295+
fixture = TestBed.createComponent(MyStandaloneComp);
296+
fixture.detectChanges();
297+
298+
// No provider overrides, expect original provider value to be used.
299+
expect(fixture.nativeElement.innerHTML).toBe('<dep>Service(original)</dep>');
300+
});
301+
251302
it('should override providers in standalone component dependencies via overrideProvider', () => {
252303
const A = new InjectionToken('A');
253304
@NgModule({

packages/core/testing/src/test_bed_compiler.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,14 @@ export class TestBedCompiler {
7878

7979
private resolvers: Resolvers = initResolvers();
8080

81-
private componentToModuleScope = new Map<Type<any>, Type<any>|TestingModuleOverride>();
81+
// Map of component type to an NgModule that declares it.
82+
//
83+
// There are a couple special cases:
84+
// - for standalone components, the module scope value is `null`
85+
// - when a component is declared in `TestBed.configureTestingModule()` call or
86+
// a component's template is overridden via `TestBed.overrideTemplateUsingTestingModule()`.
87+
// we use a special value from the `TestingModuleOverride` enum.
88+
private componentToModuleScope = new Map<Type<any>, Type<any>|TestingModuleOverride|null>();
8289

8390
// Map that keeps initial version of component/directive/pipe defs in case
8491
// we compile a Type again, thus overriding respective static fields. This is
@@ -457,15 +464,20 @@ export class TestBedCompiler {
457464
};
458465

459466
this.componentToModuleScope.forEach((moduleType, componentType) => {
460-
const moduleScope = getScopeOfModule(moduleType);
461-
this.storeFieldOfDefOnType(componentType, NG_COMP_DEF, 'directiveDefs');
462-
this.storeFieldOfDefOnType(componentType, NG_COMP_DEF, 'pipeDefs');
467+
if (moduleType !== null) {
468+
const moduleScope = getScopeOfModule(moduleType);
469+
this.storeFieldOfDefOnType(componentType, NG_COMP_DEF, 'directiveDefs');
470+
this.storeFieldOfDefOnType(componentType, NG_COMP_DEF, 'pipeDefs');
471+
patchComponentDefWithScope(getComponentDef(componentType)!, moduleScope);
472+
}
463473
// `tView` that is stored on component def contains information about directives and pipes
464474
// that are in the scope of this component. Patching component scope will cause `tView` to be
465475
// changed. Store original `tView` before patching scope, so the `tView` (including scope
466476
// information) is restored back to its previous/original state before running next test.
477+
// Resetting `tView` is also needed for cases when we apply provider overrides and those
478+
// providers are defined on component's level, in which case they may end up included into
479+
// `tView.blueprint`.
467480
this.storeFieldOfDefOnType(componentType, NG_COMP_DEF, 'tView');
468-
patchComponentDefWithScope((componentType as any).ɵcmp, moduleScope);
469481
});
470482

471483
this.componentToModuleScope.clear();
@@ -604,10 +616,7 @@ export class TestBedCompiler {
604616
// real module, which was imported. This pattern is understood to mean that the component
605617
// should use its original scope, but that the testing module should also contain the
606618
// component in its scope.
607-
//
608-
// Note: standalone components have no associated NgModule, so the `moduleType` can be `null`.
609-
if (moduleType !== null &&
610-
(!this.componentToModuleScope.has(type) ||
619+
if ((!this.componentToModuleScope.has(type) ||
611620
this.componentToModuleScope.get(type) === TestingModuleOverride.DECLARATION)) {
612621
this.componentToModuleScope.set(type, moduleType);
613622
}

0 commit comments

Comments
 (0)