Skip to content

Commit

Permalink
fix: mocking custom deps of providers
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Oct 31, 2020
1 parent ecfb15d commit 87da53b
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 56 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ TestBed.configureTestingModule({
AppSearchModule,
// ...
],
providers: [
LoginService,
DataService,
// ...
],
});
```

Expand All @@ -181,6 +186,11 @@ TestBed.configureTestingModule({
MockModule(AppSearchModule),
// ...
],
providers: [
MockProvider(LoginService),
MockProvider(DataService),
// ...
],
});
```

Expand Down
26 changes: 26 additions & 0 deletions lib/common/core.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@ export const mapEntries = <K, T>(set: Map<K, T>): Array<[K, T]> => {
return result;
};

// Accepts an array of dependencies from providers, skips injections flags,
// and adds the providers to the set.
export const extractDependency = (deps: any[], set?: Set<any>): void => {
if (!set) {
return;
}
for (const dep of deps) {
if (!Array.isArray(dep)) {
set.add(dep);
continue;
}
for (const flag of dep) {
if (flag && typeof flag === 'object' && flag.ngMetadataName === 'Optional') {
continue;
}
if (flag && typeof flag === 'object' && flag.ngMetadataName === 'SkipSelf') {
continue;
}
if (flag && typeof flag === 'object' && flag.ngMetadataName === 'Self') {
continue;
}
set.add(flag);
}
}
};

export const extendClass = <I extends object>(base: Type<I>): Type<I> => {
let child: any;
const parent: any = base;
Expand Down
17 changes: 17 additions & 0 deletions lib/mock-builder/mock-builder-promise.extract-dep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Extracts dependency among flags of parameters.
export default (decorators?: any[]): any => {
if (!decorators) {
return;
}

let provide: any;
for (const decorator of decorators) {
if (decorator && typeof decorator === 'object' && decorator.token) {
provide = decorator.token;
}
if (!provide && decorator && (typeof decorator !== 'object' || !decorator.ngMetadataName)) {
provide = decorator;
}
}
return provide;
};
29 changes: 29 additions & 0 deletions lib/mock-builder/mock-builder-promise.skip-dep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DOCUMENT } from '@angular/common';
import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser';

import { ngMocksUniverse } from '../common/ng-mocks-universe';

// Checks if we should avoid mocking of the provider.
export default (provide: any): boolean => {
if (!provide) {
return true;
}
if (ngMocksUniverse.touches.has(provide)) {
return true;
}
if (provide === DOCUMENT) {
return true;
}
if (provide === EVENT_MANAGER_PLUGINS) {
return true;
}

// Empty providedIn or things for a platform have to be skipped.
let skip = !provide.ɵprov?.providedIn || provide.ɵprov.providedIn === 'platform';
/* istanbul ignore next: A6 */
skip = skip && (!provide.ngInjectableDef?.providedIn || provide.ngInjectableDef.providedIn === 'platform');
if (typeof provide === 'function' && skip) {
return true;
}
return false;
};
74 changes: 25 additions & 49 deletions lib/mock-builder/mock-builder-promise.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { DOCUMENT } from '@angular/common';
import { InjectionToken, NgModule, PipeTransform, Provider } from '@angular/core';
import { MetadataOverride, TestBed } from '@angular/core/testing';
import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser';

import { flatten, mapEntries, mapValues } from '../common/core.helpers';
import { extractDependency, flatten, mapEntries, mapValues } from '../common/core.helpers';
import { directiveResolver, jitReflector, ngModuleResolver } from '../common/core.reflect';
import { NG_MOCKS, NG_MOCKS_OVERRIDES, NG_MOCKS_TOUCHES } from '../common/core.tokens';
import { AnyType, Type } from '../common/core.types';
Expand All @@ -18,6 +16,8 @@ import { MockPipe } from '../mock-pipe/mock-pipe';
import mockServiceHelper from '../mock-service/helper';
import MockProvider from '../mock-service/mock-provider';

import extractDep from './mock-builder-promise.extract-dep';
import skipDep from './mock-builder-promise.skip-dep';
import { IMockBuilderConfig, IMockBuilderResult } from './types';

const defaultMock = {}; // simulating Symbol
Expand Down Expand Up @@ -61,7 +61,8 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
'correctModuleExports',
]);
ngMocksUniverse.touches = new Set();
ngMocksUniverse.config.set('multi', new Set());
ngMocksUniverse.config.set('multi', new Set()); // collecting multi flags of providers.
ngMocksUniverse.config.set('deps', new Set()); // collecting all deps of providers.

for (const def of mapValues(this.keepDef)) {
ngMocksUniverse.builder.set(def, def);
Expand Down Expand Up @@ -202,43 +203,29 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
providers.push(provider);
}

// Adding missed providers
// Analyzing providers.
for (const provider of flatten(providers)) {
const provide = typeof provider === 'object' && (provider as any).provide ? (provider as any).provide : provider;
ngMocksUniverse.touches.add(provide);

if (provide !== provider && (provider as any).deps) {
extractDependency((provider as any).deps, ngMocksUniverse.config.get('deps'));
}
}

// Adding missed providers.
const parameters = new Set<any>();
if (ngMocksUniverse.touches.size) {
const touchedDefs = mapValues(ngMocksUniverse.touches);
if (ngMocksUniverse.touches.size || ngMocksUniverse.config.get('deps').size) {
const touchedDefs: any[] = mapValues(ngMocksUniverse.touches);
touchedDefs.push(...mapValues(ngMocksUniverse.config.get('deps')));
for (const def of touchedDefs) {
// Analyzing parameters.
for (const decorators of jitReflector.parameters(def)) {
if (!decorators) {
continue;
}
let provide: any;
for (const decorator of decorators) {
if (decorator && typeof decorator === 'object' && decorator.token) {
provide = decorator.token;
}
}
if (!provide && decorators[0]) {
provide = decorators[0];
}
if (!provide) {
continue;
}
if (provide === DOCUMENT) {
continue;
}
if (provide === EVENT_MANAGER_PLUGINS) {
continue;
}
if (ngMocksUniverse.touches.has(provide)) {
continue;
}
if (!skipDep(def)) {
parameters.add(def);
}

// Empty providedIn or things for a platform have to be skipped.
let skip = !provide.ɵprov?.providedIn || provide.ɵprov.providedIn === 'platform';
/* istanbul ignore next: A6 */
skip = skip && (!provide.ngInjectableDef?.providedIn || provide.ngInjectableDef.providedIn === 'platform');
if (typeof provide === 'function' && skip) {
for (const decorators of jitReflector.parameters(def)) {
const provide: any = extractDep(decorators);
if (skipDep(provide)) {
continue;
}
if (typeof provide === 'function' && touchedDefs.indexOf(provide) === -1) {
Expand All @@ -252,18 +239,7 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
// Adding missed providers.
if (parameters.size) {
const parametersMap = new Map();
const providersSkip = new Set();
// Excluding manually provided values.
for (const provider of flatten(providers)) {
const provide =
typeof provider === 'object' && (provider as any).provide ? (provider as any).provide : provider;
providersSkip.add(provide);
}
for (const parameter of mapValues(parameters)) {
if (providersSkip.has(parameter)) {
continue;
}

const mock = mockServiceHelper.resolveProvider(parameter, parametersMap);
if (mock) {
providers.push(mock);
Expand Down
2 changes: 1 addition & 1 deletion lib/mock-service/helper.mock-function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ const mockFunction: {
return func;
};

export default mockFunction;
export default (() => mockFunction)();
2 changes: 1 addition & 1 deletion lib/mock-service/helper.replace-with-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,4 @@ const replaceWithMocks = (value: any): any => {
return value;
};

export default replaceWithMocks;
export default (() => replaceWithMocks)();
5 changes: 5 additions & 0 deletions lib/mock-service/helper.resolve-provider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { extractDependency } from '../common/core.helpers';
import { NG_INTERCEPTORS } from '../common/core.tokens';
import { isNgInjectionToken } from '../common/func.is-ng-injection-token';
import { ngMocksUniverse } from '../common/ng-mocks-universe';
Expand Down Expand Up @@ -37,6 +38,10 @@ export default (def: any, resolutions: Map<any, any>, changed?: (flag: boolean)
return;
}

if (provider !== def && def.deps) {
extractDependency(def.deps, ngMocksUniverse.config.get('deps'));
}

if (
ngMocksUniverse.builder.has(NG_INTERCEPTORS) &&
ngMocksUniverse.builder.get(NG_INTERCEPTORS) === null &&
Expand Down
40 changes: 35 additions & 5 deletions tests/provider-with-custom-dependencies/test.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Injectable as InjectableSource, NgModule, Optional } from '@angular/core';
import { Component, Injectable as InjectableSource, NgModule, Optional, Self, SkipSelf, VERSION } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder, MockRender } from 'ng-mocks';

Expand Down Expand Up @@ -30,12 +30,14 @@ class Dep3Service {

@Injectable()
class TargetService {
public readonly flag: undefined;
public readonly optional?: { name: string };
public readonly service: { name: string };

constructor(service: Dep1Service, optional: Dep1Service) {
constructor(service: Dep1Service, optional: Dep1Service, flag?: undefined) {
this.service = service;
this.optional = optional;
this.flag = flag;
}
}

Expand All @@ -60,15 +62,25 @@ class TargetComponent {
exports: [TargetComponent],
providers: [
{
deps: [Dep2Service, [new Optional(), Dep3Service]],
provide: 'test',
useValue: undefined,
},
{
deps: [Dep2Service, [new Optional(), new SkipSelf(), new Self(), Dep3Service], 'test'],
provide: TargetService,
useClass: TargetService,
},
],
})
class TargetModule {}

xdescribe('provider-with-custom-dependencies', () => {
describe('provider-with-custom-dependencies', () => {
beforeEach(() => {
if (parseInt(VERSION.major, 10) <= 5) {
pending('Need Angular > 5');
}
});

describe('real', () => {
beforeEach(() =>
TestBed.configureTestingModule({
Expand All @@ -88,7 +100,7 @@ xdescribe('provider-with-custom-dependencies', () => {
});
});

describe('mock-builder', () => {
describe('mock-builder:mock', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(TargetService));

it('creates component with mocked custom dependencies', () => {
Expand All @@ -101,4 +113,22 @@ xdescribe('provider-with-custom-dependencies', () => {
expect(() => TestBed.get(Dep3Service)).toThrowError(/No provider for Dep3Service/);
});
});

describe('mock-builder:keep', () => {
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule).keep(TargetService).keep(Dep2Service, {
dependency: true,
})
);

it('creates component with kept Dep2Service', () => {
const fixture = MockRender(TargetComponent);
// Injects root dependency correctly, it is not missed, it is mocked.
expect(fixture.nativeElement.innerHTML).toContain('"service:dep-2"');
// Skips unprovided local dependency despite its mocked copy.
expect(fixture.nativeElement.innerHTML).toContain('"optional:missed"');
// The dependency should not be provided in TestBed.
expect(() => TestBed.get(Dep3Service)).toThrowError(/No provider for Dep3Service/);
});
});
});

0 comments on commit 87da53b

Please sign in to comment.