Skip to content

Commit

Permalink
fix: respect mocks in tokens with useValue
Browse files Browse the repository at this point in the history
closes #151
  • Loading branch information
satanTime committed Jul 11, 2020
1 parent 55a0031 commit ccccfc6
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 7 deletions.
1 change: 0 additions & 1 deletion lib/mock-builder/mock-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
}

public build(): NgModule {
// tslint:disable-line:cyclomatic-complexity
const backup = {
builder: ngMocksUniverse.builder,
cache: ngMocksUniverse.cache,
Expand Down
2 changes: 1 addition & 1 deletion lib/mock-module/mock-module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { APP_INITIALIZER, ApplicationModule, Component, InjectionToken, NgModule
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserModule, By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ngMocksUniverse } from 'ng-mocks/dist/lib/common/ng-mocks-universe';

import { ngMocksUniverse } from '../common/ng-mocks-universe';
import { ngModuleResolver } from '../common/reflect';
import { MockComponent } from '../mock-component';
import { MockModule, MockProvider } from '../mock-module';
Expand Down
15 changes: 12 additions & 3 deletions lib/mock-module/mock-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ngModuleResolver } from '../common/reflect';
import { MockComponent } from '../mock-component';
import { MockDirective } from '../mock-directive';
import { MockPipe } from '../mock-pipe';
import { MockService } from '../mock-service';
import { MockService, mockServiceHelper } from '../mock-service';

export type MockedModule<T> = T & Mock & {};

Expand Down Expand Up @@ -62,7 +62,6 @@ export function MockProvider(provider: any): Provider | undefined {
export function MockModule<T>(module: Type<T>): Type<T>;
export function MockModule<T>(module: NgModuleWithProviders<T>): NgModuleWithProviders<T>;
export function MockModule(module: any): any {
// tslint:disable-line:cyclomatic-complexity
let ngModule: Type<any>;
let ngModuleProviders: Provider[] | undefined;
let mockModule: typeof ngModule | undefined;
Expand Down Expand Up @@ -161,7 +160,6 @@ export function MockModule(module: any): any {

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

// tslint:disable-next-line:cyclomatic-complexity
function MockNgModuleDef(ngModuleDef: NgModule, ngModule?: Type<any>): [boolean, NgModule] {
let changed = !ngMocksUniverse.flags.has('skipMock');
const mockedModuleDef: NgModule = {};
Expand Down Expand Up @@ -206,6 +204,17 @@ function MockNgModuleDef(ngModuleDef: NgModule, ngModule?: Type<any>): [boolean,
if (!mockedDef) {
mockedDef = MockProvider(def);
}
// if provider is a value, we need to go through the value and to replace all mocked instances.
if (provider !== def && mockedDef && mockedDef.useValue) {
const useValue = mockServiceHelper.replaceWithMocks(mockedDef.useValue);
mockedDef =
useValue === mockedDef.useValue
? mockedDef
: {
...mockedDef,
useValue,
};
}

if (!isNgInjectionToken(provider) || def !== mockedDef) {
resolutions.set(provider, mockedDef);
Expand Down
24 changes: 23 additions & 1 deletion lib/mock-service/mock-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import { InjectionToken } from '@angular/core';

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

import { MockService } from './mock-service';
import { MockService, mockServiceHelper } from './mock-service';

class DeepParentClass {
public deepParentMethodName = 'deepParentMethod';
Expand Down Expand Up @@ -276,4 +277,25 @@ describe('MockService', () => {
expect(test.nameRead).toBe('fake4');
expect(test.nameSet).toBe('fake5');
});

it('respects original class in replaceWithMocks', () => {
class A {}

class B {}

class Test {
private member = A;

public getMember() {
return this.member;
}
}

ngMocksUniverse.cache.set(A, B);

const instance = new Test();
const updated = mockServiceHelper.replaceWithMocks(instance);
expect(updated).toEqual(jasmine.any(Test));
expect(updated.getMember()).toBe(B);
});
});
33 changes: 32 additions & 1 deletion lib/mock-service/mock-service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ngMocksUniverse } from '../common/ng-mocks-universe';

export type MockedFunction = () => any;

const isFunc = (value: any): boolean => {
Expand Down Expand Up @@ -138,7 +140,6 @@ const mockServiceHelperPrototype = {
}
},

// tslint:disable-next-line:cyclomatic-complexity
mock: <T = MockedFunction>(instance: any, name: string, accessType?: 'get' | 'set'): T => {
const def = Object.getOwnPropertyDescriptor(instance, name);
if (def && def[accessType || 'value']) {
Expand Down Expand Up @@ -188,6 +189,35 @@ const mockServiceHelperPrototype = {
Object.defineProperty(instance, name, mockDef);
return mock;
},

replaceWithMocks(value: any): any {
if (ngMocksUniverse.cache.has(value)) {
return ngMocksUniverse.cache.get(value);
}
if (typeof value !== 'object') {
return value;
}
let mocked: any;
let updated = false;
if (Array.isArray(value)) {
mocked = [];
for (let key = 0; key < value.length; key += 1) {
mocked[key] = mockServiceHelper.replaceWithMocks(value[key]);
updated = updated || mocked[key] !== value[key];
}
} else if (value) {
mocked = {};
for (const key of Object.keys(value)) {
mocked[key] = mockServiceHelper.replaceWithMocks(value[key]);
updated = updated || mocked[key] !== value[key];
}
}
if (updated) {
Object.setPrototypeOf(mocked, Object.getPrototypeOf(value));
return mocked;
}
return value;
},
};

// We need a single pointer to the object among all environments.
Expand All @@ -209,6 +239,7 @@ export const mockServiceHelper: {
mock<T = MockedFunction>(instance: any, name: string, style?: 'get' | 'set'): T;
mockFunction(mockName: string): MockedFunction;
registerMockFunction(mockFunction: (mockName: string) => MockedFunction | undefined): void;
replaceWithMocks(value: any): any;
} = ((window as any) || (global as any)).ngMocksMockServiceHelper;

export function MockService(service?: boolean | number | string | null, mockNamePrefix?: string): undefined;
Expand Down
23 changes: 23 additions & 0 deletions tests/issue-151/app/app-routing.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { HelloComponent } from './hello.component';
import { HelloModule } from './hello.module';

const routes: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: '/hello',
},
{
component: HelloComponent,
path: 'hello',
},
];

@NgModule({
exports: [HelloModule, RouterModule],
imports: [HelloModule, RouterModule.forRoot(routes)],
})
export class AppRoutingModule {}
7 changes: 7 additions & 0 deletions tests/issue-151/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Component } from '@angular/core';

@Component({
selector: 'my-app',
template: '<router-outlet></router-outlet>',
})
export class AppComponent {}
12 changes: 12 additions & 0 deletions tests/issue-151/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
bootstrap: [AppComponent],
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
})
export class AppModule {}
7 changes: 7 additions & 0 deletions tests/issue-151/app/hello.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Component } from '@angular/core';

@Component({
selector: 'app-hello',
template: 'hello',
})
export class HelloComponent {}
11 changes: 11 additions & 0 deletions tests/issue-151/app/hello.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';

import { HelloComponent } from './hello.component';

@NgModule({
declarations: [HelloComponent],
exports: [HelloComponent],
imports: [CommonModule],
})
export class HelloModule {}
33 changes: 33 additions & 0 deletions tests/issue-151/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router, RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MockBuilder, MockRender } from 'ng-mocks';

import { AppComponent } from './app/app.component';
import { AppModule } from './app/app.module';

describe('issue-151', () => {
let fixture: ComponentFixture<AppComponent>;

describe('mock AppRoutingModule', () => {
beforeEach(() =>
TestBed.configureTestingModule(
MockBuilder(AppComponent, AppModule).keep(RouterModule).keep(RouterTestingModule).build()
)
);

beforeEach(async () => {
fixture = MockRender(AppComponent);
const router = TestBed.get(Router);
if (fixture.ngZone) {
fixture.ngZone.run(() => router.initialNavigation());
}
await fixture.whenStable();
});

it('should create the app', () => {
// asserting that app-hello is mocked (no content) and detected by router.
expect(fixture.nativeElement.innerHTML).toContain('<app-hello></app-hello>');
});
});
});
1 change: 1 addition & 0 deletions tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"arrow-parens": false,
"comment-format": false,
"completed-docs": false,
"cyclomatic-complexity": false,
"deprecation": false,
"file-name-casing": false,
"max-classes-per-file": false,
Expand Down

0 comments on commit ccccfc6

Please sign in to comment.