Skip to content

Commit

Permalink
fix(MockRender): renders pipes with $implicit param #2398
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed May 7, 2022
1 parent c21822b commit 03f5f5e
Show file tree
Hide file tree
Showing 14 changed files with 167 additions and 38 deletions.
21 changes: 21 additions & 0 deletions docs/articles/api/MockRender.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,27 @@ fixture.componentInstance;
fixture.point.componentInstance;
```

## Example with a pipe

```ts
const fixture = MockRender(DatePipe, {
$implicit: new Date(), // the value to transform
});

// is a middle component to manage params
fixture.componentInstance.$implicit.setHours(5);

// an instance of DatePipe
fixture.point.componentInstance;
```

```ts
const fixture = MockRender('{{ 3.99 | currency }}');

// an unknown instance
fixture.point.componentInstance;
```

## Example with a service

```ts
Expand Down
10 changes: 5 additions & 5 deletions docs/articles/guides/pipe.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ beforeEach(() => MockBuilder(TargetPipe));
To verify how the pipe behaves we need to render a custom template:

```ts
const fixture = MockRender(`{{ values | target}}`, {
values: ['1', '3', '2'],
const fixture = MockRender(TargetPipe, {
$implicit: ['1', '3', '2'],
});
```

Expand Down Expand Up @@ -61,15 +61,15 @@ describe('TestPipe', () => {
beforeEach(() => MockBuilder(TargetPipe));

it('sorts strings', () => {
const fixture = MockRender('{{ values | target}}', {
values: ['1', '3', '2'],
const fixture = MockRender(TargetPipe, {
$implicit: ['1', '3', '2'],
});

expect(fixture.nativeElement.innerHTML).toEqual('1, 2, 3');
});

it('reverses strings on param', () => {
const fixture = MockRender('{{ values | target:flag}}', {
const fixture = MockRender('{{ values | target:flag }}', {
flag: false,
values: ['1', '3', '2'],
});
Expand Down
6 changes: 3 additions & 3 deletions examples/TestPipe/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ describe('TestPipe', () => {
beforeEach(() => MockBuilder(TargetPipe));

it('sorts strings', () => {
const fixture = MockRender('{{ values | target}}', {
values: ['1', '3', '2'],
const fixture = MockRender(TargetPipe, {
$implicit: ['1', '3', '2'],
});

expect(fixture.nativeElement.innerHTML).toEqual('1, 2, 3');
});

it('reverses strings on param', () => {
const fixture = MockRender('{{ values | target:flag}}', {
const fixture = MockRender('{{ values | target:flag }}', {
flag: false,
values: ['1', '3', '2'],
});
Expand Down
5 changes: 2 additions & 3 deletions libs/ng-mocks/src/lib/common/core.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ import funcGetName from './func.get-name';
* @internal
*/
export const getTestBedInjection = <I>(token: AnyType<I> | InjectionToken<I>): I | undefined => {
const testBed: any = getTestBed();
try {
// istanbul ignore next
return testBed.inject ? testBed.inject(token) : testBed.get(token);
return getInjection(token);
} catch {
return undefined;
}
Expand All @@ -29,7 +28,7 @@ export const getTestBedInjection = <I>(token: AnyType<I> | InjectionToken<I>): I
* @deprecated
* @internal
*/
export const getInjection = <I>(token: Type<I> | InjectionToken<I>): I => {
export const getInjection = <I>(token: AnyType<I> | InjectionToken<I>): I => {
const testBed: any = getTestBed();

// istanbul ignore next
Expand Down
1 change: 0 additions & 1 deletion libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ export class MockBuilderPromise implements IMockBuilder {
for (const provider of flatten(def)) {
const { provide, multi } = parseProvider(provider);
const existing = this.providerDef.has(provide) ? this.providerDef.get(provide) : [];
this.wipe(provide);
this.providerDef.set(provide, generateProviderValue(provider, existing, multi));
}

Expand Down
1 change: 1 addition & 0 deletions libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default (

ctor = generateWrapper({ ...meta, bindings, options });
coreDefineProperty(ctor, 'cacheKey', cacheKey);
coreDefineProperty(ctor, 'tpl', mockTemplate);
caches.unshift(ctor as any);
caches.splice(ngMocksUniverse.global.get('mockRenderCacheSize') ?? coreConfig.mockRenderCacheSize);

Expand Down
5 changes: 5 additions & 0 deletions libs/ng-mocks/src/lib/mock-render/func.generate-template.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { isNgDef } from '../common/func.is-ng-def';
import coreReflectPipeResolve from '../common/core.reflect.pipe-resolve';

const generateTemplateAttrWrap = (prop: string, type: 'i' | 'o') => (type === 'i' ? `[${prop}]` : `(${prop})`);

const generateTemplateAttrWithParams = (prop: string, type: 'i' | 'o'): string => {
Expand Down Expand Up @@ -31,6 +34,8 @@ export default (declaration: any, { selector, bindings, inputs, outputs }: any):
// istanbul ignore else
if (typeof declaration === 'string') {
mockTemplate = declaration;
} else if (isNgDef(declaration, 'p') && bindings && bindings.indexOf('$implicit') !== -1) {
mockTemplate = `{{ $implicit | ${coreReflectPipeResolve(declaration).name} }}`;
} else if (selector) {
mockTemplate += `<${selector}`;
mockTemplate += generateTemplateAttr(bindings, inputs, 'i');
Expand Down
43 changes: 35 additions & 8 deletions libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import ngMocksUniverse from '../common/ng-mocks-universe';
import { ngMocks } from '../mock-helper/mock-helper';
import helperDefinePropertyDescriptor from '../mock-service/helper.define-property-descriptor';
import { MockService } from '../mock-service/mock-service';
import funcGetName from '../common/func.get-name';
import { getInjection } from '../common/core.helpers';

import funcCreateWrapper from './func.create-wrapper';
import funcInstallPropReader from './func.install-prop-reader';
Expand All @@ -23,21 +25,41 @@ export interface MockRenderFactory<C = any, F extends keyof any = keyof C> {
<T extends Record<F, any>>(params?: Partial<T>, detectChanges?: boolean): MockedComponentFixture<C, T>;
}

const isExpectedRender = (template: any): boolean =>
typeof template === 'string' || isNgDef(template, 'c') || isNgDef(template, 'd');

const renderDeclaration = (fixture: any, template: any, params: any): void => {
fixture.point = fixture.debugElement.children[0] || fixture.debugElement.childNodes[0];
fixture.point =
fixture.debugElement.children[0] &&
fixture.debugElement.children[0].nativeElement.nodeName !== '#text' &&
fixture.debugElement.children[0].nativeElement.nodeName !== '#comment'
? fixture.debugElement.children[0]
: fixture.debugElement;
if (isNgDef(template, 'd')) {
helperDefinePropertyDescriptor(fixture.point, 'componentInstance', {
get: () => ngMocks.get(fixture.point, template),
});
} else if (isNgDef(template, 'p')) {
helperDefinePropertyDescriptor(fixture.point, 'componentInstance', {
get: () => ngMocks.findInstance(fixture.point, template),
});
}
tryWhen(!params, () => funcInstallPropReader(fixture.componentInstance, fixture.point?.componentInstance, []));
tryWhen(!params, () => funcInstallPropReader(fixture.componentInstance, fixture.point.componentInstance, []));
};

const renderInjection = (fixture: any, template: any, params: any): void => {
const instance = TestBed.get(template);
let instance: any;
try {
instance = getInjection(template);
} catch (error) {
if (isNgDef(template, 'p')) {
throw new Error(
[
`Cannot render ${funcGetName(template)}.`,
'Did you forget to set $implicit param, or add the pipe to providers?',
'https://ng-mocks.sudo.eu/guides/pipe',
].join(' '),
);
}
throw error;
}
if (params) {
ngMocks.stub(instance, params);
}
Expand Down Expand Up @@ -111,7 +133,7 @@ const generateFactoryInstall = (ctor: AnyType<any>, options: IMockRenderFactoryO
};

const generateFactory = (
componentCtor: Type<any>,
componentCtor: Type<any> & { tpl?: string },
bindings: undefined | null | string[],
template: any,
options: IMockRenderFactoryOptions,
Expand All @@ -127,7 +149,12 @@ const generateFactory = (
fixture.detectChanges();
}

if (isExpectedRender(template)) {
if (
typeof template === 'string' ||
isNgDef(template, 'c') ||
isNgDef(template, 'd') ||
(componentCtor.tpl && isNgDef(template, 'p'))
) {
renderDeclaration(fixture, template, params);
} else {
renderInjection(fixture, template, params);
Expand Down
5 changes: 0 additions & 5 deletions libs/ng-mocks/src/lib/mock-render/mock-render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,6 @@ describe('MockRender', () => {
);
});

it('renders empty templates w/o point', () => {
const fixture = MockRender('');
expect(fixture.point).toBeUndefined();
});

it('assigns outputs to a literals', () => {
const fixture = MockRender(RenderRealComponent, {
trigger: undefined,
Expand Down
20 changes: 10 additions & 10 deletions libs/ng-mocks/src/lib/mock-render/mock-render.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { InjectionToken } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';

import { AnyType } from '../common/core.types';

import { MockRenderFactory } from './mock-render-factory';
import { IMockRenderOptions, MockedComponentFixture } from './types';

/**
* This signature of MockRender lets create an empty fixture.
*
* @see https://ng-mocks.sudo.eu/api/MockRender
*/
export function MockRender(): MockedComponentFixture<void, void>;

/**
* This signature of MockRender lets create a fixture to access a token.
*
Expand Down Expand Up @@ -57,13 +63,6 @@ export function MockRender<MComponent, TComponent extends object = Record<keyof
*/
export function MockRender<MComponent>(template: AnyType<MComponent>): MockedComponentFixture<MComponent, MComponent>;

/**
* This signature of MockRender with an empty template does not have the point.
*
* @see https://ng-mocks.sudo.eu/api/MockRender
*/
export function MockRender(template: ''): ComponentFixture<void> & { point: undefined };

/**
* This signature of MockRender without params should not autocomplete any keys of any types.
*
Expand Down Expand Up @@ -105,13 +104,14 @@ export function MockRender<MComponent, TComponent extends Record<keyof any, any>
): MockedComponentFixture<MComponent, TComponent>;

export function MockRender<MComponent, TComponent extends Record<keyof any, any>>(
template: string | AnyType<MComponent> | InjectionToken<MComponent>,
template?: string | AnyType<MComponent> | InjectionToken<MComponent>,
params?: TComponent,
flags: boolean | IMockRenderOptions = true,
): any {
const tpl = arguments.length === 0 ? '' : template;
const bindings = params && typeof params === 'object' ? Object.keys(params) : params;
const options = typeof flags === 'boolean' ? { detectChanges: flags } : { ...flags };
const factory = (MockRenderFactory as any)(template, bindings, options);
const factory = (MockRenderFactory as any)(tpl, bindings, options);

return factory(params, options.detectChanges);
}
82 changes: 82 additions & 0 deletions tests/issue-2398/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Pipe, PipeTransform } from '@angular/core';

import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';

@Pipe({
name: 'phone',
})
class PhonePipe implements PipeTransform {
transform(value: string | number): string {
const inputVal = value.toString();
const slice1 = inputVal.slice(0, 3);
const slice2 = inputVal.slice(3, 6);
const slice3 = inputVal.slice(6);
return `+1(${slice1})-${slice2}-${slice3}`;
}
}

// https://github.com/ike18t/ng-mocks/issues/2398
describe('issue-2398', () => {
describe('provided', () => {
beforeEach(() => MockBuilder(PhonePipe).provide(PhonePipe));

it('transforms the value as a generator', () => {
// the pipe is present in component
const fixture = MockRender(PhonePipe, {
$implicit: '4161234567',
});
expect(ngMocks.formatText(fixture)).toEqual('+1(416)-123-4567');

// point instance should be the pipe
expect(
fixture.point.componentInstance.transform('4161234568'),
).toEqual('+1(416)-123-4568');
});

it('transforms the value as a service', () => {
// the pipe is present in component
const fixture = MockRender(PhonePipe);
expect(
fixture.point.componentInstance.transform('4161234567'),
).toEqual('+1(416)-123-4567');
});

it('transforms the value as a template', () => {
// the pipe is present in component
const fixture = MockRender('{{ "4161234567" | phone }}');
expect(ngMocks.formatText(fixture)).toBe('+1(416)-123-4567');
});

it('provides the service', () => {
const fixture = MockRender();

// the pipe is injected as service
expect(() =>
fixture.point.injector.get(PhonePipe),
).not.toThrow();
});
});

describe('declared', () => {
beforeEach(() => MockBuilder(PhonePipe));

it('transforms the value as a generator', () => {
// the pipe is present in component
const fixture = MockRender(PhonePipe, {
$implicit: '4161234567',
});
expect(ngMocks.formatText(fixture)).toEqual('+1(416)-123-4567');

// point instance should be the pipe
expect(
fixture.point.componentInstance.transform('4161234568'),
).toEqual('+1(416)-123-4568');
});

it('fails on not provided pipes', () => {
expect(() => MockRender(PhonePipe)).toThrowError(
/Did you forget to set \$implicit param/,
);
});
});
});
2 changes: 1 addition & 1 deletion tests/issue-240/test.builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('issue-240:builder', () => {
);

const pure = ngMocks.findInstance(PurePipe);
const impure = ngMocks.get(fixture.point, ImpurePipe);
const impure = ngMocks.findInstance(fixture.point, ImpurePipe);

// Without auto-spy we need the code below.
// Calls would start with 0.
Expand Down
2 changes: 1 addition & 1 deletion tests/issue-240/test.classic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('issue-240:classic', () => {
expect(countPure).toEqual(1);
expect(countImpure).toEqual(2);

const pure = ngMocks.get(fixture.point, PurePipe);
const pure = ngMocks.findInstance(fixture.point, PurePipe);
const impure = ngMocks.findInstance(ImpurePipe);

// We do not have auto-spies, because we provided callbacks.
Expand Down
2 changes: 1 addition & 1 deletion tests/issue-240/test.guts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('issue-240:guts', () => {
`,
);

const pure = ngMocks.get(fixture.point, PurePipe);
const pure = ngMocks.findInstance(fixture.point, PurePipe);
const impure = ngMocks.findInstance(ImpurePipe);

// Without auto-spy we need the code below.
Expand Down

0 comments on commit 03f5f5e

Please sign in to comment.