diff --git a/libs/ng-mocks/src/lib/common/core.helpers.ts b/libs/ng-mocks/src/lib/common/core.helpers.ts index 6a0cdbc353..ef8ce32f4f 100644 --- a/libs/ng-mocks/src/lib/common/core.helpers.ts +++ b/libs/ng-mocks/src/lib/common/core.helpers.ts @@ -36,13 +36,18 @@ export const getInjection = (token: AnyDeclaration): I => { return testBed.inject ? testBed.inject(token) : testBed.get(token); }; -export const flatten = (values: T | T[], result: T[] = []): T[] => { +export const flatten = (values: T | T[] | { ɵproviders: T[] }, result: T[] = []): T[] => { if (Array.isArray(values)) { for (const value of values) { flatten(value, result); } + } else if (values !== null && typeof values === 'object' && Array.isArray((values as any).ɵproviders)) { + for (const value of (values as any).ɵproviders) { + flatten(value, result); + } } else { - result.push(values); + // any is needed to cover ɵproviders + result.push(values as any); } return result; diff --git a/libs/ng-mocks/src/lib/common/func.extract-deps.ts b/libs/ng-mocks/src/lib/common/func.extract-deps.ts index d42b26afcb..49a610122f 100644 --- a/libs/ng-mocks/src/lib/common/func.extract-deps.ts +++ b/libs/ng-mocks/src/lib/common/func.extract-deps.ts @@ -6,7 +6,11 @@ import { AnyDeclaration } from './core.types'; import { getNgType } from './func.get-ng-type'; import funcGetType from './func.get-type'; -export const funcExtractDeps = (def: any, result: Set>): Set> => { +export const funcExtractDeps = ( + def: any, + result: Set>, + recursive = false, +): Set> => { const meta = collectDeclarations(def); const type = getNgType(def); // istanbul ignore if @@ -22,7 +26,13 @@ export const funcExtractDeps = (def: any, result: Set>): Set for (const item of flatten(decorator[field])) { // istanbul ignore if: it is here for standalone things, however they don't support modules with providers. - result.add(funcGetType(item)); + const itemType = funcGetType(item); + if (!result.has(itemType)) { + result.add(itemType); + if (recursive) { + funcExtractDeps(itemType, result); + } + } } } diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts index f1779c1d40..7bc231c4fd 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts @@ -1,4 +1,5 @@ import { mapEntries, mapValues } from '../../common/core.helpers'; +import { funcExtractDeps } from '../../common/func.extract-deps'; import ngMocksUniverse from '../../common/ng-mocks-universe'; import initExcludeDef from './init-exclude-def'; @@ -31,8 +32,20 @@ export default ({ const dependencies = initKeepDef(keepDef, configDef); for (const dependency of mapValues(dependencies)) { ngMocksUniverse.touches.add(dependency); - - // MockBuilder has instruction about the dependency, skipping it. + } + for (const dependency of mapValues(keepDef)) { + dependencies.add(dependency); + funcExtractDeps(dependency, dependencies, true); + } + for (const dependency of mapValues(mockDef)) { + dependencies.add(dependency); + funcExtractDeps(dependency, dependencies, true); + } + for (const dependency of mapValues(replaceDef)) { + dependencies.add(dependency); + funcExtractDeps(dependency, dependencies, true); + } + for (const dependency of mapValues(dependencies)) { if (configDef.has(dependency)) { continue; } @@ -46,14 +59,21 @@ export default ({ keepDef.add(dependency); } else if (resolution === 'exclude') { excludeDef.add(dependency); - } else { + } else if (resolution === 'mock') { + mockDef.add(dependency); + } else if (ngMocksUniverse.touches.has(dependency)) { mockDef.add(dependency); } - configDef.set(dependency, { - dependency: true, - __internal: true, - }); + configDef.set( + dependency, + ngMocksUniverse.touches.has(dependency) + ? { + dependency: true, + __internal: true, + } + : {}, + ); } for (const [k, v] of mapEntries(configDef)) { diff --git a/tests/issue-6402/test.spec.ts b/tests/issue-6402/test.spec.ts new file mode 100644 index 0000000000..01e866a6ef --- /dev/null +++ b/tests/issue-6402/test.spec.ts @@ -0,0 +1,73 @@ +import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { Injectable, NgModule } from '@angular/core'; + +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +@Injectable() +class TargetService { + constructor(private http: HttpClient) {} + + getConfig() { + return this.http.get('/api/config'); + } +} + +@NgModule({ + imports: [HttpClientModule], + providers: [TargetService], +}) +class TargetModule {} + +// The issue was that MockBuilder didn't apply global rules to mocked declarations. +// @see /~https://github.com/help-me-mom/ng-mocks/issues/6402 +describe('issue-6402', () => { + describe('MockBuilder:replace', () => { + beforeEach(() => + MockBuilder(TargetService, TargetModule).replace( + HttpClientModule, + HttpClientTestingModule, + ), + ); + + it('sends /api/config request', () => { + MockRender(TargetService); + const service = ngMocks.get(TargetService); + const controller = ngMocks.get(HttpTestingController); + + service.getConfig().subscribe(); + + const expectation = controller.expectOne('/api/config'); + expectation.flush([]); + controller.verify(); + expect(expectation.request.method).toEqual('GET'); + }); + }); + + describe('ngMocks.globalReplace', () => { + beforeAll(() => + ngMocks.globalReplace( + HttpClientModule, + HttpClientTestingModule, + ), + ); + beforeEach(() => MockBuilder(TargetService, TargetModule)); + afterAll(() => ngMocks.globalWipe(HttpClientModule)); + + it('sends /api/config request', () => { + MockRender(TargetService); + const service = ngMocks.get(TargetService); + const controller = ngMocks.get(HttpTestingController); + + service.getConfig().subscribe(); + + const expectation = controller.expectOne('/api/config'); + expectation.flush([]); + controller.verify(); + expect(expectation.request.method).toEqual('GET'); + }); + }); +});