Skip to content

Commit

Permalink
feat: mocked providers for kept declarations
Browse files Browse the repository at this point in the history
closes #172
  • Loading branch information
satanTime committed Sep 27, 2020
1 parent e05d770 commit 062d147
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 36 deletions.
30 changes: 30 additions & 0 deletions lib/common/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,36 @@ export const mapEntries = <K, T>(set: Map<K, T>): Array<[K, T]> => {
return result;
};

export const extendClass = <I extends object>(base: Type<I>): Type<I> => {
let child: any;
const parent: any = base;

// first we try to eval es2015 style and if it fails to use es5 transpilation in the catch block.
(window as any).ngMocksParent = parent;
try {
// tslint:disable-next-line:no-eval
eval(`
class child extends window.ngMocksParent {
}
window.ngMocksResult = child
`);
child = (window as any).ngMocksResult;
} catch (e) {
class ClassEs5 extends parent {}
child = ClassEs5;
}
(window as any).ngMocksParent = undefined;

// the next step is to respect constructor parameters as the parent class.
if (child) {
child.parameters = jitReflector
.parameters(parent)
.map(parameter => ngMocksUniverse.cacheMocks.get(parameter) || parameter);
}

return child;
};

export const isNgType = (object: Type<any>, type: string): boolean =>
jitReflector.annotations(object).some(annotation => annotation.ngMetadataName === type);

Expand Down
48 changes: 45 additions & 3 deletions lib/mock-builder/mock-builder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InjectionToken, NgModule, PipeTransform, Provider } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MetadataOverride, TestBed } from '@angular/core/testing';
import { directiveResolver, ngModuleResolver } from 'ng-mocks/dist/lib/common/reflect';

import {
flatten,
Expand All @@ -15,7 +16,7 @@ import {
import { ngMocksUniverse } from '../common/ng-mocks-universe';
import { MockComponent } from '../mock-component';
import { MockDirective } from '../mock-directive';
import { MockModule, MockProvider } from '../mock-module';
import { MockModule, MockNgDef, MockProvider } from '../mock-module';
import { MockPipe } from '../mock-pipe';
import { mockServiceHelper } from '../mock-service';

Expand Down Expand Up @@ -190,6 +191,45 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
ngMocksUniverse.touches.delete(def);
}

// Redefining providers for kept declarations.
for (const value of mapValues(ngMocksUniverse.builder)) {
let meta: NgModule | undefined;
if (isNgDef(value, 'm')) {
meta = ngModuleResolver.resolve(value);
} else if (isNgDef(value, 'c')) {
meta = directiveResolver.resolve(value);
} else if (isNgDef(value, 'd')) {
meta = directiveResolver.resolve(value);
} else {
continue;
}

const skipMock = ngMocksUniverse.flags.has('skipMock');
if (!skipMock) {
ngMocksUniverse.flags.add('skipMock');
}
const [changed, def] = MockNgDef({ providers: meta.providers });
if (!skipMock) {
ngMocksUniverse.flags.delete('skipMock');
}
if (!changed) {
continue;
}
const override: MetadataOverride<{ providers: Provider[] | undefined }> = {
set: {
providers: def.providers,
},
};

if (isNgDef(value, 'm')) {
TestBed.overrideModule(value, override);
} else if (isNgDef(value, 'c')) {
TestBed.overrideComponent(value, override);
} else if (isNgDef(value, 'd')) {
TestBed.overrideDirective(value, override);
}
}

// Setting up TestBed.
const imports: Array<Type<any> | NgModuleWithProviders> = [];

Expand Down Expand Up @@ -422,7 +462,9 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
this.mockDef.pipe.delete(source);
this.replaceDef.pipe.set(source, destination);
} else {
throw new Error('cannot replace the source by destination destination, wrong types');
throw new Error(
'Cannot replace the declaration, both have to be a Module, a Component, a Directive or a Pipe, for Providers use `.mock` or `.provide`'
);
}
if (config) {
this.configDef.set(source, config);
Expand Down
18 changes: 15 additions & 3 deletions lib/mock-component/mock-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ export function MockComponent<TComponent>(
providers: [
{
provide: component,
useExisting: forwardRef(() => ComponentMock),
useExisting: (() => {
const value: Type<any> & { __ngMocksSkip?: boolean } = forwardRef(() => ComponentMock);
value.__ngMocksSkip = true;
return value;
})(),
},
],
selector,
Expand All @@ -124,7 +128,11 @@ export function MockComponent<TComponent>(
options.providers.push({
multi: true,
provide,
useExisting: forwardRef(() => ComponentMock),
useExisting: (() => {
const value: Type<any> & { __ngMocksSkip?: boolean } = forwardRef(() => ComponentMock);
value.__ngMocksSkip = true;
return value;
})(),
});
continue;
}
Expand All @@ -133,7 +141,11 @@ export function MockComponent<TComponent>(
options.providers.push({
multi: true,
provide,
useExisting: forwardRef(() => ComponentMock),
useExisting: (() => {
const value: Type<any> & { __ngMocksSkip?: boolean } = forwardRef(() => ComponentMock);
value.__ngMocksSkip = true;
return value;
})(),
});
continue;
}
Expand Down
18 changes: 15 additions & 3 deletions lib/mock-directive/mock-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ export function MockDirective<TDirective>(directive: Type<TDirective>): Type<Moc
providers: [
{
provide: directive,
useExisting: forwardRef(() => DirectiveMock),
useExisting: (() => {
const value: Type<any> & { __ngMocksSkip?: boolean } = forwardRef(() => DirectiveMock);
value.__ngMocksSkip = true;
return value;
})(),
},
],
selector,
Expand All @@ -89,7 +93,11 @@ export function MockDirective<TDirective>(directive: Type<TDirective>): Type<Moc
options.providers.push({
multi: true,
provide,
useExisting: forwardRef(() => DirectiveMock),
useExisting: (() => {
const value: Type<any> & { __ngMocksSkip?: boolean } = forwardRef(() => DirectiveMock);
value.__ngMocksSkip = true;
return value;
})(),
});
continue;
}
Expand All @@ -98,7 +106,11 @@ export function MockDirective<TDirective>(directive: Type<TDirective>): Type<Moc
options.providers.push({
multi: true,
provide,
useExisting: forwardRef(() => DirectiveMock),
useExisting: (() => {
const value: Type<any> & { __ngMocksSkip?: boolean } = forwardRef(() => DirectiveMock);
value.__ngMocksSkip = true;
return value;
})(),
});
continue;
}
Expand Down
38 changes: 11 additions & 27 deletions lib/mock-module/mock-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ApplicationModule, NgModule, Provider } from '@angular/core';
import { getTestBed } from '@angular/core/testing';

import {
extendClass,
flatten,
getMockedNgDefOf,
isNgDef,
Expand All @@ -14,7 +15,7 @@ import {
Type,
} from '../common';
import { ngMocksUniverse } from '../common/ng-mocks-universe';
import { jitReflector, ngModuleResolver } from '../common/reflect';
import { ngModuleResolver } from '../common/reflect';
import { MockComponent } from '../mock-component';
import { MockDirective } from '../mock-directive';
import { MockPipe } from '../mock-pipe';
Expand Down Expand Up @@ -118,37 +119,15 @@ export function MockModule(module: any): any {
}
}

const [changed, ngModuleDef] = MockNgModuleDef(meta, ngModule);
const [changed, ngModuleDef] = MockNgDef(meta, ngModule);
if (changed) {
mockModuleDef = ngModuleDef;
}
}

if (mockModuleDef) {
const parent = ngMocksUniverse.flags.has('skipMock') ? ngModule : Mock;

// first we try to eval es2015 style and if it fails to use es5 transpilation in the catch block.
(window as any).ngMocksParent = parent;
try {
// tslint:disable-next-line:no-eval
eval(`
class mockModule extends window.ngMocksParent {
}
window.ngMocksResult = mockModule
`);
mockModule = (window as any).ngMocksResult;
} catch (e) {
class ClassEs5 extends parent {}
mockModule = ClassEs5;
}
(window as any).ngMocksParent = undefined;

// the next step is to respect constructor parameters as the parent class.
if (mockModule) {
(mockModule as any).parameters = jitReflector
.parameters(parent)
.map(parameter => ngMocksUniverse.cacheMocks.get(parameter) || parameter);
}
mockModule = extendClass(parent);

// the last thing is to apply decorators.
NgModule(mockModuleDef)(mockModule as any);
Expand All @@ -163,7 +142,7 @@ export function MockModule(module: any): any {
}

if (ngModuleProviders) {
const [changed, ngModuleDef] = MockNgModuleDef({ providers: ngModuleProviders });
const [changed, ngModuleDef] = MockNgDef({ providers: ngModuleProviders });
mockModuleProviders = changed ? ngModuleDef.providers : ngModuleProviders;
}

Expand All @@ -180,7 +159,12 @@ export function MockModule(module: any): any {

const NEVER_MOCK: Array<Type<any>> = [CommonModule, ApplicationModule];

function MockNgModuleDef(ngModuleDef: NgModule, ngModule?: Type<any>): [boolean, NgModule] {
/**
* Can be changed at any time.
*
* @internal
*/
export function MockNgDef(ngModuleDef: NgModule, ngModule?: Type<any>): [boolean, NgModule] {
let changed = !ngMocksUniverse.flags.has('skipMock');
const mockedModuleDef: NgModule = {};
const {
Expand Down
6 changes: 6 additions & 0 deletions lib/mock-service/mock-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,12 @@ const mockServiceHelperPrototype = {
resolveProvider: (def: any, resolutions: Map<any, any>, changed?: (flag: boolean) => void) => {
const provider = typeof def === 'object' && def.provide ? def.provide : def;
const multi = def !== provider && !!def.multi;

// we shouldn't touch our system providers at all.
if (typeof def === 'object' && def.useExisting && def.useExisting.__ngMocksSkip) {
return def;
}

let mockedDef: typeof def;
if (resolutions.has(provider)) {
mockedDef = resolutions.get(provider);
Expand Down
115 changes: 115 additions & 0 deletions tests/issues-172/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Component, Injectable, NgModule, OnInit } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder, MockRender } from 'ng-mocks';

@Injectable()
class Target1Service {
protected readonly name = 'Target1Service';

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

@Injectable()
class Target2Service {
protected readonly name = 'Target2Service';

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

@Component({
providers: [Target1Service, Target2Service],
selector: 'app-target',
template: '{{echo}}',
})
class TargetComponent implements OnInit {
public echo = '';

protected readonly target1Service: Target1Service;
protected readonly target2Service: Target2Service;

constructor(target1Service: Target1Service, target2Service: Target2Service) {
this.target1Service = target1Service;
this.target2Service = target2Service;
}

public ngOnInit(): void {
this.echo = `${this.target1Service.echo()}${this.target2Service.echo()}`;
}
}

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

describe('issue-172:real', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [TargetModule],
}).compileComponents()
);

it('renders echo', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerHTML).toContain('<app-target>Target1ServiceTarget2Service</app-target>');
});
});

describe('issue-172:test', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [TargetModule],
}).compileComponents()
);

it('renders echo', () => {
TestBed.overrideComponent(TargetComponent, {
add: {
providers: [
{
provide: Target1Service,
useValue: {
echo: () => 'MockService',
},
},
],
},
remove: {
providers: [Target1Service],
},
});
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerHTML).toContain('<app-target>MockServiceTarget2Service</app-target>');
});
});

describe('issue-172:mock', () => {
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule).mock(Target1Service, {
echo: () => 'MockService',
})
);

it('renders mocked echo', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerHTML).toContain('<app-target>MockServiceTarget2Service</app-target>');
});
});

describe('issue-172:restore', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [TargetModule],
}).compileComponents()
);

it('renders echo', () => {
const fixture = MockRender(TargetComponent);
expect(fixture.nativeElement.innerHTML).toContain('<app-target>Target1ServiceTarget2Service</app-target>');
});
});

0 comments on commit 062d147

Please sign in to comment.