Skip to content

Commit

Permalink
fix(core): handle NgModules with standalone pipes in TestBed correctly (
Browse files Browse the repository at this point in the history
#46407)

Prior to this commit, the TestBed logic erroneously tried to apply provider overrides to standalone pipes that were imported in an NgModule. This commit updates the logic to recognize types that may have a scope (an NgModule or a standalone component) and skip other types while applying provider overrides recursively.

PR Close #46407
  • Loading branch information
AndrewKushnir committed Jun 21, 2022
1 parent c79bb14 commit 5d3b97e
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 9 deletions.
42 changes: 42 additions & 0 deletions packages/core/test/test_bed_spec.ts
Expand Up @@ -367,6 +367,48 @@ describe('TestBed with Standalone types', () => {
expect(rootElement.innerHTML).toBe('Overridden MyStandaloneComponent');
});

it('should make overridden providers available in pipes', () => {
const TOKEN_A = new InjectionToken('TOKEN_A');
@Pipe({
name: 'testPipe',
standalone: true,
})
class TestPipe {
constructor(@Inject(TOKEN_A) private token: string) {}

transform(value: string): string {
return `transformed ${value} using ${this.token} token`;
}
}
@NgModule({
imports: [TestPipe],
exports: [TestPipe],
providers: [{provide: TOKEN_A, useValue: 'A'}],
})
class TestNgModule {
}

@Component({
selector: 'test-component',
standalone: true,
imports: [TestNgModule],
template: `{{ 'original value' | testPipe }}`
})
class TestComponent {
}

TestBed.configureTestingModule({
imports: [TestComponent],
});
TestBed.overrideProvider(TOKEN_A, {useValue: 'Overridden A'});

const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

const hostElement = fixture.nativeElement.firstChild;
expect(hostElement.textContent).toBe('transformed original value using Overridden A token');
});

describe('NgModules as dependencies', () => {
@Component({
selector: 'test-cmp',
Expand Down
21 changes: 12 additions & 9 deletions packages/core/testing/src/r3_test_bed_compiler.ts
Expand Up @@ -441,7 +441,13 @@ export class R3TestBedCompiler {
* and all imported NgModules and standalone components recursively.
*/
private applyProviderOverridesInScope(type: Type<any>): void {
if (this.scopesWithOverriddenProviders.has(type)) {
const hasScope = isStandaloneComponent(type) || isNgModule(type);

// The function can be re-entered recursively while inspecting dependencies
// of an NgModule or a standalone component. Exit early if we come across a
// type that can not have a scope (directive or pipe) or the type is already
// processed earlier.
if (!hasScope || this.scopesWithOverriddenProviders.has(type)) {
return;
}
this.scopesWithOverriddenProviders.add(type);
Expand All @@ -461,14 +467,7 @@ export class R3TestBedCompiler {
const def = getComponentDef(type);
const dependencies = maybeUnwrapFn(def.dependencies ?? []);
for (const dependency of dependencies) {
// Proceed with examining dependencies recursively
// when a dependency is a standalone component or an NgModule.
// In AOT, the `dependencies` might also contain regular (NgModule-based)
// Component, Directive and Pipes. Skip them here, they are handled in a
// different location (in the `configureTestingModule` function).
if (isStandaloneComponent(dependency) || hasNgModuleDef(dependency)) {
this.applyProviderOverridesInScope(dependency);
}
this.applyProviderOverridesInScope(dependency);
}
} else {
const providers = [
Expand Down Expand Up @@ -880,6 +879,10 @@ function hasNgModuleDef<T>(value: Type<T>): value is NgModuleType<T> {
return value.hasOwnProperty('ɵmod');
}

function isNgModule<T>(value: Type<T>): boolean {
return hasNgModuleDef(value);
}

function maybeUnwrapFn<T>(maybeFn: (() => T)|T): T {
return maybeFn instanceof Function ? maybeFn() : maybeFn;
}
Expand Down

0 comments on commit 5d3b97e

Please sign in to comment.