diff --git a/libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts b/libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts index 3b2cacdc1c..ae3b9d6525 100644 --- a/libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts +++ b/libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts @@ -2,6 +2,7 @@ import { Injector, ViewContainerRef } from '@angular/core'; import { getTestBed, MetadataOverride, TestBed, TestBedStatic, TestModuleMetadata } from '@angular/core/testing'; import funcExtractTokens from '../mock-builder/func.extract-tokens'; +import { MockBuilder } from '../mock-builder/mock-builder'; import getOverrideDef from '../mock-builder/promise/get-override-def'; import { ngMocks } from '../mock-helper/mock-helper'; import mockHelperFasterInstall from '../mock-helper/mock-helper.faster-install'; @@ -15,9 +16,11 @@ import coreInjector from './core.injector'; import coreReflectMeta from './core.reflect.meta'; import coreReflectModuleResolve from './core.reflect.module-resolve'; import coreReflectProvidedIn from './core.reflect.provided-in'; -import { NG_MOCKS, NG_MOCKS_TOUCHES } from './core.tokens'; +import { NG_MOCKS, NG_MOCKS_ROOT_PROVIDERS, NG_MOCKS_TOUCHES } from './core.tokens'; import { AnyType, dependencyKeys } from './core.types'; import funcGetProvider from './func.get-provider'; +import { getSourceOfMock } from './func.get-source-of-mock'; +import { isMockNgDef } from './func.is-mock-ng-def'; import { isNgDef } from './func.is-ng-def'; import { isNgModuleDefWithProviders } from './func.is-ng-module-def-with-providers'; import ngMocksUniverse from './ng-mocks-universe'; @@ -183,13 +186,47 @@ const configureTestingModule = (moduleDef: TestModuleMetadata) => { initTestBed(); + const useMockBuilder = + typeof moduleDef === 'object' && + !!moduleDef && + (!moduleDef.providers || moduleDef.providers.indexOf(MockBuilder) === -1); + // 0b10 - mock exist + // 0b01 - real exist + let hasMocks = 0; + const mockBuilder: Array<[any, boolean]> = []; + for (const key of useMockBuilder ? ['imports', 'declarations'] : []) { + for (const declaration of flatten(moduleDef[key as never])) { + if (!declaration) { + continue; + } + mockBuilder.push([getSourceOfMock(declaration), isMockNgDef(declaration)]); + if (key === 'imports') { + hasMocks |= mockBuilder[mockBuilder.length - 1][1] ? 0b10 : 0b01; + } + } + } + // We should do magic only then both mock and real exist. + let finalModuleDef = hasMocks === 0b11 ? undefined : moduleDef; + if (!finalModuleDef) { + let builder = MockBuilder(NG_MOCKS_ROOT_PROVIDERS); + for (const [def, isMock] of mockBuilder) { + builder = isMock ? builder.mock(def) : builder.keep(def); + } + finalModuleDef = builder.build(); + finalModuleDef = { + ...moduleDef, + ...finalModuleDef, + providers: [...(moduleDef.providers ?? []), ...(finalModuleDef.providers as never)], + }; + } + const testBed = getTestBed(); - const providers = funcExtractTokens(moduleDef.providers); + const providers = funcExtractTokens(finalModuleDef.providers); const { mocks, overrides } = providers; // touches are important, // therefore we are trying to fetch them from the known providers. - const touches = defineTouches(testBed, moduleDef, providers.touches); + const touches = defineTouches(testBed, finalModuleDef, providers.touches); if (mocks) { ngMocks.flushTestBed(); @@ -205,7 +242,7 @@ const configureTestingModule = applyPlatformOverrides(testBed, touches); } - return original.call(instance, moduleDef); + return original.call(instance, finalModuleDef); }; const resetTestingModule = diff --git a/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts b/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts index b720f8bc54..33fb7ccbf7 100644 --- a/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts +++ b/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts @@ -9,6 +9,7 @@ import { isNgDef } from '../common/func.is-ng-def'; import { isNgModuleDefWithProviders } from '../common/func.is-ng-module-def-with-providers'; import ngMocksUniverse from '../common/ng-mocks-universe'; +import { MockBuilder } from './mock-builder'; import { MockBuilderStash } from './mock-builder-stash'; import addRequestedProviders from './promise/add-requested-providers'; import applyPlatformModules from './promise/apply-platform-modules'; @@ -87,6 +88,7 @@ export class MockBuilderPromise implements IMockBuilder { createNgMocksToken(), createNgMocksTouchesToken(), createNgMocksOverridesToken(this.replaceDef, this.defValue), + MockBuilder as never, ); return ngModule; diff --git a/libs/ng-mocks/src/lib/mock-builder/performance/required-metadata.ts b/libs/ng-mocks/src/lib/mock-builder/performance/required-metadata.ts index e972d71ca1..dd0b46c2ce 100644 --- a/libs/ng-mocks/src/lib/mock-builder/performance/required-metadata.ts +++ b/libs/ng-mocks/src/lib/mock-builder/performance/required-metadata.ts @@ -2,11 +2,12 @@ import { TestModuleMetadata } from '@angular/core/testing'; export default ( ngModule: TestModuleMetadata, -): { +): TestModuleMetadata & { declarations: any[]; imports: any[]; providers: any[]; } => ({ + ...ngModule, declarations: [...(ngModule.declarations || /* istanbul ignore next */ [])], imports: [...(ngModule.imports || /* istanbul ignore next */ [])], providers: [...(ngModule.providers || /* istanbul ignore next */ [])], diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-modules.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-modules.ts index 4f9adabb9a..b4dbd17059 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-modules.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-modules.ts @@ -28,7 +28,7 @@ export default ( const isModule = isNgDef(def, 'm'); if (providers.length > 0) { - const [, loDef] = mockNgDef({ providers, skipMarkProviders: !isModule }); + const [, loDef] = mockNgDef({ providers, skipMarkProviders: !isModule, skipExports: true }); loProviders.set(def, loDef.providers); } if (isModule) { diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts index efbe1f6ca6..76adb9e57c 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts @@ -70,6 +70,8 @@ export default ( if (!config.dependency && config.export && !configInstance?.exported && (isNgDef(def, 'i') || !isNgDef(def))) { handleDef(meta, def, defProviders); markProviders([def]); + } else if (!config.dependency && config.export && !configInstance?.exported) { + handleDef(meta, def, defProviders); } else if (!ngMocksUniverse.touches.has(def) && !config.dependency) { handleDef(meta, def, defProviders); } else if ( diff --git a/libs/ng-mocks/src/lib/mock-module/mark-providers.ts b/libs/ng-mocks/src/lib/mock-module/mark-providers.ts index 2ef17768f8..de54c36458 100644 --- a/libs/ng-mocks/src/lib/mock-module/mark-providers.ts +++ b/libs/ng-mocks/src/lib/mock-module/mark-providers.ts @@ -7,7 +7,9 @@ export default (providers?: any[]): void => { const provide = funcGetProvider(provider); const config = ngMocksUniverse.configInstance.get(provide) ?? {}; - config.exported = true; + if (!config.exported) { + config.exported = true; + } ngMocksUniverse.configInstance.set(provide, config); } }; diff --git a/libs/ng-mocks/src/lib/mock-module/mock-module.ts b/libs/ng-mocks/src/lib/mock-module/mock-module.ts index e8646f0075..a2fd1bdfa4 100644 --- a/libs/ng-mocks/src/lib/mock-module/mock-module.ts +++ b/libs/ng-mocks/src/lib/mock-module/mock-module.ts @@ -148,7 +148,7 @@ const detectMockModule = (ngModule: Type, mockModule?: Type): Type { if (ngModuleProviders) { - const [changed, ngModuleDef] = mockNgDef({ providers: ngModuleProviders }); + const [changed, ngModuleDef] = mockNgDef({ providers: ngModuleProviders, skipExports: true }); return changed ? ngModuleDef.providers : ngModuleProviders; } diff --git a/libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts b/libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts index 9455a0c839..4c033b6075 100644 --- a/libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts +++ b/libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts @@ -85,6 +85,11 @@ const resolveDefForExport = ( return undefined; } + ngMocksUniverse.configInstance.set(instance, { + ...ngMocksUniverse.configInstance.get(instance), + exported: true, + }); + return mockDef; }; @@ -116,6 +121,7 @@ const addExports = ( export default ( ngModuleDef: NgModule & { skipMarkProviders?: boolean; + skipExports?: boolean; }, ngModule?: Type, ): [boolean, NgModule, Map] => { @@ -131,7 +137,9 @@ export default ( }; const { resolve, resolveProvider } = createResolvers(change, ngMocksUniverse.config.get('mockNgDefResolver')); const mockModuleDef = processMeta(ngModuleDef, resolve, resolveProvider); - addExports(resolve, change, ngModuleDef, mockModuleDef, ngModule); + if (!ngModuleDef.skipExports) { + addExports(resolve, change, ngModuleDef, mockModuleDef, ngModule); + } const resolutions = ngMocksUniverse.config.get('mockNgDefResolver').pop(); if (!hasResolver) { diff --git a/libs/ng-mocks/src/lib/mock/decorate-declaration.ts b/libs/ng-mocks/src/lib/mock/decorate-declaration.ts index 71206080ad..71e0f74175 100644 --- a/libs/ng-mocks/src/lib/mock/decorate-declaration.ts +++ b/libs/ng-mocks/src/lib/mock/decorate-declaration.ts @@ -65,7 +65,7 @@ export default ( } if (meta.standalone && meta.imports) { - const [, { imports }] = mockNgDef({ imports: meta.imports }); + const [, { imports }] = mockNgDef({ imports: meta.imports, skipExports: true }); if (imports?.length) { options.imports = imports as never; } diff --git a/libs/ng-mocks/src/lib/mock/return-cached-mock.ts b/libs/ng-mocks/src/lib/mock/return-cached-mock.ts index fd6cf00d5b..98a545ccb6 100644 --- a/libs/ng-mocks/src/lib/mock/return-cached-mock.ts +++ b/libs/ng-mocks/src/lib/mock/return-cached-mock.ts @@ -1,7 +1,20 @@ +import { NG_MOCKS } from '../common/core.tokens'; import ngMocksUniverse from '../common/ng-mocks-universe'; +import funcGetLastFixture from '../mock-helper/func.get-last-fixture'; export default (declaration: any) => { - const result = ngMocksUniverse.cacheDeclarations.get(declaration); + let result: any; + + try { + result = funcGetLastFixture().debugElement.injector.get(NG_MOCKS).get(declaration); + } catch { + // nothing to do. + } + + if (!result) { + result = ngMocksUniverse.cacheDeclarations.get(declaration); + } + if (declaration.__ngMocksResolutions && ngMocksUniverse.config.has('mockNgDefResolver')) { ngMocksUniverse.config.get('mockNgDefResolver').merge(declaration.__ngMocksResolutions); } diff --git a/tests-e2e/src/issue-4344/test.spec.ts b/tests-e2e/src/issue-4344/test.spec.ts new file mode 100644 index 0000000000..5b44268117 --- /dev/null +++ b/tests-e2e/src/issue-4344/test.spec.ts @@ -0,0 +1,70 @@ +import { + CdkFixedSizeVirtualScroll, + ScrollingModule, +} from '@angular/cdk/scrolling'; +import { Component, NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + MockBuilder, + MockModule, + MockRender, + ngMocks, +} from 'ng-mocks'; + +@Component({ + selector: 'dependency', + template: + '', +}) +class DependencyComponent {} + +@NgModule({ + imports: [ScrollingModule], + declarations: [DependencyComponent], + exports: [DependencyComponent, ScrollingModule], +}) +class DependencyModule {} + +@Component({ + selector: 'target', + template: + '', +}) +class TargetComponent {} + +@NgModule({ + imports: [DependencyModule], + declarations: [TargetComponent], + exports: [TargetComponent], +}) +class TargetModule {} + +// @see https://github.com/help-me-mom/ng-mocks/issues/4344 +// Type CdkFixedSizeVirtualScroll is part of the declarations of 2 modules: +// MockOfScrollingModule and ScrollingModule! +// Please consider moving CdkFixedSizeVirtualScroll to a higher module +// that imports MockOfScrollingModule and ScrollingModule. +describe('issue-4344', () => { + beforeAll(() => ngMocks.globalKeep(CdkFixedSizeVirtualScroll)); + afterAll(() => ngMocks.globalWipe(CdkFixedSizeVirtualScroll)); + + describe('TestBed', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [MockModule(DependencyModule), TargetModule], + }).compileComponents(), + ); + + it('creates TargetComponent', () => { + expect(() => MockRender(TargetComponent)).not.toThrow(); + }); + }); + + describe('MockBuilder', () => { + beforeEach(() => MockBuilder(TargetModule, DependencyModule)); + + it('creates TargetComponent', () => { + expect(() => MockRender(TargetComponent)).not.toThrow(); + }); + }); +}); diff --git a/tests/issue-4344/standalone-explicit.spec.ts b/tests/issue-4344/standalone-explicit.spec.ts new file mode 100644 index 0000000000..e225f84fd7 --- /dev/null +++ b/tests/issue-4344/standalone-explicit.spec.ts @@ -0,0 +1,107 @@ +import { + AsyncPipe, + CommonModule, + DecimalPipe, +} from '@angular/common'; +import { + Component, + Injectable, + NgModule, + VERSION, +} from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +import { + isMockOf, + MockComponent, + MockModule, + MockRender, + ngMocks, +} from 'ng-mocks'; + +@Injectable() +class TargetService {} + +@Component({ + selector: 'target', + template: '{{ 1 | number }}', +}) +class TargetComponent { + constructor( + public readonly service: TargetService, + public readonly pipe: AsyncPipe, + ) {} +} +@NgModule({ + declarations: [TargetComponent], + imports: [CommonModule], + exports: [CommonModule, TargetComponent], + providers: [TargetService, AsyncPipe], +}) +class TargetModule {} + +@Component( + { + selector: 'standalone', + template: '{{ 1 | number }}', + standalone: true, + imports: [TargetModule], + providers: [AsyncPipe], + } as never /* TODO: remove after upgrade to a14 */, +) +class StandaloneComponent { + constructor( + public readonly service: TargetService, + public readonly pipe: AsyncPipe, + ) {} +} + +ngMocks.globalKeep(TargetComponent); +ngMocks.globalMock(TargetModule); + +// @see https://github.com/help-me-mom/ng-mocks/issues/4344 +// exporting AsyncPipe from CommonModule which is kept, +// causes an issue, because ng-mocks mocks AsyncPipe, whereas it shouldn't. +// That happens because a previously checked CommonModule doesn't expose its guts anymore. +describe('issue-4344:standalone:explicit', () => { + if (Number.parseInt(VERSION.major, 10) < 14) { + it('needs >=a14', () => { + expect(true).toBeTruthy(); + }); + + return; + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MockModule(CommonModule), + MockComponent(StandaloneComponent), + MockModule(TargetModule), + ], + }).compileComponents(); + }); + + it('creates StandaloneComponent', () => { + expect(() => MockRender(StandaloneComponent)).not.toThrow(); + + const targetService = ngMocks.findInstance(TargetService); + expect(isMockOf(targetService, TargetService)).toEqual(true); + + const asyncPipe = ngMocks.findInstance(AsyncPipe); + expect(isMockOf(asyncPipe, AsyncPipe)).toEqual(false); + }); + + it('creates TargetComponent', () => { + expect(() => MockRender(TargetComponent)).not.toThrow(); + + const decimalPipe = ngMocks.findInstance(DecimalPipe); + expect(isMockOf(decimalPipe, DecimalPipe)).toEqual(false); + + const targetService = ngMocks.findInstance(TargetService); + expect(isMockOf(targetService, TargetService)).toEqual(true); + + const asyncPipe = ngMocks.findInstance(AsyncPipe); + expect(isMockOf(asyncPipe, AsyncPipe)).toEqual(false); + }); +});