From 233f014387993ad24c599cb433cf7dcaba01d75b Mon Sep 17 00:00:00 2001 From: satanTime Date: Sat, 26 Nov 2022 14:29:34 +0100 Subject: [PATCH] fix(core): correct caching of touched declarations #4344 --- .../src/lib/common/core.def-stack.spec.ts | 19 ++++ .../ng-mocks/src/lib/common/core.def-stack.ts | 53 +++++++++ libs/ng-mocks/src/lib/common/func.is-mock.ts | 2 +- .../lib/mock-builder/mock-builder.promise.ts | 5 +- .../promise/handle-root-providers.ts | 6 +- .../src/lib/mock-helper/mock-helper.guts.ts | 3 +- .../src/lib/mock-module/create-resolvers.ts | 19 +++- .../src/lib/mock-module/mock-module.ts | 27 ++--- .../src/lib/mock-module/mock-ng-def.ts | 12 +- libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts | 3 +- .../mock-service/helper.resolve-provider.ts | 3 +- libs/ng-mocks/src/lib/mock/clone-providers.ts | 7 +- .../src/lib/mock/decorate-declaration.ts | 38 +++++-- libs/ng-mocks/src/lib/mock/get-mock.ts | 4 +- .../src/lib/mock/return-cached-mock.ts | 10 ++ tests/issue-4344/standalone.spec.ts | 107 ++++++++++++++++++ tests/issue-4344/test.spec.ts | 60 ++++++++++ 17 files changed, 331 insertions(+), 47 deletions(-) create mode 100644 libs/ng-mocks/src/lib/common/core.def-stack.spec.ts create mode 100644 libs/ng-mocks/src/lib/common/core.def-stack.ts create mode 100644 libs/ng-mocks/src/lib/mock/return-cached-mock.ts create mode 100644 tests/issue-4344/standalone.spec.ts create mode 100644 tests/issue-4344/test.spec.ts diff --git a/libs/ng-mocks/src/lib/common/core.def-stack.spec.ts b/libs/ng-mocks/src/lib/common/core.def-stack.spec.ts new file mode 100644 index 0000000000..c37448ecc9 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/core.def-stack.spec.ts @@ -0,0 +1,19 @@ +import CoreDefStack from './core.def-stack'; + +describe('CoreDefStack', () => { + it('returns empty map on empty pop', () => { + const stack = new CoreDefStack(); + + const result1 = stack.pop(); + const result2 = stack.pop(); + + expect(result1).toBeDefined(); + expect(result2).toBeDefined(); + expect(result1).not.toBe(result2); + }); + + it('returns undefined on empty get', () => { + const stack = new CoreDefStack(); + expect(stack.get(CoreDefStack)).toBeUndefined(); + }); +}); diff --git a/libs/ng-mocks/src/lib/common/core.def-stack.ts b/libs/ng-mocks/src/lib/common/core.def-stack.ts new file mode 100644 index 0000000000..25e1bb5813 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/core.def-stack.ts @@ -0,0 +1,53 @@ +import { mapEntries } from './core.helpers'; + +export default class { + protected stack: Array> = []; + + public constructor() { + this.push(); + } + + public push() { + this.stack.push(new Map()); + } + + public pop(): Map { + return this.stack.pop() ?? new Map(); + } + + public has(key: K): ReturnType['has']> { + for (let i = this.stack.length - 1; i >= 0; i -= 1) { + if (this.stack[i].has(key)) { + return true; + } + } + + return false; + } + + public get(key: K): ReturnType['get']> { + for (let i = this.stack.length - 1; i >= 0; i -= 1) { + if (this.stack[i].has(key)) { + return this.stack[i].get(key); + } + } + + return undefined; + } + + public set(key: K, value: V): this { + for (let i = this.stack.length - 1; i >= 0; i -= 1) { + this.stack[i].set(key, value); + } + + return this; + } + + public merge(resolutions: Map): this { + for (const [key, value] of mapEntries(resolutions)) { + this.set(key, value); + } + + return this; + } +} diff --git a/libs/ng-mocks/src/lib/common/func.is-mock.ts b/libs/ng-mocks/src/lib/common/func.is-mock.ts index 81900e3ed7..051a461c77 100644 --- a/libs/ng-mocks/src/lib/common/func.is-mock.ts +++ b/libs/ng-mocks/src/lib/common/func.is-mock.ts @@ -10,5 +10,5 @@ export default ( __template?: TemplateRef; __vcr?: ViewContainerRef; } => { - return value && typeof value === 'object' && !!(value as any).__ngMocksConfig; + return value && typeof value === 'object' && !!(value as any).__ngMocks; }; 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 c0c6c97c8c..da9bfb1766 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 @@ -1,6 +1,7 @@ import { NgModule, Provider } from '@angular/core'; import { TestBed, TestBedStatic, TestModuleMetadata } from '@angular/core/testing'; +import CoreDefStack from '../common/core.def-stack'; import { flatten, mapValues } from '../common/core.helpers'; import { Type } from '../common/core.types'; import funcGetName from '../common/func.get-name'; @@ -69,7 +70,7 @@ export class MockBuilderPromise implements IMockBuilder { public build(): TestModuleMetadata { this.stash.backup(); - ngMocksUniverse.config.set('mockNgDefResolver', new Map()); + ngMocksUniverse.config.set('mockNgDefResolver', new CoreDefStack()); ngMocksUniverse.flags.add('hasRootModule'); try { @@ -77,7 +78,7 @@ export class MockBuilderPromise implements IMockBuilder { const ngModule = initNgModules(params, initUniverse(params)); addRequestedProviders(ngModule, params); - handleRootProviders(ngModule, params); + handleRootProviders(ngModule, params, ngMocksUniverse.config.get('mockNgDefResolver')); handleEntryComponents(ngModule); applyPlatformModules(); diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/handle-root-providers.ts b/libs/ng-mocks/src/lib/mock-builder/promise/handle-root-providers.ts index 8029195edd..3c2d0fbfdc 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/handle-root-providers.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/handle-root-providers.ts @@ -1,3 +1,4 @@ +import CoreDefStack from '../../common/core.def-stack'; import { mapValues } from '../../common/core.helpers'; import { NG_MOCKS_ROOT_PROVIDERS } from '../../common/core.tokens'; import { isNgInjectionToken } from '../../common/func.is-ng-injection-token'; @@ -9,13 +10,12 @@ import getRootProviderParameters from './get-root-provider-parameters'; import { BuilderData, NgMeta } from './types'; // Mocking root providers. -export default (ngModule: NgMeta, { keepDef, mockDef }: BuilderData): void => { +export default (ngModule: NgMeta, { keepDef, mockDef }: BuilderData, resolutions: CoreDefStack): void => { // Adding missed providers. const parameters = keepDef.has(NG_MOCKS_ROOT_PROVIDERS) ? new Set() : getRootProviderParameters(mockDef); if (parameters.size > 0) { - const parametersMap = new Map(); for (const parameter of mapValues(parameters)) { - const mock = helperResolveProvider(parameter, parametersMap); + const mock = helperResolveProvider(parameter, resolutions); if (mock) { ngModule.providers.push(mock); } else if (isNgInjectionToken(parameter)) { diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.guts.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.guts.ts index 096492f1b5..cf33896d84 100644 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.guts.ts +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.guts.ts @@ -1,5 +1,6 @@ import { TestModuleMetadata } from '@angular/core/testing'; +import CoreDefStack from '../common/core.def-stack'; import { flatten, mapKeys, mapValues } from '../common/core.helpers'; import coreReflectModuleResolve from '../common/core.reflect.module-resolve'; import funcGetProvider from '../common/func.get-provider'; @@ -226,7 +227,7 @@ export default (keep: any, mock: any = null, exclude: any = null): TestModuleMet resolutions.set(mockDef, 'exclude'); } - ngMocksUniverse.config.set('mockNgDefResolver', new Map()); + ngMocksUniverse.config.set('mockNgDefResolver', new CoreDefStack()); for (const def of mapValues(data.mock)) { resolutions.set(def, 'mock'); if (data.optional.has(def)) { diff --git a/libs/ng-mocks/src/lib/mock-module/create-resolvers.ts b/libs/ng-mocks/src/lib/mock-module/create-resolvers.ts index d2e0cdf27c..88d7f80e85 100644 --- a/libs/ng-mocks/src/lib/mock-module/create-resolvers.ts +++ b/libs/ng-mocks/src/lib/mock-module/create-resolvers.ts @@ -1,5 +1,6 @@ import { Provider } from '@angular/core'; +import CoreDefStack from '../common/core.def-stack'; 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'; @@ -35,14 +36,18 @@ const processDef = (def: any) => { // resolveProvider is a special case because of the def structure. const createResolveProvider = - (resolutions: Map, change: () => void): ((def: Provider) => any) => + (resolutions: CoreDefStack, change: () => void): ((def: Provider) => any) => (def: Provider) => helperMockService.resolveProvider(def, resolutions, change); const createResolveWithProviders = (def: any, mockDef: any): boolean => mockDef && mockDef.ngModule && isNgModuleDefWithProviders(def); -const createResolveExisting = (def: any, resolutions: Map, change: (flag?: boolean) => void): any => { +const createResolveExisting = ( + def: any, + resolutions: CoreDefStack, + change: (flag?: boolean) => void, +): any => { const mockDef = resolutions.get(def); if (def !== mockDef) { change(); @@ -51,14 +56,18 @@ const createResolveExisting = (def: any, resolutions: Map, change: (fl return mockDef; }; -const createResolveExcluded = (def: any, resolutions: Map, change: (flag?: boolean) => void): void => { +const createResolveExcluded = ( + def: any, + resolutions: CoreDefStack, + change: (flag?: boolean) => void, +): void => { resolutions.set(def, undefined); change(); }; const createResolve = - (resolutions: Map, change: (flag?: boolean) => void): ((def: any) => any) => + (resolutions: CoreDefStack, change: (flag?: boolean) => void): ((def: any) => any) => (def: any) => { if (resolutions.has(def)) { return createResolveExisting(def, resolutions, change); @@ -85,7 +94,7 @@ const createResolve = export default ( change: () => void, - resolutions: Map, + resolutions: CoreDefStack, ): { resolve: (def: any) => any; resolveProvider: (def: Provider) => any; 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 8012cf36b8..e8646f0075 100644 --- a/libs/ng-mocks/src/lib/mock-module/mock-module.ts +++ b/libs/ng-mocks/src/lib/mock-module/mock-module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import coreConfig from '../common/core.config'; +import coreDefineProperty from '../common/core.define-property'; import { extendClass } from '../common/core.helpers'; import coreReflectModuleResolve from '../common/core.reflect.module-resolve'; import { AnyType, Type } from '../common/core.types'; @@ -12,6 +13,7 @@ import { isNgDef } from '../common/func.is-ng-def'; import { isNgModuleDefWithProviders, NgModuleWithProviders } from '../common/func.is-ng-module-def-with-providers'; import { Mock } from '../common/mock'; import ngMocksUniverse from '../common/ng-mocks-universe'; +import returnCachedMock from '../mock/return-cached-mock'; import mockNgDef from './mock-ng-def'; @@ -104,7 +106,7 @@ const getExistingMockModule = (ngModule: Type, isRootModule: boolean): Type // Every module should be replaced with its mock copy only once to avoid errors like: // Failed: Type ...Component is part of the declarations of 2 modules: ...Module and ...Module... if (ngMocksUniverse.flags.has('cacheModule') && ngMocksUniverse.cacheDeclarations.has(ngModule)) { - return ngMocksUniverse.cacheDeclarations.get(ngModule); + return returnCachedMock(ngModule); } // Now we check if we need to keep the original module or to replace it with some other. @@ -122,27 +124,20 @@ const getExistingMockModule = (ngModule: Type, isRootModule: boolean): Type return undefined; }; -const getMockModuleDef = (ngModule: Type, mockModule?: Type): NgModule | undefined => { - if (!mockModule) { - const meta = coreReflectModuleResolve(ngModule); - const [changed, ngModuleDef] = mockNgDef(meta, ngModule); - if (changed) { - return ngModuleDef; - } - } - - return undefined; -}; - const detectMockModule = (ngModule: Type, mockModule?: Type): Type => { - const mockModuleDef = getMockModuleDef(ngModule, mockModule); + const [changed, ngModuleDef, resolutions] = mockModule + ? [false] + : mockNgDef(coreReflectModuleResolve(ngModule), ngModule); + if (resolutions) { + coreDefineProperty(ngModule, '__ngMocksResolutions', resolutions); + } - if (mockModuleDef) { + if (changed) { const parent = ngMocksUniverse.flags.has('skipMock') ? ngModule : Mock; const mock = extendClass(parent); // the last thing is to apply decorators. - NgModule(mockModuleDef)(mock); + NgModule(ngModuleDef)(mock); decorateMock(mock, ngModule); return mock; 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 e77f74de0e..9455a0c839 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 @@ -1,5 +1,6 @@ import { Component, Directive, NgModule, Pipe, Provider } from '@angular/core'; +import CoreDefStack from '../common/core.def-stack'; import { flatten } from '../common/core.helpers'; import { dependencyKeys, Type } from '../common/core.types'; import { isNgModuleDefWithProviders } from '../common/func.is-ng-module-def-with-providers'; @@ -18,11 +19,11 @@ const configureProcessMetaKeys = ( resolveProvider: (def: Provider) => any, ): Array<[dependencyKeys, (def: any) => any]> => [ ['declarations', resolve], + ['imports', resolve], ['entryComponents', resolve], ['bootstrap', resolve], ['providers', resolveProvider], ['viewProviders', resolveProvider], - ['imports', resolve], ['exports', resolve], ['schemas', v => v], ]; @@ -117,11 +118,13 @@ export default ( skipMarkProviders?: boolean; }, ngModule?: Type, -): [boolean, NgModule] => { +): [boolean, NgModule, Map] => { const hasResolver = ngMocksUniverse.config.has('mockNgDefResolver'); if (!hasResolver) { - ngMocksUniverse.config.set('mockNgDefResolver', new Map()); + ngMocksUniverse.config.set('mockNgDefResolver', new CoreDefStack()); } + ngMocksUniverse.config.get('mockNgDefResolver').push(); + let changed = !ngMocksUniverse.flags.has('skipMock'); const change = (flag = true) => { changed = changed || flag; @@ -130,9 +133,10 @@ export default ( const mockModuleDef = processMeta(ngModuleDef, resolve, resolveProvider); addExports(resolve, change, ngModuleDef, mockModuleDef, ngModule); + const resolutions = ngMocksUniverse.config.get('mockNgDefResolver').pop(); if (!hasResolver) { ngMocksUniverse.config.delete('mockNgDefResolver'); } - return [changed, mockModuleDef]; + return [changed, mockModuleDef, resolutions]; }; diff --git a/libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts b/libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts index 8de061565c..806765a3f0 100644 --- a/libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts +++ b/libs/ng-mocks/src/lib/mock-pipe/mock-pipe.ts @@ -10,6 +10,7 @@ import { isMockNgDef } from '../common/func.is-mock-ng-def'; import { Mock } from '../common/mock'; import ngMocksUniverse from '../common/ng-mocks-universe'; import helperMockService from '../mock-service/helper.mock-service'; +import returnCachedMock from '../mock/return-cached-mock'; import { MockedPipe } from './types'; @@ -79,7 +80,7 @@ export function MockPipe( // istanbul ignore next if (ngMocksUniverse.flags.has('cachePipe') && ngMocksUniverse.cacheDeclarations.has(pipe)) { - return ngMocksUniverse.cacheDeclarations.get(pipe); + return returnCachedMock(pipe); } const mock = getMockClass(pipe, transform); diff --git a/libs/ng-mocks/src/lib/mock-service/helper.resolve-provider.ts b/libs/ng-mocks/src/lib/mock-service/helper.resolve-provider.ts index 0ce5328575..3268950342 100644 --- a/libs/ng-mocks/src/lib/mock-service/helper.resolve-provider.ts +++ b/libs/ng-mocks/src/lib/mock-service/helper.resolve-provider.ts @@ -1,3 +1,4 @@ +import CoreDefStack from '../common/core.def-stack'; import { extractDependency } from '../common/core.helpers'; import { NG_MOCKS_INTERCEPTORS } from '../common/core.tokens'; import funcExtractForwardRef from '../common/func.extract-forward-ref'; @@ -179,7 +180,7 @@ const isPreconfiguredUseExisting = (provider: any, provide: any): boolean => { }; // tries to resolve a provider based on current universe state. -export default (provider: any, resolutions: Map, changed?: () => void) => { +export default (provider: any, resolutions: CoreDefStack, changed?: () => void) => { const { provide, multi, change } = parseProvider(provider, changed); if (isPreconfiguredDependency(provider, provide)) { return change(); diff --git a/libs/ng-mocks/src/lib/mock/clone-providers.ts b/libs/ng-mocks/src/lib/mock/clone-providers.ts index 259189048a..7d2887094a 100644 --- a/libs/ng-mocks/src/lib/mock/clone-providers.ts +++ b/libs/ng-mocks/src/lib/mock/clone-providers.ts @@ -1,5 +1,6 @@ import { Provider } from '@angular/core'; +import CoreDefStack from '../common/core.def-stack'; import coreForm from '../common/core.form'; import { flatten } from '../common/core.helpers'; import { AnyType } from '../common/core.types'; @@ -49,7 +50,7 @@ const processProvider = ( sourceType: AnyType, mockType: AnyType, provider: any, - resolutions: Map, + resolutions: CoreDefStack, ): any => { const token = processTokens(mockType, provider); if (token) { @@ -67,14 +68,14 @@ const processProvider = ( export default ( sourceType: AnyType, mockType: AnyType, - providers?: any[], + providers: any[], + resolutions: CoreDefStack, ): { providers: Provider[]; setControlValueAccessor?: boolean; } => { const result: Provider[] = []; let setControlValueAccessor: boolean | undefined; - const resolutions = new Map(); for (const provider of flatten(providers || /* istanbul ignore next */ [])) { const provide = funcGetProvider(provider); diff --git a/libs/ng-mocks/src/lib/mock/decorate-declaration.ts b/libs/ng-mocks/src/lib/mock/decorate-declaration.ts index eb772e6cfb..71206080ad 100644 --- a/libs/ng-mocks/src/lib/mock/decorate-declaration.ts +++ b/libs/ng-mocks/src/lib/mock/decorate-declaration.ts @@ -1,5 +1,6 @@ import { Component, Directive, NgModule, ViewChild } from '@angular/core'; +import CoreDefStack from '../common/core.def-stack'; import { AnyType } from '../common/core.types'; import decorateInputs from '../common/decorate.inputs'; import decorateMock from '../common/decorate.mock'; @@ -44,6 +45,11 @@ export default ( }, params: T, ): Component & Directive => { + const hasResolver = ngMocksUniverse.config.has('mockNgDefResolver'); + if (!hasResolver) { + ngMocksUniverse.config.set('mockNgDefResolver', new CoreDefStack()); + } + const options: T & { imports?: any[]; standalone?: boolean } = { ...params, }; @@ -58,22 +64,32 @@ export default ( options.standalone = meta.standalone; } - const { setControlValueAccessor, providers } = cloneProviders(source, mock, meta.providers || []); + if (meta.standalone && meta.imports) { + const [, { imports }] = mockNgDef({ imports: meta.imports }); + if (imports?.length) { + options.imports = imports as never; + } + } + + const { setControlValueAccessor, providers } = cloneProviders( + source, + mock, + meta.providers || [], + ngMocksUniverse.config.get('mockNgDefResolver'), + ); providers.push(toExistingProvider(source, mock)); options.providers = providers; - const { providers: viewProviders } = cloneProviders(source, mock, meta.viewProviders || []); + const { providers: viewProviders } = cloneProviders( + source, + mock, + meta.viewProviders || [], + ngMocksUniverse.config.get('mockNgDefResolver'), + ); if (viewProviders.length > 0) { options.viewProviders = viewProviders; } - if (meta.standalone && meta.imports) { - const { imports } = mockNgDef({ imports: meta.imports })[1]; - if (imports?.length) { - options.imports = imports as never; - } - } - const config: ngMocksMockConfig = buildConfig( source, meta, @@ -107,5 +123,9 @@ export default ( } } + if (!hasResolver) { + ngMocksUniverse.config.delete('mockNgDefResolver'); + } + return options; }; diff --git a/libs/ng-mocks/src/lib/mock/get-mock.ts b/libs/ng-mocks/src/lib/mock/get-mock.ts index 02b0215ab9..5d80094652 100644 --- a/libs/ng-mocks/src/lib/mock/get-mock.ts +++ b/libs/ng-mocks/src/lib/mock/get-mock.ts @@ -3,6 +3,8 @@ import funcImportExists from '../common/func.import-exists'; import { isMockNgDef } from '../common/func.is-mock-ng-def'; import ngMocksUniverse from '../common/ng-mocks-universe'; +import returnCachedMock from './return-cached-mock'; + export default (def: any, type: any, func: string, cacheFlag: string, base: any, decorator: any) => { funcImportExists(def, func); @@ -11,7 +13,7 @@ export default (def: any, type: any, func: string, cacheFlag: string, base: any, } if (ngMocksUniverse.flags.has(cacheFlag) && ngMocksUniverse.cacheDeclarations.has(def)) { - return ngMocksUniverse.cacheDeclarations.get(def); + return returnCachedMock(def); } const hasNgMocksDepsResolution = ngMocksUniverse.config.has('ngMocksDepsResolution'); diff --git a/libs/ng-mocks/src/lib/mock/return-cached-mock.ts b/libs/ng-mocks/src/lib/mock/return-cached-mock.ts new file mode 100644 index 0000000000..fd6cf00d5b --- /dev/null +++ b/libs/ng-mocks/src/lib/mock/return-cached-mock.ts @@ -0,0 +1,10 @@ +import ngMocksUniverse from '../common/ng-mocks-universe'; + +export default (declaration: any) => { + const result = ngMocksUniverse.cacheDeclarations.get(declaration); + if (declaration.__ngMocksResolutions && ngMocksUniverse.config.has('mockNgDefResolver')) { + ngMocksUniverse.config.get('mockNgDefResolver').merge(declaration.__ngMocksResolutions); + } + + return result; +}; diff --git a/tests/issue-4344/standalone.spec.ts b/tests/issue-4344/standalone.spec.ts new file mode 100644 index 0000000000..9cc8f99d58 --- /dev/null +++ b/tests/issue-4344/standalone.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: [DecimalPipe, 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', () => { + 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); + }); +}); diff --git a/tests/issue-4344/test.spec.ts b/tests/issue-4344/test.spec.ts new file mode 100644 index 0000000000..9355446a83 --- /dev/null +++ b/tests/issue-4344/test.spec.ts @@ -0,0 +1,60 @@ +import { + AsyncPipe, + CommonModule, + DecimalPipe, +} from '@angular/common'; +import { Component, Injectable, NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +import { isMockOf, MockModule, MockRender, ngMocks } from 'ng-mocks'; + +@Injectable() +class TargetService {} + +@Component({ + selector: 'target', + template: '{{ 1 | number }}', + providers: [AsyncPipe], +}) +export class TargetComponent { + constructor( + public readonly service: TargetService, + public readonly pipe: AsyncPipe, + ) {} +} + +@NgModule({ + declarations: [TargetComponent], + imports: [CommonModule], + exports: [DecimalPipe, TargetComponent], + providers: [TargetService, AsyncPipe], +}) +export class TargetModule {} + +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', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MockModule(CommonModule), MockModule(TargetModule)], + }).compileComponents(); + }); + + 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); + }); +});