Skip to content

Commit

Permalink
WIP: TestBed provider override for components
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewKushnir committed Nov 14, 2023
1 parent 3cf18bb commit cd5f09b
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/core/src/render3/di.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ function lookupTokenUsingModuleInjector<T>(
export function getOrCreateInjectable<T>(
tNode: TDirectiveHostNode|null, lView: LView, token: ProviderToken<T>,
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.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/render3/features/standalone_feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class StandaloneService implements OnDestroy {
}

ngOnDestroy() {
debugger;
try {
for (const injector of this.cachedInjectors.values()) {
if (injector !== null) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function createLView<T>(
tHostNode: TNode|null, environment: LViewEnvironment|null, renderer: Renderer|null,
injector: Injector|null, embeddedViewInjector: Injector|null,
hydrationInfo: DehydratedView|null): LView<T> {
debugger;
const lView = tView.blueprint.slice() as LView;
lView[HOST] = host;
lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.FirstLViewPass;
Expand Down
170 changes: 170 additions & 0 deletions packages/core/test/test_bed_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<dep />',
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('<dep>CmpDependency(mock)</dep>');

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('<dep>CmpDependency(original)</dep>');
});

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: '<dep />',
})
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('<dep>CmpDependency(mock)</dep>');

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('<dep>CmpDependency(original)</dep>');
});

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: '<dep />',
})
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('<dep>CmpDependency(mock)</dep>');

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('<dep>CmpDependency(original)</dep>');
});

it('should override providers in standalone component dependencies via overrideProvider', () => {
const A = new InjectionToken('A');
@NgModule({
Expand Down
30 changes: 28 additions & 2 deletions packages/core/testing/src/test_bed_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export class TestBedCompiler {
private pendingDirectives = new Set<Type<any>>();
private pendingPipes = new Set<Type<any>>();

private parentComponents = new WeakMap<Type<unknown>, Array<Type<unknown>>>();

// Keep track of all components and directives, so we can patch Providers onto defs later.
private seenComponents = new Set<Type<any>>();
private seenDirectives = new Set<Type<any>>();
Expand Down Expand Up @@ -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<any>): void {
private applyProviderOverridesInScope(
type: Type<any>, parentStandaloneComponentType?: Type<unknown>): void {
const hasScope = isStandaloneComponent(type) || isNgModule(type);

// The function can be re-entered recursively while inspecting dependencies
Expand All @@ -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<Provider|InternalEnvironmentProviders> = [
...injectorDef.providers,
...(this.providerOverridesByModule.get(type as InjectorType<any>) || [])
];
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');
Expand Down Expand Up @@ -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
Expand All @@ -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');
}
});
}
}
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -900,6 +925,7 @@ export class TestBedCompiler {

private patchDefWithProviderOverrides(declaration: Type<any>, field: string): void {
const def = (declaration as any)[field];
debugger;
if (def && def.providersResolver) {
this.maybeStoreNgDef(field, declaration);

Expand Down

0 comments on commit cd5f09b

Please sign in to comment.