Skip to content

Commit

Permalink
feat(core): hidden usage of MockBuilder in TestBed if kept and mock m…
Browse files Browse the repository at this point in the history
…odules are used together help-me-mom#4344
  • Loading branch information
satanTime committed Dec 4, 2022
1 parent 9f68244 commit e06c20e
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 11 deletions.
45 changes: 41 additions & 4 deletions libs/ng-mocks/src/lib/common/ng-mocks-global-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -205,7 +242,7 @@ const configureTestingModule =
applyPlatformOverrides(testBed, touches);
}

return original.call(instance, moduleDef);
return original.call(instance, finalModuleDef);
};

const resetTestingModule =
Expand Down
2 changes: 2 additions & 0 deletions libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -87,6 +88,7 @@ export class MockBuilderPromise implements IMockBuilder {
createNgMocksToken(),
createNgMocksTouchesToken(),
createNgMocksOverridesToken(this.replaceDef, this.defValue),
MockBuilder as never,
);

return ngModule;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */ [])],
Expand Down
2 changes: 1 addition & 1 deletion libs/ng-mocks/src/lib/mock-builder/promise/init-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
4 changes: 3 additions & 1 deletion libs/ng-mocks/src/lib/mock-module/mark-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
2 changes: 1 addition & 1 deletion libs/ng-mocks/src/lib/mock-module/mock-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const detectMockModule = (ngModule: Type<any>, mockModule?: Type<any>): Type<any

const getMockProviders = (ngModuleProviders: NgModule['providers']): NgModule['providers'] => {
if (ngModuleProviders) {
const [changed, ngModuleDef] = mockNgDef({ providers: ngModuleProviders });
const [changed, ngModuleDef] = mockNgDef({ providers: ngModuleProviders, skipExports: true });

return changed ? ngModuleDef.providers : ngModuleProviders;
}
Expand Down
10 changes: 9 additions & 1 deletion libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ const resolveDefForExport = (
return undefined;
}

ngMocksUniverse.configInstance.set(instance, {
...ngMocksUniverse.configInstance.get(instance),
exported: true,
});

return mockDef;
};

Expand Down Expand Up @@ -116,6 +121,7 @@ const addExports = (
export default (
ngModuleDef: NgModule & {
skipMarkProviders?: boolean;
skipExports?: boolean;
},
ngModule?: Type<any>,
): [boolean, NgModule, Map<any, any>] => {
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion libs/ng-mocks/src/lib/mock/decorate-declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default <T extends Component & Directive>(
}

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;
}
Expand Down
15 changes: 14 additions & 1 deletion libs/ng-mocks/src/lib/mock/return-cached-mock.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Expand Down
70 changes: 70 additions & 0 deletions tests-e2e/src/issue-4344/test.spec.ts
Original file line number Diff line number Diff line change
@@ -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:
'<cdk-virtual-scroll-viewport [itemSize]="15"></cdk-virtual-scroll-viewport>',
})
class DependencyComponent {}

@NgModule({
imports: [ScrollingModule],
declarations: [DependencyComponent],
exports: [DependencyComponent, ScrollingModule],
})
class DependencyModule {}

@Component({
selector: 'target',
template:
'<dependency></dependency><cdk-virtual-scroll-viewport [itemSize]="15"></cdk-virtual-scroll-viewport>',
})
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();
});
});
});
107 changes: 107 additions & 0 deletions tests/issue-4344/standalone-explicit.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit e06c20e

Please sign in to comment.