Skip to content

Commit

Permalink
fix(core): mocking viewProviders #1507
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Mar 6, 2022
1 parent af0cdcc commit 421c473
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 6 deletions.
5 changes: 4 additions & 1 deletion docs/articles/api/MockRender.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ fixture.point.componentInstance;

## Example with providers

If we want, we can specify providers for the render passing them via the 3rd parameter.
If we want, we can specify `providers` or `viewProviders` for the render passing them via the 3rd parameter.
It is useful, when we want to **provide mock system tokens / services** such as `APP_INITIALIZER`, `DOCUMENT` etc.

```ts
Expand All @@ -488,6 +488,9 @@ const fixture = MockRender(
useValue: MockService(Document),
},
],
providers: [MockProvider(OtherService, {
serviceFlag: true,
})],
},
);
```
Expand Down
17 changes: 13 additions & 4 deletions libs/ng-mocks/src/lib/mock-module/mock-ng-def.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NgModule, Provider } from '@angular/core';
import { Component, Directive, NgModule, Pipe, Provider } from '@angular/core';

import { flatten } from '../common/core.helpers';
import { Type } from '../common/core.types';
Expand All @@ -13,7 +13,14 @@ const flatToExisting = <T, R>(data: T | T[], callback: (arg: T) => R | undefined
.map(callback)
.filter((item): item is R => !!item);

type processMeta = 'declarations' | 'entryComponents' | 'bootstrap' | 'providers' | 'imports' | 'exports';
type processMeta =
| 'declarations'
| 'entryComponents'
| 'bootstrap'
| 'providers'
| 'viewProviders'
| 'imports'
| 'exports';

const configureProcessMetaKeys = (
resolve: (def: any) => any,
Expand All @@ -23,16 +30,17 @@ const configureProcessMetaKeys = (
['entryComponents', resolve],
['bootstrap', resolve],
['providers', resolveProvider],
['viewProviders', resolveProvider],
['imports', resolve],
['exports', resolve],
];

const processMeta = (
ngModule: NgModule,
ngModule: Partial<NgModule & Component & Directive & Pipe>,
resolve: (def: any) => any,
resolveProvider: (def: Provider) => any,
): NgModule => {
const mockModuleDef: NgModule = {};
const mockModuleDef: Partial<NgModule & Component & Directive & Pipe> = {};
const keys = configureProcessMetaKeys(resolve, resolveProvider);

const cachePipe = ngMocksUniverse.flags.has('cachePipe');
Expand All @@ -45,6 +53,7 @@ const processMeta = (
}
}
markProviders(mockModuleDef.providers);
markProviders(mockModuleDef.viewProviders);

if (!cachePipe) {
ngMocksUniverse.flags.delete('cachePipe');
Expand Down
8 changes: 7 additions & 1 deletion libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ export default (
const caches = getCache();

// nulls help to detect defaults
const cacheKey = [template, ...(bindings ?? [null]), ...(flags.providers ?? [null])];
const cacheKey = [
template,
...(bindings ?? [null]),
...(flags.providers ?? [null]),
...(flags.viewProviders ?? [null]),
];
let ctor = checkCache(caches, cacheKey);
if (ctor) {
return ctor;
Expand All @@ -94,6 +99,7 @@ export default (
providers: flags.providers,
selector: 'mock-render',
template: mockTemplate,
viewProviders: flags.viewProviders,
};

ctor = generateWrapper({ ...meta, bindings, options });
Expand Down
1 change: 1 addition & 0 deletions libs/ng-mocks/src/lib/mock-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface IMockRenderOptions {
detectChanges?: boolean;
providers?: Provider[];
reset?: boolean;
viewProviders?: Provider[];
}

export interface IMockRenderFactoryOptions extends IMockRenderOptions {
Expand Down
151 changes: 151 additions & 0 deletions tests/issue-1507/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Component, Injectable, Input } from '@angular/core';
import {
MockBuilder,
MockInstance,
MockProvider,
MockRender,
ngMocks,
} from 'ng-mocks';

@Injectable()
export class ProviderService {
public description = 'real provider';
}

@Injectable()
export class ViewProviderService {
public description = 'real viewProvider';
}

@Component({
providers: [ProviderService],
selector: 'hello',
template: `
<h1 class="name">{{ name }}</h1>
<div class="provider">{{ provider.description }}</div>
<div class="viewProvider">{{ viewProvider.description }}</div>
`,
viewProviders: [ViewProviderService],
})
export class HelloComponent {
@Input() public readonly name: string | null = null;

public constructor(
public readonly provider: ProviderService,
public readonly viewProvider: ViewProviderService,
) {}
}

// @see https://github.com/ike18t/ng-mocks/issues/1507
// Difference between MockBuilder.provide and MockBuilder.mock
// Only MockBuilder.mock replaces providers on the component / directive level.
// MockBuilder.provide simply adds a service to global scope.
describe('issue-1507', () => {
MockInstance.scope();

describe('default', () => {
beforeEach(() => MockBuilder(HelloComponent));

it('keeps providers and viewProviders as they are', () => {
MockRender(HelloComponent, { name: 'Test1' });

const name = ngMocks.formatText(ngMocks.find('.name'));
expect(name).toEqual('Test1');

const provider = ngMocks.formatText(ngMocks.find('.provider'));
expect(provider).toEqual('real provider');

const viewProvider = ngMocks.formatText(
ngMocks.find('.viewProvider'),
);
expect(viewProvider).toEqual('real viewProvider');
});
});

describe(`.keep`, () => {
beforeEach(() =>
MockBuilder(HelloComponent)
.keep(ProviderService)
.keep(ViewProviderService),
);

it('keeps providers and viewProviders as they are', () => {
MockRender(HelloComponent, { name: 'Test2' });

const name = ngMocks.formatText(ngMocks.find('.name'));
expect(name).toEqual('Test2');

const provider = ngMocks.formatText(ngMocks.find('.provider'));
expect(provider).toEqual('real provider');

const viewProvider = ngMocks.formatText(
ngMocks.find('.viewProvider'),
);
expect(viewProvider).toEqual('real viewProvider');
});
});

describe(`.provide`, () => {
beforeEach(() =>
MockBuilder(HelloComponent)
.provide(
MockProvider(ProviderService, {
description: 'provided provider',
}),
)
.provide(
MockProvider(ViewProviderService, {
description: 'provided viewProvider',
}),
),
);

it('provides mocks on the root level', () => {
MockRender(HelloComponent, { name: 'Test3' });

const name = ngMocks.formatText(ngMocks.find('.name'));
expect(name).toEqual('Test3');

const provider = ngMocks.formatText(ngMocks.find('.provider'));
expect(provider).toEqual('real provider');
expect(
ngMocks.findInstance(ProviderService).description,
).toEqual('provided provider');

const viewProvider = ngMocks.formatText(
ngMocks.find('.viewProvider'),
);
expect(viewProvider).toEqual('real viewProvider');
expect(
ngMocks.findInstance(ViewProviderService).description,
).toEqual('provided viewProvider');
});
});

describe(`.mock`, () => {
beforeEach(() =>
MockBuilder(HelloComponent)
.mock(ProviderService, {
description: 'mock provider',
})
.mock(ViewProviderService, {
description: 'mock viewProvider',
}),
);

it('mocks services on the component level', () => {
MockRender(HelloComponent, { name: 'Test4' });

const name = ngMocks.formatText(ngMocks.find('.name'));
expect(name).toEqual('Test4');

const provider = ngMocks.formatText(ngMocks.find('.provider'));
expect(provider).toEqual('mock provider');

const viewProvider = ngMocks.formatText(
ngMocks.find('.viewProvider'),
);
expect(viewProvider).toEqual('mock viewProvider');
});
});
});
37 changes: 37 additions & 0 deletions tests/mock-render-view-providers/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Component, Injectable } from '@angular/core';
import { MockBuilder, MockProvider, MockRender } from 'ng-mocks';

@Injectable()
class TargetService {
public name = 'real';
}

@Component({
selector: 'target',
template: `{{ service.name }}`,
})
class TargetComponent {
public constructor(public readonly service: TargetService) {}
}

describe('MockRender.viewProviders', () => {
beforeEach(() => MockBuilder(TargetComponent));

it('throws without the service', () => {
expect(() => MockRender(TargetComponent)).toThrowError(
/No provider for TargetService/,
);
});

it('providers services via viewProviders', () => {
expect(() =>
MockRender(TargetComponent, null, {
viewProviders: [
MockProvider(TargetService, {
name: 'mock',
}),
],
}),
).not.toThrow();
});
});

0 comments on commit 421c473

Please sign in to comment.