Skip to content

Commit

Permalink
fix: support of modules with providers in MockBuilder
Browse files Browse the repository at this point in the history
closes #197
  • Loading branch information
satanTime committed Oct 9, 2020
1 parent a16aece commit e0250e0
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 14 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,8 @@ const ngModule = MockBuilder().keep(MyComponent, { export: true }).mock(MyModule
// We should use .keep.
const ngModule = MockBuilder(MyComponent, MyModule)
.keep(SomeModule)
.keep(SomeModule.forSome())
.keep(SomeModule.forAnother())
.keep(SomeComponent)
.keep(SomeDirective)
.keep(SomePipe)
Expand All @@ -788,6 +790,8 @@ const ngModule = MockBuilder(MyComponent, MyModule)
// If we want to mock something, even a part of a kept module we should use .mock.
const ngModule = MockBuilder(MyComponent, MyModule)
.mock(SomeModule)
.mock(SomeModule.forSome())
.mock(SomeModule.forAnother())
.mock(SomeComponent)
.mock(SomeDirective)
.mock(SomePipe)
Expand Down
9 changes: 2 additions & 7 deletions lib/common/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,8 @@ export const extendClass = <I extends object>(base: Type<I>): Type<I> => {
return child;
};

export const isNgType = (object: Type<any>, type: string): boolean => {
try {
return jitReflector.annotations(object).some(annotation => annotation.ngMetadataName === type);
} catch (error) {
return false;
}
};
export const isNgType = (object: Type<any>, type: string): boolean =>
jitReflector.annotations(object).some(annotation => annotation.ngMetadataName === type);

/**
* Checks whether a class was decorated by a ng type.
Expand Down
4 changes: 2 additions & 2 deletions lib/common/ng-mocks-universe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { InjectionToken } from '@angular/core';

import { AnyType } from './lib';
import { AbstractType, Type } from './lib';

/**
* Can be changed any time.
Expand All @@ -14,5 +14,5 @@ export const ngMocksUniverse = {
config: new Map(),
flags: new Set<string>(['cacheModule', 'cacheComponent', 'cacheDirective', 'cacheProvider']),
resetOverrides: new Set(),
touches: new Set<AnyType<any> | InjectionToken<any>>(),
touches: new Set<Type<any> | AbstractType<any> | InjectionToken<any>>(),
};
49 changes: 44 additions & 5 deletions lib/mock-builder/mock-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const defaultMock = {}; // simulating Symbol
export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
protected beforeCC: Set<(testBed: typeof TestBed) => void> = new Set();
protected configDef: Map<Type<any> | InjectionToken<any>, any> = new Map();
protected defProviders: Map<Type<any> | InjectionToken<any>, Provider[]> = new Map();
protected defValue: Map<Type<any> | InjectionToken<any>, any> = new Map();
protected excludeDef: Set<Type<any> | InjectionToken<any>> = new Set();
protected keepDef: Set<Type<any> | InjectionToken<any>> = new Set();
Expand Down Expand Up @@ -126,7 +127,13 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
}

for (const def of mapValues(this.mockDef)) {
if (isNgDef(def, 'c')) {
if (isNgDef(def, 'm') && this.defProviders.has(def)) {
const loProviders = this.defProviders.get(def);
const [changed, loDef] = loProviders ? MockNgDef({ providers: loProviders }) : [false, {}];
if (changed && loDef.providers) {
this.defProviders.set(def, loDef.providers);
}
} else if (isNgDef(def, 'c')) {
ngMocksUniverse.builder.set(def, MockComponent(def));
} else if (isNgDef(def, 'd')) {
ngMocksUniverse.builder.set(def, MockDirective(def));
Expand Down Expand Up @@ -157,15 +164,26 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {

// Adding suitable leftovers.
for (const def of [...mapValues(this.mockDef), ...mapValues(this.keepDef), ...mapValues(this.replaceDef)]) {
if (!isNgDef(def) || ngMocksUniverse.touches.has(def)) {
if (isNgDef(def, 'm') && this.defProviders.has(def)) {
// nothing to do
} else if (!isNgDef(def) || ngMocksUniverse.touches.has(def)) {
continue;
}
const config = this.configDef.get(def);
if (config && config.dependency) {
continue;
}
if (isNgDef(def, 'm')) {
imports.push(ngMocksUniverse.builder.get(def));
const loModule = ngMocksUniverse.builder.get(def);
const loProviders = this.defProviders.has(def) ? this.defProviders.get(def) : undefined;
imports.push(
loProviders
? {
ngModule: loModule,
providers: loProviders,
}
: loModule
);
} else {
declarations.push(ngMocksUniverse.builder.get(def));
}
Expand Down Expand Up @@ -317,11 +335,19 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
}

public keep(input: any, config?: IMockBuilderConfig): this {
const def = isNgModuleDefWithProviders(input) ? input.ngModule : input;
const { def, providers } = isNgModuleDefWithProviders(input)
? { def: input.ngModule, providers: input.providers }
: { def: input, providers: undefined };

const existing = this.keepDef.has(def) ? this.defProviders.get(def) : [];
this.wipe(def);
this.keepDef.add(def);

// a magic to support modules with providers.
if (providers) {
this.defProviders.set(def, [...existing, ...providers]);
}

if (config) {
this.configDef.set(def, config);
} else {
Expand All @@ -338,19 +364,31 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
config?: IMockBuilderConfig
): this;
public mock<T>(token: InjectionToken<T>, mock?: any): this;
public mock<T>(def: NgModuleWithProviders<T>): this;
public mock<T>(def: AnyType<T>, mock: IMockBuilderConfig): this;
public mock<T>(provider: AnyType<T>, mock?: Partial<T>): this;
public mock<T>(def: AnyType<T>): this;
public mock(def: any, a1: any = defaultMock, a2?: any): this {
public mock(input: any, a1: any = defaultMock, a2?: any): this {
const { def, providers } = isNgModuleDefWithProviders(input)
? { def: input.ngModule, providers: input.providers }
: { def: input, providers: undefined };

let mock: any = a1;
let config: any = a1 === defaultMock ? undefined : a1;
if (isNgDef(def, 'p') && typeof a1 === 'function') {
mock = a1;
config = a2;
}

const existing = this.mockDef.has(def) ? this.defProviders.get(def) : [];
this.wipe(def);
this.mockDef.add(def);

// a magic to support modules with providers.
if (providers) {
this.defProviders.set(def, [...existing, ...providers]);
}

if (isNgDef(def, 'p') && typeof mock === 'function') {
this.defValue.set(def, mock);
}
Expand Down Expand Up @@ -414,6 +452,7 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
}

private wipe(def: Type<any>): void {
this.defProviders.delete(def);
this.defValue.delete(def);
this.excludeDef.delete(def);
this.keepDef.delete(def);
Expand Down
13 changes: 13 additions & 0 deletions tests/issue-197/abstract.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TestBed } from '@angular/core/testing';
import { DomSanitizer } from '@angular/platform-browser';
import { MockBuilder } from 'ng-mocks';

describe('issue-197:abstract', () => {
const expected = {};
beforeEach(() => MockBuilder().mock(DomSanitizer, expected));

it('mocks abstract classes', () => {
const actual = TestBed.get(DomSanitizer);
expect(actual).toBe(expected);
});
});
91 changes: 91 additions & 0 deletions tests/issue-197/with-providers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// tslint:disable:no-unnecessary-class

import { Component, Injectable, NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder, MockRender, NgModuleWithProviders } from 'ng-mocks';

@Injectable()
class DependencyService {
private readonly name: string = 'dependency';

public echo(): string {
return this.name;
}
}

@NgModule({})
class DependencyModule {
static withProviders(): NgModuleWithProviders<DependencyModule> {
return {
ngModule: DependencyModule,
providers: [
{
provide: DependencyService,
useValue: {
echo: () => 'via-provider',
},
},
],
};
}

public readonly service: DependencyService;

constructor(service: DependencyService) {
this.service = service;
}
}

@Component({
selector: 'target',
template: '{{ service.echo() }}',
})
class TargetComponent {
public readonly service: DependencyService;

constructor(service: DependencyService) {
this.service = service;
}
}

@NgModule({
declarations: [TargetComponent],
})
class TargetModule {}

describe('issue-197:with-providers:manually-injection', () => {
beforeEach(() => {
const module = MockBuilder(TargetComponent, TargetModule).build();

return TestBed.configureTestingModule({
declarations: module.declarations,
imports: [...module.imports, DependencyModule.withProviders()],
providers: module.providers,
}).compileComponents();
});

it('creates component with provided dependencies', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerHTML).toEqual('<target>via-provider</target>');
});
});

describe('issue-197:with-providers', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(DependencyModule.withProviders()));

it('creates component with provided dependencies', () => {
const fixture = MockRender(TargetComponent);

expect(fixture.nativeElement.innerHTML).toEqual('<target>via-provider</target>');
});
});

describe('issue-197:with-providers', () => {
beforeEach(() => MockBuilder(TargetComponent, TargetModule).mock(DependencyModule.withProviders()));

it('creates component with provided dependencies', () => {
const fixture = MockRender(TargetComponent);

expect(fixture.nativeElement.innerHTML).toEqual('<target></target>');
});
});

0 comments on commit e0250e0

Please sign in to comment.