diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts
index 6049dc3a9d..ff5235974c 100644
--- a/packages/core/test/test_bed_spec.ts
+++ b/packages/core/test/test_bed_spec.ts
@@ -359,6 +359,126 @@ describe('TestBed', () => {
});
});
+ describe('nested module overrides using TestBed.overrideModule', () => {
+ // Set up an NgModule hierarchy with two modules, A and B, each with their own component.
+ // Module B additionally re-exports module A. Also declare two mock components which can be
+ // used in tests to verify that overrides within this hierarchy are working correctly.
+
+ // ModuleA content:
+
+ @Component({
+ selector: 'comp-a',
+ template: 'comp-a content',
+ })
+ class CompA {
+ }
+
+ @Component({
+ selector: 'comp-a',
+ template: 'comp-a mock content',
+ })
+ class MockCompA {
+ }
+
+ @NgModule({
+ declarations: [CompA],
+ exports: [CompA],
+ })
+ class ModuleA {
+ }
+
+ // ModuleB content:
+
+ @Component({
+ selector: 'comp-b',
+ template: 'comp-b content',
+ })
+ class CompB {
+ }
+
+ @Component({
+ selector: 'comp-b',
+ template: 'comp-b mock content',
+ })
+ class MockCompB {
+ }
+
+ @NgModule({
+ imports: [ModuleA],
+ declarations: [CompB],
+ exports: [CompB, ModuleA],
+ })
+ class ModuleB {
+ }
+
+ // AppModule content:
+
+ @Component({
+ selector: 'app',
+ template: `
+
+
+ `,
+ })
+ class App {
+ }
+
+ @NgModule({
+ imports: [ModuleB],
+ exports: [ModuleB],
+ })
+ class AppModule {
+ }
+
+ it('should detect nested module override', () => {
+ TestBed
+ .configureTestingModule({
+ declarations: [App],
+ // AppModule -> ModuleB -> ModuleA (to be overridden)
+ imports: [AppModule],
+ })
+ .overrideModule(ModuleA, {
+ remove: {declarations: [CompA], exports: [CompA]},
+ add: {declarations: [MockCompA], exports: [MockCompA]}
+ })
+ .compileComponents();
+
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ // CompA is overridden, expect mock content.
+ expect(fixture.nativeElement.textContent).toContain('comp-a mock content');
+
+ // CompB is not overridden, expect original content.
+ expect(fixture.nativeElement.textContent).toContain('comp-b content');
+ });
+
+ it('should detect chained modules override', () => {
+ TestBed
+ .configureTestingModule({
+ declarations: [App],
+ // AppModule -> ModuleB (to be overridden) -> ModuleA (to be overridden)
+ imports: [AppModule],
+ })
+ .overrideModule(ModuleA, {
+ remove: {declarations: [CompA], exports: [CompA]},
+ add: {declarations: [MockCompA], exports: [MockCompA]}
+ })
+ .overrideModule(ModuleB, {
+ remove: {declarations: [CompB], exports: [CompB]},
+ add: {declarations: [MockCompB], exports: [MockCompB]}
+ })
+ .compileComponents();
+
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ // Both CompA and CompB are overridden, expect mock content for both.
+ expect(fixture.nativeElement.textContent).toContain('comp-a mock content');
+ expect(fixture.nativeElement.textContent).toContain('comp-b mock content');
+ });
+ });
+
describe('multi providers', () => {
const multiToken = new InjectionToken('multiToken');
const singleToken = new InjectionToken('singleToken');
diff --git a/packages/core/testing/src/r3_test_bed_compiler.ts b/packages/core/testing/src/r3_test_bed_compiler.ts
index 6d3a93c1fa..5fd7623f86 100644
--- a/packages/core/testing/src/r3_test_bed_compiler.ts
+++ b/packages/core/testing/src/r3_test_bed_compiler.ts
@@ -57,6 +57,9 @@ export class R3TestBedCompiler {
private seenComponents = new Set>();
private seenDirectives = new Set>();
+ // Keep track of overridden modules, so that we can collect all affected ones in the module tree.
+ private overriddenModules = new Set>();
+
// Store resolved styles for Components that have template overrides present and `styleUrls`
// defined at the same time.
private existingComponentStyles = new Map, string[]>();
@@ -88,7 +91,6 @@ export class R3TestBedCompiler {
private testModuleType: NgModuleType;
private testModuleRef: NgModuleRef|null = null;
- private hasModuleOverrides: boolean = false;
constructor(private platform: PlatformRef, private additionalModuleTypes: Type|Type[]) {
class DynamicTestModule {}
@@ -123,7 +125,7 @@ export class R3TestBedCompiler {
}
overrideModule(ngModule: Type, override: MetadataOverride): void {
- this.hasModuleOverrides = true;
+ this.overriddenModules.add(ngModule as NgModuleType);
// Compile the module right away.
this.resolvers.module.addOverride(ngModule, override);
@@ -348,21 +350,26 @@ export class R3TestBedCompiler {
}
private applyTransitiveScopes(): void {
+ if (this.overriddenModules.size > 0) {
+ // Module overrides (via `TestBed.overrideModule`) might affect scopes that were previously
+ // calculated and stored in `transitiveCompileScopes`. If module overrides are present,
+ // collect all affected modules and reset scopes to force their re-calculatation.
+ const testingModuleDef = (this.testModuleType as any)[NG_MOD_DEF];
+ const affectedModules = this.collectModulesAffectedByOverrides(testingModuleDef.imports);
+ if (affectedModules.size > 0) {
+ affectedModules.forEach(moduleType => {
+ this.storeFieldOfDefOnType(moduleType as any, NG_MOD_DEF, 'transitiveCompileScopes');
+ (moduleType as any)[NG_MOD_DEF].transitiveCompileScopes = null;
+ });
+ }
+ }
+
const moduleToScope = new Map|TestingModuleOverride, NgModuleTransitiveScopes>();
const getScopeOfModule =
(moduleType: Type|TestingModuleOverride): NgModuleTransitiveScopes => {
if (!moduleToScope.has(moduleType)) {
const isTestingModule = isTestingModuleOverride(moduleType);
const realType = isTestingModule ? this.testModuleType : moduleType as Type;
- // Module overrides (via TestBed.overrideModule) might affect scopes that were
- // previously calculated and stored in `transitiveCompileScopes`. If module overrides
- // are present, always re-calculate transitive scopes to have the most up-to-date
- // information available. The `moduleToScope` map avoids repeated re-calculation of
- // scopes for the same module.
- if (!isTestingModule && this.hasModuleOverrides) {
- this.storeFieldOfDefOnType(moduleType as any, NG_MOD_DEF, 'transitiveCompileScopes');
- (moduleType as any)[NG_MOD_DEF].transitiveCompileScopes = null;
- }
moduleToScope.set(moduleType, transitiveScopesFor(realType));
}
return moduleToScope.get(moduleType)!;
@@ -532,6 +539,46 @@ export class R3TestBedCompiler {
queueTypesFromModulesArrayRecur(arr);
}
+ // When module overrides (via `TestBed.overrideModule`) are present, it might affect all modules
+ // that import (even transitively) an overridden one. For all affected modules we need to
+ // recalculate their scopes for a given test run and restore original scopes at the end. The goal
+ // of this function is to collect all affected modules in a set for further processing. Example:
+ // if we have the following module hierarchy: A -> B -> C (where `->` means `imports`) and module
+ // `C` is overridden, we consider `A` and `B` as affected, since their scopes might become
+ // invalidated with the override.
+ private collectModulesAffectedByOverrides(arr: any[]): Set> {
+ const seenModules = new Set>();
+ const affectedModules = new Set>();
+ const calcAffectedModulesRecur = (arr: any[], path: NgModuleType[]): void => {
+ for (const value of arr) {
+ if (Array.isArray(value)) {
+ // If the value is an array, just flatten it (by invoking this function recursively),
+ // keeping "path" the same.
+ calcAffectedModulesRecur(value, path);
+ } else if (hasNgModuleDef(value)) {
+ if (seenModules.has(value)) {
+ // If we've seen this module before and it's included into "affected modules" list, mark
+ // the whole path that leads to that module as affected, but do not descend into its
+ // imports, since we already examined them before.
+ if (affectedModules.has(value)) {
+ path.forEach(item => affectedModules.add(item));
+ }
+ continue;
+ }
+ seenModules.add(value);
+ if (this.overriddenModules.has(value)) {
+ path.forEach(item => affectedModules.add(item));
+ }
+ // Examine module imports recursively to look for overridden modules.
+ const moduleDef = (value as any)[NG_MOD_DEF];
+ calcAffectedModulesRecur(maybeUnwrapFn(moduleDef.imports), path.concat(value));
+ }
+ }
+ };
+ calcAffectedModulesRecur(arr, []);
+ return affectedModules;
+ }
+
private maybeStoreNgDef(prop: string, type: Type) {
if (!this.initialNgDefs.has(type)) {
const currentDef = Object.getOwnPropertyDescriptor(type, prop);