Skip to content

Commit 4f1a813

Browse files
AndrewKushniralxhub
authored andcommitted
fix(core): restore NgModule state correctly after TestBed overrides (#46049)
This commit updates the NgModule logic to account for a case when a type has more than one generated def. This is a common situation for NgModules which have at least two: ɵmod and ɵinj. Previously, the second def was not stored before applying overrides, thus leaving it modified after the test, leaking the state as a result. This fix ensures that we store all defs before applying any overrides. PR Close #46049
1 parent 2c52284 commit 4f1a813

File tree

2 files changed

+69
-18
lines changed

2 files changed

+69
-18
lines changed

packages/core/test/test_bed_spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,44 @@ describe('TestBed with Standalone types', () => {
413413
const app = fixture.componentInstance;
414414
expect(app.testCmpCtrl.testField).toBe('overridden');
415415
});
416+
417+
it('should allow removing an import via `overrideModule`', () => {
418+
const fooToken = new InjectionToken<string>('foo');
419+
420+
@NgModule({
421+
providers: [{provide: fooToken, useValue: 'FOO'}],
422+
})
423+
class ImportedModule {
424+
}
425+
426+
@NgModule({
427+
imports: [ImportedModule],
428+
})
429+
class ImportingModule {
430+
}
431+
432+
TestBed.configureTestingModule({
433+
imports: [ImportingModule],
434+
});
435+
436+
TestBed.overrideModule(ImportingModule, {
437+
remove: {
438+
imports: [ImportedModule],
439+
},
440+
});
441+
442+
expect(TestBed.inject(fooToken, 'BAR')).toBe('BAR');
443+
444+
// Emulate an end of a test.
445+
TestBed.resetTestingModule();
446+
447+
// Emulate the start of a next test, make sure previous overrides
448+
// are not persisted across tests.
449+
TestBed.configureTestingModule({
450+
imports: [ImportingModule],
451+
});
452+
expect(TestBed.inject(fooToken, 'BAR')).toBe('FOO');
453+
});
416454
});
417455
});
418456

packages/core/testing/src/r3_test_bed_compiler.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,10 @@ export class R3TestBedCompiler {
7171

7272
// Map that keeps initial version of component/directive/pipe defs in case
7373
// we compile a Type again, thus overriding respective static fields. This is
74-
// required to make sure we restore defs to their initial states between test runs
75-
// TODO: we should support the case with multiple defs on a type
76-
private initialNgDefs = new Map<Type<any>, [string, PropertyDescriptor|undefined]>();
74+
// required to make sure we restore defs to their initial states between test runs.
75+
// Note: one class may have multiple defs (for example: ɵmod and ɵinj in case of an
76+
// NgModule), store all of them in a map.
77+
private initialNgDefs = new Map<Type<any>, Map<string, PropertyDescriptor|undefined>>();
7778

7879
// Array that keeps cleanup operations for initial versions of component/directive/pipe/module
7980
// defs in case TestBed makes changes to the originals.
@@ -623,10 +624,20 @@ export class R3TestBedCompiler {
623624
return affectedModules;
624625
}
625626

627+
/**
628+
* Preserve an original def (such as ɵmod, ɵinj, etc) before applying an override.
629+
* Note: one class may have multiple defs (for example: ɵmod and ɵinj in case of
630+
* an NgModule). If there is a def in a set already, don't override it, since
631+
* an original one should be restored at the end of a test.
632+
*/
626633
private maybeStoreNgDef(prop: string, type: Type<any>) {
627634
if (!this.initialNgDefs.has(type)) {
635+
this.initialNgDefs.set(type, new Map());
636+
}
637+
const currentDefs = this.initialNgDefs.get(type)!;
638+
if (!currentDefs.has(prop)) {
628639
const currentDef = Object.getOwnPropertyDescriptor(type, prop);
629-
this.initialNgDefs.set(type, [prop, currentDef]);
640+
currentDefs.set(prop, currentDef);
630641
}
631642
}
632643

@@ -668,20 +679,22 @@ export class R3TestBedCompiler {
668679
op.object[op.fieldName] = op.originalValue;
669680
});
670681
// Restore initial component/directive/pipe defs
671-
this.initialNgDefs.forEach((value: [string, PropertyDescriptor|undefined], type: Type<any>) => {
672-
const [prop, descriptor] = value;
673-
if (!descriptor) {
674-
// Delete operations are generally undesirable since they have performance implications
675-
// on objects they were applied to. In this particular case, situations where this code
676-
// is invoked should be quite rare to cause any noticeable impact, since it's applied
677-
// only to some test cases (for example when class with no annotations extends some
678-
// @Component) when we need to clear 'ɵcmp' field on a given class to restore
679-
// its original state (before applying overrides and running tests).
680-
delete (type as any)[prop];
681-
} else {
682-
Object.defineProperty(type, prop, descriptor);
683-
}
684-
});
682+
this.initialNgDefs.forEach(
683+
(defs: Map<string, PropertyDescriptor|undefined>, type: Type<any>) => {
684+
defs.forEach((descriptor, prop) => {
685+
if (!descriptor) {
686+
// Delete operations are generally undesirable since they have performance
687+
// implications on objects they were applied to. In this particular case, situations
688+
// where this code is invoked should be quite rare to cause any noticeable impact,
689+
// since it's applied only to some test cases (for example when class with no
690+
// annotations extends some @Component) when we need to clear 'ɵcmp' field on a given
691+
// class to restore its original state (before applying overrides and running tests).
692+
delete (type as any)[prop];
693+
} else {
694+
Object.defineProperty(type, prop, descriptor);
695+
}
696+
});
697+
});
685698
this.initialNgDefs.clear();
686699
this.moduleProvidersOverridden.clear();
687700
this.restoreComponentResolutionQueue();

0 commit comments

Comments
 (0)