diff --git a/libs/ng-mocks/src/lib/mock-builder/mock-builder.ts b/libs/ng-mocks/src/lib/mock-builder/mock-builder.ts index bf2f1836cb..452d9b8acd 100644 --- a/libs/ng-mocks/src/lib/mock-builder/mock-builder.ts +++ b/libs/ng-mocks/src/lib/mock-builder/mock-builder.ts @@ -1,9 +1,11 @@ -import { InjectionToken } from '@angular/core'; -import { MetadataOverride, TestBed, TestBedStatic, TestModuleMetadata } from '@angular/core/testing'; +import { InjectionToken, NgModule } from '@angular/core'; +import { getTestBed, MetadataOverride, TestBed, TestBedStatic, TestModuleMetadata } from '@angular/core/testing'; +import coreConfig from '../common/core.config'; import { flatten, mapEntries } from '../common/core.helpers'; +import coreReflectModuleResolve from '../common/core.reflect.module-resolve'; import { NG_MOCKS, NG_MOCKS_OVERRIDES } from '../common/core.tokens'; -import { AnyType } from '../common/core.types'; +import { AnyType, Type } from '../common/core.types'; import { isNgDef } from '../common/func.is-ng-def'; import ngMocksUniverse from '../common/ng-mocks-universe'; import { ngMocks } from '../mock-helper/mock-helper'; @@ -50,6 +52,96 @@ const applyOverrides = (testBed: TestBedStatic, overrides: Map, Met } }; +const applyPlatformOverridesNormalization = ( + module: Type | Array>, + mocks: Map, + resetSet: Set, + track: Set, + callback: any, +): module is Type => { + // istanbul ignore if + if (Array.isArray(module)) { + for (const moduleCtor of module) { + callback(moduleCtor, mocks, resetSet, track); + } + + return false; + } + // istanbul ignore if + if (track.has(module)) { + return false; + } + track.add(module); + + return true; +}; + +const applyPlatformOverride = (overrides: any, ctorDef: AnyType, mock: AnyType, prop: string): boolean => { + const bucketAdd: any[] = overrides.add[prop] || []; + bucketAdd.push(mock); + overrides.add[prop] = bucketAdd; + + const bucketRemove: any[] = overrides.remove[prop] || []; + bucketRemove.push(ctorDef); + overrides.remove[prop] = bucketRemove; + + return true; +}; + +const applyPlatformOverridesGetMock = (mocks: Map, ctorDef: any): AnyType | undefined => { + const mock = mocks.get(ctorDef); + if (mock && mock !== ctorDef && coreConfig.neverMockModule.indexOf(ctorDef) !== -1) { + return mock; + } + + return undefined; +}; + +const applyPlatformOverridesData = (module: AnyType): Array<['imports' | 'exports', any]> => { + const result: Array<['imports' | 'exports', any]> = []; + const meta = coreReflectModuleResolve(module); + for (const prop of ['imports', 'exports'] as const) { + for (const ctorDef of meta[prop] || []) { + result.push([prop, ctorDef]); + } + } + + return result; +}; + +const applyPlatformOverrides = ( + module: Type | Array>, + mocks: Map, + resetSet: Set, + track: Set, +): void => { + // istanbul ignore if + if (!applyPlatformOverridesNormalization(module, mocks, resetSet, track, applyPlatformOverrides)) { + return; + } + + let changed = false; + const overrides: MetadataOverride = { add: {}, remove: {} }; + + for (const [prop, ctorDef] of applyPlatformOverridesData(module)) { + if (!isNgDef(ctorDef, 'm')) { + continue; + } + + const mock = applyPlatformOverridesGetMock(mocks, ctorDef); + if (mock) { + changed = applyPlatformOverride(overrides, ctorDef, mock, prop); + } else { + applyPlatformOverrides(ctorDef, mocks, resetSet, track); + } + } + + if (changed) { + resetSet.add(module); + TestBed.overrideModule(module, overrides); + } +}; + // Thanks Ivy and its TestBed.override - it does not clean up leftovers. const applyNgMocksOverrides = (testBed: TestBedStatic & { ngMocksOverrides?: any }): void => { if (testBed.ngMocksOverrides) { @@ -60,6 +152,8 @@ const applyNgMocksOverrides = (testBed: TestBedStatic & { ngMocksOverrides?: any testBed.overrideComponent(def, {}); } else if (isNgDef(def, 'd')) { testBed.overrideDirective(def, {}); + } else if (isNgDef(def, 'm')) { + testBed.overrideModule(def, {}); } } testBed.ngMocksOverrides = undefined; @@ -75,9 +169,9 @@ const configureTestingModule = ( if (mocks) { ngMocks.flushTestBed(); } - const testBed = original.call(TestBed, moduleDef); + const testBedStatic = original.call(TestBed, moduleDef); if (!mocks) { - return testBed; + return testBedStatic; } // istanbul ignore else @@ -87,10 +181,11 @@ const configureTestingModule = ( } // istanbul ignore else if (overrides) { - applyOverrides(testBed, overrides); + applyOverrides(testBedStatic, overrides); } + applyPlatformOverrides(getTestBed().ngModule, mocks, (TestBed as any).ngMocksOverrides, new Set()); - return testBed; + return testBedStatic; }; const resetTestingModule = ( diff --git a/tests/issue-435/test.spec.ts b/tests/issue-435/test.spec.ts new file mode 100644 index 0000000000..d1991fa868 --- /dev/null +++ b/tests/issue-435/test.spec.ts @@ -0,0 +1,44 @@ +import { CommonModule, DatePipe } from '@angular/common'; +import { Component, NgModule } from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +@Component({ + selector: 'target', + template: `{{ stamp | date }}`, +}) +class TargetComponent { + public readonly stamp = '2021-05-01'; +} + +@NgModule({ + declarations: [TargetComponent], + imports: [CommonModule], +}) +class TargetModule {} + +describe('issue-435', () => { + describe('mock pipe', () => { + beforeEach(() => + MockBuilder(TargetComponent, TargetModule).mock( + DatePipe, + (value: string) => `MOCK:${value}`, + ), + ); + + it('mocks declarations from CommonModule', () => { + const fixture = MockRender(TargetComponent); + expect(ngMocks.formatText(fixture)).toEqual('MOCK:2021-05-01'); + }); + }); + + describe('normal pipe', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + + it('restores original declarations from CommonModule', () => { + const fixture = MockRender(TargetComponent); + expect(ngMocks.formatText(fixture)).not.toEqual( + 'MOCK:2021-05-01', + ); + }); + }); +});