From eda5bc974f69b37e5b5fa81941e8f18d5974947a Mon Sep 17 00:00:00 2001 From: satanTime Date: Sat, 7 May 2022 15:46:54 +0200 Subject: [PATCH] fix(MockRender): renders pipes with $implicit param #2398 --- docs/articles/api/MockRender.md | 21 +++++ docs/articles/guides/pipe.md | 10 +-- examples/TestPipe/test.spec.ts | 6 +- libs/ng-mocks/src/lib/common/core.helpers.ts | 5 +- .../lib/mock-builder/mock-builder.promise.ts | 1 - .../lib/mock-render/func.create-wrapper.ts | 1 + .../lib/mock-render/func.generate-template.ts | 5 ++ .../lib/mock-render/mock-render-factory.ts | 43 ++++++++-- .../src/lib/mock-render/mock-render.spec.ts | 5 -- .../src/lib/mock-render/mock-render.ts | 20 ++--- tests/issue-2398/test.spec.ts | 82 +++++++++++++++++++ 11 files changed, 164 insertions(+), 35 deletions(-) create mode 100644 tests/issue-2398/test.spec.ts diff --git a/docs/articles/api/MockRender.md b/docs/articles/api/MockRender.md index edeb540659..69fce3f3f0 100644 --- a/docs/articles/api/MockRender.md +++ b/docs/articles/api/MockRender.md @@ -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 diff --git a/docs/articles/guides/pipe.md b/docs/articles/guides/pipe.md index 00a4efc332..eaefd784f5 100644 --- a/docs/articles/guides/pipe.md +++ b/docs/articles/guides/pipe.md @@ -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'], }); ``` @@ -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'], }); diff --git a/examples/TestPipe/test.spec.ts b/examples/TestPipe/test.spec.ts index f196e02bbc..cba395642f 100644 --- a/examples/TestPipe/test.spec.ts +++ b/examples/TestPipe/test.spec.ts @@ -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'], }); diff --git a/libs/ng-mocks/src/lib/common/core.helpers.ts b/libs/ng-mocks/src/lib/common/core.helpers.ts index fbbc3669db..5aeb823664 100644 --- a/libs/ng-mocks/src/lib/common/core.helpers.ts +++ b/libs/ng-mocks/src/lib/common/core.helpers.ts @@ -14,10 +14,9 @@ import funcGetName from './func.get-name'; * @internal */ export const getTestBedInjection = (token: AnyType | InjectionToken): 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; } @@ -29,7 +28,7 @@ export const getTestBedInjection = (token: AnyType | InjectionToken): I * @deprecated * @internal */ -export const getInjection = (token: Type | InjectionToken): I => { +export const getInjection = (token: AnyType | InjectionToken): I => { const testBed: any = getTestBed(); // istanbul ignore next diff --git a/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts b/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts index 4916b9a1bc..53a3f84365 100644 --- a/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts +++ b/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts @@ -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)); } diff --git a/libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts b/libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts index ad7be7286d..12be436d41 100644 --- a/libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts +++ b/libs/ng-mocks/src/lib/mock-render/func.create-wrapper.ts @@ -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); diff --git a/libs/ng-mocks/src/lib/mock-render/func.generate-template.ts b/libs/ng-mocks/src/lib/mock-render/func.generate-template.ts index 04557053fd..2d237a05e8 100644 --- a/libs/ng-mocks/src/lib/mock-render/func.generate-template.ts +++ b/libs/ng-mocks/src/lib/mock-render/func.generate-template.ts @@ -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 => { @@ -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'); diff --git a/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts b/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts index 6b143004a1..8d31c688a8 100644 --- a/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts +++ b/libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts @@ -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'; @@ -23,21 +25,41 @@ export interface MockRenderFactory { >(params?: Partial, detectChanges?: boolean): MockedComponentFixture; } -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); } @@ -111,7 +133,7 @@ const generateFactoryInstall = (ctor: AnyType, options: IMockRenderFactoryO }; const generateFactory = ( - componentCtor: Type, + componentCtor: Type & { tpl?: string }, bindings: undefined | null | string[], template: any, options: IMockRenderFactoryOptions, @@ -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); diff --git a/libs/ng-mocks/src/lib/mock-render/mock-render.spec.ts b/libs/ng-mocks/src/lib/mock-render/mock-render.spec.ts index d5e8ae8ff7..26893ca1a1 100644 --- a/libs/ng-mocks/src/lib/mock-render/mock-render.spec.ts +++ b/libs/ng-mocks/src/lib/mock-render/mock-render.spec.ts @@ -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, diff --git a/libs/ng-mocks/src/lib/mock-render/mock-render.ts b/libs/ng-mocks/src/lib/mock-render/mock-render.ts index 43e986edae..a3303361e8 100644 --- a/libs/ng-mocks/src/lib/mock-render/mock-render.ts +++ b/libs/ng-mocks/src/lib/mock-render/mock-render.ts @@ -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; + /** * This signature of MockRender lets create a fixture to access a token. * @@ -57,13 +63,6 @@ export function MockRender(template: AnyType): MockedComponentFixture; -/** - * 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 & { point: undefined }; - /** * This signature of MockRender without params should not autocomplete any keys of any types. * @@ -105,13 +104,14 @@ export function MockRender ): MockedComponentFixture; export function MockRender>( - template: string | AnyType | InjectionToken, + template?: string | AnyType | InjectionToken, 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); } diff --git a/tests/issue-2398/test.spec.ts b/tests/issue-2398/test.spec.ts new file mode 100644 index 0000000000..c5293f9b89 --- /dev/null +++ b/tests/issue-2398/test.spec.ts @@ -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/, + ); + }); + }); +});