Skip to content

Commit

Permalink
fix(MockBuilder): detecting parent modules to build correct TestBed #…
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Nov 18, 2023
1 parent 4453dce commit 2d0012c
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 34 deletions.
2 changes: 1 addition & 1 deletion libs/ng-mocks/src/lib/common/ng-mocks-universe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ ngMocksUniverse.getBuildDeclaration = (def: any): undefined | null | any => {
if (mode === 'exclude') {
return null;
}
if (mode === 'keep') {
if (!mode || mode === 'keep') {
return def;
}
if (mode === 'replace') {
Expand Down
2 changes: 1 addition & 1 deletion libs/ng-mocks/src/lib/mock-builder/promise/init-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ngMocksUniverse from '../../common/ng-mocks-universe';
import { BuilderData } from './types';

export default (def: Type<any>, defProviders: BuilderData['defProviders']): Type<any> | ModuleWithProviders<any> => {
const loModule = ngMocksUniverse.getBuildDeclaration(def);
const loModule = ngMocksUniverse.config.get('mockNgDefResolver').get(def) ?? ngMocksUniverse.getBuildDeclaration(def);
const loProviders = defProviders.has(def) ? defProviders.get(def) : undefined;

return loProviders
Expand Down
87 changes: 67 additions & 20 deletions libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { flatten, mapValues } from '../../common/core.helpers';
import coreReflectProvidedIn from '../../common/core.reflect.provided-in';
import { AnyDeclaration } from '../../common/core.types';
import { AnyDeclaration, Type } from '../../common/core.types';
import errorJestMock from '../../common/error.jest-mock';
import funcGetName from '../../common/func.get-name';
import funcGetType from '../../common/func.get-type';
import { isNgDef } from '../../common/func.is-ng-def';
import { isNgInjectionToken } from '../../common/func.is-ng-injection-token';
import { isStandalone } from '../../common/func.is-standalone';
import ngMocksUniverse from '../../common/ng-mocks-universe';
import markExported from '../../mock/mark-exported';
import markProviders from '../../mock-module/mark-providers';

import initModule from './init-module';
Expand Down Expand Up @@ -50,41 +51,87 @@ const handleDef = ({ imports, declarations, providers }: NgMeta, def: any, defPr
}

if (touched) {
markExported(def);
ngMocksUniverse.touches.add(def);
}
};

export default (
{ configDef, configDefault, keepDef, mockDef, replaceDef }: BuilderData,
defProviders: Map<any, any>,
): NgMeta => {
const isExportedOnRoot = (
def: any,
configInstance: Map<any, { exported?: Set<any> }>,
configDef: Map<any, any>,
): undefined | Type<any> => {
const cnfInstance = configInstance.get(def);
const cnfDef = configDef.get(def);

if (isNgDef(def, 'm') && cnfDef.onRoot) {
return def;
}

if (!cnfInstance?.exported) {
return def;
}

for (const parent of mapValues(cnfInstance.exported)) {
const returnModule = isExportedOnRoot(parent, configInstance, configDef);
// istanbul ignore else
if (returnModule) {
return returnModule;
}
}

return undefined;
};

const moveModulesUp = <T>(a: T, b: T) => {
const isA = isNgDef(a, 'm');
const isB = isNgDef(b, 'm');
if (isA && isB) {
return 0;
}
if (isA) {
return -1;
}
if (isB) {
return 1;
}
return 0;
};

export default ({ configDefault, keepDef, mockDef, replaceDef }: BuilderData, defProviders: Map<any, any>): NgMeta => {
const meta: NgMeta = { imports: [], declarations: [], providers: [] };

const processed: AnyDeclaration<any>[] = [];
const forgotten: AnyDeclaration<any>[] = [];

const defs = [...mapValues(mockDef), ...mapValues(keepDef), ...mapValues(replaceDef)];
defs.sort(moveModulesUp);

// Adding suitable leftovers.
for (const def of [...mapValues(mockDef), ...mapValues(keepDef), ...mapValues(replaceDef)]) {
const configInstance = ngMocksUniverse.configInstance.get(def);
const config = configDef.get(def);
for (const originalDef of defs) {
const def =
isNgDef(originalDef, 'm') && defProviders.has(originalDef)
? originalDef
: isExportedOnRoot(originalDef, ngMocksUniverse.configInstance, ngMocksUniverse.config);
if (!def || processed.indexOf(def) !== -1) {
continue;
}

const cnfDef = ngMocksUniverse.config.get(def);
processed.push(def);
cnfDef.onRoot = cnfDef.onRoot || !cnfDef.dependency;

if (isNgDef(def, 'm') && config.onRoot) {
if (isNgDef(def, 'm') && cnfDef.onRoot) {
handleDef(meta, def, defProviders);
} else if (
!config.dependency &&
config.export &&
!configInstance?.exported &&
(isNgDef(def, 'i') || !isNgDef(def))
) {
} else if (!cnfDef.dependency && cnfDef.export && (isNgDef(def, 'i') || !isNgDef(def))) {
handleDef(meta, def, defProviders);
markProviders([def]);
} else if (!config.dependency && isNgDef(def, 'm') && defProviders.has(def)) {
handleDef(meta, def, defProviders);
} else if (!config.dependency && config.export && !configInstance?.exported) {
} else if (!cnfDef.dependency && cnfDef.export) {
handleDef(meta, def, defProviders);
} else if (!ngMocksUniverse.touches.has(def) && !config.dependency) {
} else if (!ngMocksUniverse.touches.has(def) && !cnfDef.dependency) {
handleDef(meta, def, defProviders);
} else if (
config.dependency &&
cnfDef.dependency &&
configDefault.dependency &&
coreReflectProvidedIn(def) !== 'root' &&
(typeof def !== 'object' || !(def as any).__ngMocksSkip)
Expand Down
11 changes: 3 additions & 8 deletions libs/ng-mocks/src/lib/mock-module/mark-providers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { flatten } from '../common/core.helpers';
import funcGetType from '../common/func.get-type';
import ngMocksUniverse from '../common/ng-mocks-universe';
import markExported from '../mock/mark-exported';

export default (providers?: any[]): void => {
for (const provider of flatten(providers ?? [])) {
const provide = funcGetType(provider);

const config = ngMocksUniverse.configInstance.get(provide) ?? {};
if (!config.exported) {
config.exported = true;
}
ngMocksUniverse.configInstance.set(provide, config);
const instance = funcGetType(provider);
markExported(instance);
}
};
9 changes: 5 additions & 4 deletions libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { flatten } from '../common/core.helpers';
import { dependencyKeys, Type } from '../common/core.types';
import funcGetType from '../common/func.get-type';
import ngMocksUniverse from '../common/ng-mocks-universe';
import markExported from '../mock/mark-exported';

import createResolvers from './create-resolvers';
import markProviders from './mark-providers';
Expand Down Expand Up @@ -102,10 +103,7 @@ const resolveDefForExport = (
return undefined;
}

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

return mockDef;
};
Expand Down Expand Up @@ -160,6 +158,9 @@ export default <
if (!ngModuleDef.skipExports) {
addExports(resolve, change, ngModuleDef, mockModuleDef, ngModule);
}
for (const def of ngModule && mockModuleDef.exports ? (flatten(mockModuleDef.exports) as Array<any>) : []) {
markExported(def, ngModule);
}

const resolutions = ngMocksUniverse.config.get('mockNgDefResolver').pop();
if (!hasResolver) {
Expand Down
17 changes: 17 additions & 0 deletions libs/ng-mocks/src/lib/mock/mark-exported.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getSourceOfMock } from '../common/func.get-source-of-mock';
import ngMocksUniverse from '../common/ng-mocks-universe';

export default (instanceDef: any, ngModuleDef?: any) => {
const instance = getSourceOfMock(instanceDef);
const configInstance = ngMocksUniverse.configInstance.get(instance) ?? { __set: true };
if (!configInstance.exported) {
configInstance.exported = new Set();
}
if (ngModuleDef) {
configInstance.exported.add(getSourceOfMock(ngModuleDef));
}
if (configInstance.__set) {
configInstance.__set = undefined;
ngMocksUniverse.configInstance.set(instance, configInstance);
}
};
154 changes: 154 additions & 0 deletions tests/issue-6928/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { CommonModule } from '@angular/common';
import { Component, NgModule, VERSION } from '@angular/core';
import { TestBed } from '@angular/core/testing';

import {
MockBuilder,
MockComponent,
MockModule,
ngMocks,
} from 'ng-mocks';

// @see /~https://github.com/help-me-mom/ng-mocks/issues/6928
describe('issue-6928', () => {
if (Number.parseInt(VERSION.major, 10) < 14) {
it('needs >=a14', () => {
expect(true).toBeTruthy();
});

return;
}

ngMocks.throwOnConsole();

@Component({
selector: 'app-shared1',
template: '',
})
class Shared1Component {}

@Component({
selector: 'app-shared2',
template: '',
})
class Shared2Component {}

@NgModule({
imports: [CommonModule],
declarations: [Shared1Component, Shared2Component],
exports: [Shared1Component, Shared2Component],
})
class SharedModule {}

@Component(
{
selector: 'app-standalone',
template: '<app-shared1></app-shared1>',
standalone: true,
imports: [CommonModule, SharedModule],
} as never /* TODO: remove after upgrade to a14 */,
)
class StandaloneComponent {}

@Component({
selector: 'app-my-component',
template:
'<app-shared2></app-shared2><app-standalone></app-standalone>',
})
class MyComponent {}

@NgModule({
imports: [
CommonModule,
StandaloneComponent,
SharedModule,
] as never /* TODO: remove after upgrade to a14 */,
declarations: [MyComponent],
})
class AppModule {}

describe('missing module import', () => {
it('throws on 2 declarations w/o ng-mocks', () =>
expect(() => {
TestBed.configureTestingModule(
{
imports: [StandaloneComponent],
declarations: [MyComponent, Shared2Component],
} as never /* TODO: remove after upgrade to a14 */,
).compileComponents();
TestBed.createComponent(MyComponent).detectChanges();
}).toThrowError(
/is part of the declarations of 2 modules: DynamicTestModule/,
));

it('handles TestBed correctly w/ ng-mocks', () => {
expect(() => {
TestBed.configureTestingModule(
{
imports: [MockComponent(StandaloneComponent)],
declarations: [
MyComponent,
MockComponent(Shared2Component),
],
} as never /* TODO: remove after upgrade to a14 */,
).compileComponents();
TestBed.createComponent(MyComponent).detectChanges();
}).not.toThrow();
});

it('handles TestBed correctly w/ MockBuilder', () => {
expect(() => {
MockBuilder(MyComponent, [
StandaloneComponent,
Shared2Component,
]).then();
TestBed.createComponent(MyComponent).detectChanges();
}).not.toThrow();
});
});

describe('correct module import', () => {
it('passes w/o ng-mocks', () =>
expect(() => {
TestBed.configureTestingModule(
{
imports: [StandaloneComponent, SharedModule],
declarations: [MyComponent],
} as never /* TODO: remove after upgrade to a14 */,
).compileComponents();
TestBed.createComponent(MyComponent).detectChanges();
}).not.toThrow());

it('passes w/ ng-mocks', () =>
expect(() => {
TestBed.configureTestingModule(
{
imports: [
MockComponent(StandaloneComponent),
MockModule(SharedModule),
],
declarations: [MyComponent],
} as never /* TODO: remove after upgrade to a14 */,
).compileComponents();
TestBed.createComponent(MyComponent).detectChanges();
}).not.toThrow());

it('passes w/ MockBuilder', () =>
expect(() => {
MockBuilder(MyComponent, [
StandaloneComponent,
SharedModule,
]).then();
TestBed.createComponent(MyComponent).detectChanges();
}).not.toThrow());

it('passes w/ MockBuilder and AppModule', () =>
expect(() => {
MockBuilder(MyComponent, [
AppModule,
Shared2Component,
]).then();
TestBed.createComponent(MyComponent).detectChanges();
}).not.toThrow());
});
});

0 comments on commit 2d0012c

Please sign in to comment.