Skip to content

Commit

Permalink
fix(core): avoid stale provider info when TestBed.overrideProvider is…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
AndrewKushnir authored and pkozlowski-opensource committed Nov 29, 2023
1 parent dffeac8 commit 6be8804
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 9 deletions.
51 changes: 51 additions & 0 deletions packages/core/test/test_bed_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,57 @@ describe('TestBed with Standalone types', () => {
expect(fixture.nativeElement.innerHTML).toBe('Overridden A');
});

it('should override providers on components used as standalone component dependency', () => {
@Injectable()
class Service {
id = 'Service(original)';
}

@Injectable()
class MockService {
id = 'Service(mock)';
}

@Component({
selector: 'dep',
standalone: true,
template: '{{ service.id }}',
providers: [Service],
})
class Dep {
service = inject(Service);
}

@Component({
standalone: true,
template: '<dep />',
imports: [Dep],
})
class MyStandaloneComp {
}

TestBed.configureTestingModule({imports: [MyStandaloneComp]});
TestBed.overrideProvider(Service, {useFactory: () => new MockService()});

let fixture = TestBed.createComponent(MyStandaloneComp);
fixture.detectChanges();

expect(fixture.nativeElement.innerHTML).toBe('<dep>Service(mock)</dep>');

// 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]});

fixture = TestBed.createComponent(MyStandaloneComp);
fixture.detectChanges();

// No provider overrides, expect original provider value to be used.
expect(fixture.nativeElement.innerHTML).toBe('<dep>Service(original)</dep>');
});

it('should override providers in standalone component dependencies via overrideProvider', () => {
const A = new InjectionToken('A');
@NgModule({
Expand Down
27 changes: 18 additions & 9 deletions packages/core/testing/src/test_bed_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,14 @@ export class TestBedCompiler {

private resolvers: Resolvers = initResolvers();

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

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

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

this.componentToModuleScope.clear();
Expand Down Expand Up @@ -604,10 +616,7 @@ export class TestBedCompiler {
// 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.
//
// Note: standalone components have no associated NgModule, so the `moduleType` can be `null`.
if (moduleType !== null &&
(!this.componentToModuleScope.has(type) ||
if ((!this.componentToModuleScope.has(type) ||
this.componentToModuleScope.get(type) === TestingModuleOverride.DECLARATION)) {
this.componentToModuleScope.set(type, moduleType);
}
Expand Down

0 comments on commit 6be8804

Please sign in to comment.