Skip to content

Commit

Permalink
fix: respecting mock keep switch in nested modules
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Nov 3, 2020
1 parent dc078af commit 2f185fb
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 27 deletions.
19 changes: 11 additions & 8 deletions lib/mock-builder/mock-builder-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -64,21 +64,26 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
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;
}
Expand Down Expand Up @@ -112,7 +117,7 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {

// 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;
}
Expand Down Expand Up @@ -317,9 +322,7 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
}

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);
Expand All @@ -332,7 +335,7 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
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');
Expand Down
8 changes: 2 additions & 6 deletions lib/mock-builder/mock-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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, {});
Expand Down
41 changes: 29 additions & 12 deletions lib/mock-module/mock-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
}

Expand All @@ -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) {
Expand All @@ -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);
}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/issue-222/injector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
81 changes: 81 additions & 0 deletions tests/issue-222/mock-keep-priorities.spec.ts
Original file line number Diff line number Diff line change
@@ -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>shared</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('<shared></shared>');
});
});

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>shared</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>shared</shared>');
});
});
});

0 comments on commit 2f185fb

Please sign in to comment.