From 2f185fb9ca7be3d96abb14e37f700d76826a13de Mon Sep 17 00:00:00 2001 From: MG Date: Tue, 3 Nov 2020 18:16:51 +0100 Subject: [PATCH] fix: respecting mock keep switch in nested modules --- lib/mock-builder/mock-builder-promise.ts | 19 +++-- lib/mock-builder/mock-builder.ts | 8 +- lib/mock-module/mock-module.ts | 41 +++++++--- tests/issue-222/injector.spec.ts | 2 +- tests/issue-222/mock-keep-priorities.spec.ts | 81 ++++++++++++++++++++ 5 files changed, 124 insertions(+), 27 deletions(-) create mode 100644 tests/issue-222/mock-keep-priorities.spec.ts diff --git a/lib/mock-builder/mock-builder-promise.ts b/lib/mock-builder/mock-builder-promise.ts index 3e6a8b12e3..b19ac028dc 100644 --- a/lib/mock-builder/mock-builder-promise.ts +++ b/lib/mock-builder/mock-builder-promise.ts @@ -2,7 +2,7 @@ import { InjectionToken, NgModule, PipeTransform, Provider } from '@angular/core import { MetadataOverride, TestBed } from '@angular/core/testing'; import { extractDependency, flatten, mapEntries, mapValues } from '../common/core.helpers'; -import { directiveResolver, jitReflector, ngModuleResolver } from '../common/core.reflect'; +import { directiveResolver, jitReflector } from '../common/core.reflect'; import { NG_MOCKS, NG_MOCKS_OVERRIDES, NG_MOCKS_ROOT_PROVIDERS, NG_MOCKS_TOUCHES } from '../common/core.tokens'; import { AnyType, Type } from '../common/core.types'; import { isNgDef } from '../common/func.is-ng-def'; @@ -64,21 +64,26 @@ export class MockBuilderPromise implements PromiseLike { ngMocksUniverse.config.set('multi', new Set()); // collecting multi flags of providers. ngMocksUniverse.config.set('deps', new Set()); // collecting all deps of providers. ngMocksUniverse.config.set('depsSkip', new Set()); // collecting all declarations of kept modules. + ngMocksUniverse.config.set('resolution', new Map()); // flags to understand how to mock nested declarations. for (const def of mapValues(this.keepDef)) { ngMocksUniverse.builder.set(def, def); + ngMocksUniverse.config.get('resolution').set(def, 'keep'); } - for (const source of mapValues(this.replaceDef)) { - ngMocksUniverse.builder.set(source, this.defValue.get(source)); + for (const def of mapValues(this.replaceDef)) { + ngMocksUniverse.builder.set(def, this.defValue.get(def)); + ngMocksUniverse.config.get('resolution').set(def, 'replace'); } for (const def of [...mapValues(this.excludeDef)]) { ngMocksUniverse.builder.set(def, null); + ngMocksUniverse.config.get('resolution').set(def, 'exclude'); } // mocking requested things. for (const def of mapValues(this.mockDef)) { + ngMocksUniverse.config.get('resolution').set(def, 'mock'); if (isNgDef(def)) { continue; } @@ -112,7 +117,7 @@ export class MockBuilderPromise implements PromiseLike { // Now we need to run through requested modules. const defProviders = new Map(); - for (const def of [...mapValues(this.mockDef), ...mapValues(this.keepDef), ...mapValues(this.replaceDef)]) { + for (const def of [...mapValues(this.keepDef), ...mapValues(this.mockDef), ...mapValues(this.replaceDef)]) { if (!isNgDef(def, 'm')) { continue; } @@ -317,9 +322,7 @@ export class MockBuilderPromise implements PromiseLike { } let meta: NgModule | undefined; - if (isNgDef(value, 'm')) { - meta = ngModuleResolver.resolve(value); - } else if (isNgDef(value, 'c')) { + if (isNgDef(value, 'c')) { meta = directiveResolver.resolve(value); } else if (isNgDef(value, 'd')) { meta = directiveResolver.resolve(value); @@ -332,7 +335,7 @@ export class MockBuilderPromise implements PromiseLike { if (!skipMock) { ngMocksUniverse.flags.add('skipMock'); } - const [changed, def] = MockNgDef(meta); + const [changed, def] = MockNgDef({ providers: meta.providers }); /* istanbul ignore else */ if (!skipMock) { ngMocksUniverse.flags.delete('skipMock'); diff --git a/lib/mock-builder/mock-builder.ts b/lib/mock-builder/mock-builder.ts index b7ededf082..3b26c1ba8f 100644 --- a/lib/mock-builder/mock-builder.ts +++ b/lib/mock-builder/mock-builder.ts @@ -53,9 +53,7 @@ export function MockBuilder( for (const [def, override] of mapEntries(overrides)) { (TestBed as any).ngMocksOverrides.add(def); /* istanbul ignore else */ - if (isNgDef(def, 'm')) { - testBed.overrideModule(def, override); - } else if (isNgDef(def, 'c')) { + if (isNgDef(def, 'c')) { testBed.overrideComponent(def, override); } else if (isNgDef(def, 'd')) { testBed.overrideDirective(def, override); @@ -81,9 +79,7 @@ export function MockBuilder( ngMocks.flushTestBed(); for (const def of (TestBed as any).ngMocksOverrides) { /* istanbul ignore else */ - if (isNgDef(def, 'm')) { - TestBed.overrideModule(def, {}); - } else if (isNgDef(def, 'c')) { + if (isNgDef(def, 'c')) { TestBed.overrideComponent(def, {}); } else if (isNgDef(def, 'd')) { TestBed.overrideDirective(def, {}); diff --git a/lib/mock-module/mock-module.ts b/lib/mock-module/mock-module.ts index 4b350cac86..2727a86736 100644 --- a/lib/mock-module/mock-module.ts +++ b/lib/mock-module/mock-module.ts @@ -32,7 +32,7 @@ export function MockModule(module: any): any { let mockModule: typeof ngModule | undefined; let mockModuleProviders: typeof ngModuleProviders; let mockModuleDef: NgModule | undefined; - let releaseSkipMockFlag = false; + let toggleSkipMockFlag = false; if (isNgModuleDefWithProviders(module)) { ngModule = module.ngModule; @@ -59,8 +59,24 @@ export function MockModule(module: any): any { mockModule = ngMocksUniverse.cacheMocks.get(ngModule); } + const resolution: undefined | 'mock' | 'keep' | 'replace' | 'exclude' = ngMocksUniverse.config + .get('resolution') + ?.get(ngModule); + if (resolution === 'mock' && ngMocksUniverse.flags.has('skipMock')) { + toggleSkipMockFlag = true; + ngMocksUniverse.flags.delete('skipMock'); + } + if (resolution === 'keep' && !ngMocksUniverse.flags.has('skipMock')) { + toggleSkipMockFlag = true; + ngMocksUniverse.flags.add('skipMock'); + } + if (resolution === 'replace' && !ngMocksUniverse.flags.has('skipMock')) { + toggleSkipMockFlag = true; + ngMocksUniverse.flags.add('skipMock'); + } + if (ngConfig.neverMockModule.indexOf(ngModule) !== -1 && !ngMocksUniverse.flags.has('skipMock')) { - releaseSkipMockFlag = true; + toggleSkipMockFlag = true; ngMocksUniverse.flags.add('skipMock'); } @@ -70,10 +86,6 @@ export function MockModule(module: any): any { if (isNgDef(instance, 'm') && instance !== ngModule) { mockModule = instance; } - if (!ngMocksUniverse.flags.has('skipMock')) { - releaseSkipMockFlag = true; - ngMocksUniverse.flags.add('skipMock'); - } } if (!mockModule) { @@ -98,15 +110,18 @@ export function MockModule(module: any): any { // the last thing is to apply decorators. NgModule(mockModuleDef)(mockModule as any); MockOf(ngModule)(mockModule as any); - - /* istanbul ignore else */ - if (ngMocksUniverse.flags.has('cacheModule')) { - ngMocksUniverse.cacheMocks.set(ngModule, mockModule); - } } if (!mockModule) { mockModule = ngModule; } + + // We should always cache the result, in global scope it always will be a mock. + // In MockBuilder scope it will be reset later anyway. + /* istanbul ignore else */ + if (ngMocksUniverse.flags.has('cacheModule')) { + ngMocksUniverse.cacheMocks.set(ngModule, mockModule); + } + if (ngMocksUniverse.flags.has('skipMock')) { ngMocksUniverse.config.get('depsSkip')?.add(mockModule); } @@ -116,8 +131,10 @@ export function MockModule(module: any): any { mockModuleProviders = changed ? ngModuleDef.providers : ngModuleProviders; } - if (releaseSkipMockFlag) { + if (toggleSkipMockFlag && ngMocksUniverse.flags.has('skipMock')) { ngMocksUniverse.flags.delete('skipMock'); + } else if (toggleSkipMockFlag && !ngMocksUniverse.flags.has('skipMock')) { + ngMocksUniverse.flags.add('skipMock'); } return mockModule === ngModule && mockModuleProviders === ngModuleProviders diff --git a/tests/issue-222/injector.spec.ts b/tests/issue-222/injector.spec.ts index 6ae466bbba..729294d415 100644 --- a/tests/issue-222/injector.spec.ts +++ b/tests/issue-222/injector.spec.ts @@ -25,7 +25,7 @@ class TargetComponent { }) class TargetModule {} -describe('issue-222', () => { +describe('issue-222:Injector', () => { beforeEach(() => MockBuilder(TargetComponent, TargetModule)); it('does not mock Injector, fails on ivy only', () => { diff --git a/tests/issue-222/mock-keep-priorities.spec.ts b/tests/issue-222/mock-keep-priorities.spec.ts new file mode 100644 index 0000000000..f3fccba8cf --- /dev/null +++ b/tests/issue-222/mock-keep-priorities.spec.ts @@ -0,0 +1,81 @@ +import { Component, NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +// This directive is shared via a module that is actually is kept in one and is mocked in another one. +@Component({ + selector: 'shared', + template: 'shared', +}) +class SharedComponent {} + +@NgModule({ + declarations: [SharedComponent], + exports: [SharedComponent], +}) +class SharedModule {} + +@NgModule({ + exports: [SharedModule], + imports: [SharedModule], +}) +class MockModule {} + +@NgModule({ + exports: [SharedModule], + imports: [SharedModule], +}) +class KeepModule {} + +@Component({ + selector: 'target', + template: 'target', +}) +class TargetComponent {} + +@NgModule({ + bootstrap: [TargetComponent], + declarations: [TargetComponent], + imports: [BrowserModule, BrowserAnimationsModule, KeepModule, MockModule], + providers: [], +}) +export class TargetModule {} + +describe('issue-222:mock-keep-priorities', () => { + describe('keep', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(KeepModule)); + + it('keeps all child imports', () => { + const fixture = MockRender(SharedComponent); + expect(fixture.nativeElement.innerHTML).toEqual('shared'); + }); + }); + + describe('mock', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(KeepModule).mock(SharedModule)); + + it('mocks the nested module of a kept module', () => { + const fixture = MockRender(SharedComponent); + expect(fixture.nativeElement.innerHTML).toEqual(''); + }); + }); + + describe('reverse', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).mock(KeepModule).keep(SharedModule)); + + it('keeps the nested module of a mocked module', () => { + const fixture = MockRender(SharedComponent); + expect(fixture.nativeElement.innerHTML).toEqual('shared'); + }); + }); + + describe('mock keep priority', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(KeepModule).mock(MockModule)); + + it('keep wins', () => { + const fixture = MockRender(SharedComponent); + expect(fixture.nativeElement.innerHTML).toEqual('shared'); + }); + }); +});