From 9297cd13a47d258e8180e25ac1352982801a0282 Mon Sep 17 00:00:00 2001 From: MG Date: Sun, 13 Dec 2020 14:21:30 +0100 Subject: [PATCH] fix: now MockRender's proxy component respects outside params changes --- examples/TestLifecycleHooks/fixtures.ts | 101 ++++++++++++++++ examples/TestLifecycleHooks/test.spec.ts | 112 +---------------- .../TestLifecycleHooks/test.string.spec.ts | 93 ++++++++++++++ ...ec.ts => test.type-without-params.spec.ts} | 114 +----------------- lib/mock-builder/mock-builder.ts | 2 + lib/mock-render/mock-render.ts | 90 ++++++++------ ...helper.extract-property-descriptor.spec.ts | 9 ++ tests-failures/mock-render-string.ts | 96 +++++++++++++++ tests-failures/mock-render-type.ts | 87 +++++++++++++ tests/mock-render-param-ref/test.spect.ts | 82 +++++++++++++ 10 files changed, 535 insertions(+), 251 deletions(-) create mode 100644 examples/TestLifecycleHooks/fixtures.ts create mode 100644 examples/TestLifecycleHooks/test.string.spec.ts rename examples/TestLifecycleHooks/{without-params.spec.ts => test.type-without-params.spec.ts} (65%) create mode 100644 lib/mock-service/helper.extract-property-descriptor.spec.ts create mode 100644 tests-failures/mock-render-string.ts create mode 100644 tests-failures/mock-render-type.ts create mode 100644 tests/mock-render-param-ref/test.spect.ts diff --git a/examples/TestLifecycleHooks/fixtures.ts b/examples/TestLifecycleHooks/fixtures.ts new file mode 100644 index 0000000000..8b4278277e --- /dev/null +++ b/examples/TestLifecycleHooks/fixtures.ts @@ -0,0 +1,101 @@ +import { + AfterContentChecked, + AfterContentInit, + AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, + Component, + Injectable, + Input, + NgModule, + OnChanges, + OnDestroy, + OnInit, +} from '@angular/core'; + +// A dummy service we are going to replace with its mock copy and to use for assertions. +@Injectable() +export class TargetService { + protected called = false; + + public afterContentChecked() { + this.called = true; + } + + public afterContentInit() { + this.called = true; + } + + public afterViewChecked() { + this.called = true; + } + + public afterViewInit() { + this.called = true; + } + + public ctor() { + this.called = true; + } + + public onChanges() { + this.called = true; + } + + public onDestroy() { + this.called = true; + } + + public onInit() { + this.called = true; + } +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'target', + template: ``, +}) +export class TargetComponent + implements OnInit, OnDestroy, OnChanges, AfterViewInit, AfterViewChecked, AfterContentInit, AfterContentChecked { + @Input() public input: string | null = null; + + public constructor(protected readonly service: TargetService) { + this.service.ctor(); + } + + public ngAfterContentChecked(): void { + this.service.afterContentChecked(); + } + + public ngAfterContentInit(): void { + this.service.afterContentInit(); + } + + public ngAfterViewChecked(): void { + this.service.afterViewChecked(); + } + + public ngAfterViewInit(): void { + this.service.afterViewInit(); + } + + public ngOnChanges(): void { + this.service.onChanges(); + } + + public ngOnDestroy(): void { + this.service.onDestroy(); + } + + public ngOnInit(): void { + this.service.onInit(); + } +} + +@NgModule({ + declarations: [TargetComponent], + exports: [TargetComponent], + providers: [TargetService], +}) +export class TargetModule {} diff --git a/examples/TestLifecycleHooks/test.spec.ts b/examples/TestLifecycleHooks/test.spec.ts index da344baaf6..6a61905416 100644 --- a/examples/TestLifecycleHooks/test.spec.ts +++ b/examples/TestLifecycleHooks/test.spec.ts @@ -1,113 +1,11 @@ -import { - AfterContentChecked, - AfterContentInit, - AfterViewChecked, - AfterViewInit, - ChangeDetectionStrategy, - Component, - Injectable, - Input, - NgModule, - OnChanges, - OnDestroy, - OnInit, -} from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; -// A dummy service we are going to replace with its mock copy and to use for assertions. -@Injectable() -class TargetService { - protected called = false; - - public afterContentChecked() { - this.called = true; - } - - public afterContentInit() { - this.called = true; - } - - public afterViewChecked() { - this.called = true; - } - - public afterViewInit() { - this.called = true; - } - - public ctor() { - this.called = true; - } - - public onChanges() { - this.called = true; - } - - public onDestroy() { - this.called = true; - } - - public onInit() { - this.called = true; - } -} - -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'target', - template: ``, -}) -class TargetComponent - implements - OnInit, - OnDestroy, - OnChanges, - AfterViewInit, - AfterViewChecked, - AfterContentInit, - AfterContentChecked { - @Input() public input: string | null = null; - - public constructor(protected readonly service: TargetService) { - this.service.ctor(); - } - - public ngAfterContentChecked(): void { - this.service.afterContentChecked(); - } - - public ngAfterContentInit(): void { - this.service.afterContentInit(); - } - - public ngAfterViewChecked(): void { - this.service.afterViewChecked(); - } - - public ngAfterViewInit(): void { - this.service.afterViewInit(); - } - - public ngOnChanges(): void { - this.service.onChanges(); - } - - public ngOnDestroy(): void { - this.service.onDestroy(); - } - - public ngOnInit(): void { - this.service.onInit(); - } -} - -@NgModule({ - declarations: [TargetComponent], - exports: [TargetComponent], - providers: [TargetService], -}) -class TargetModule {} +import { + TargetComponent, + TargetModule, + TargetService, +} from './fixtures'; describe('TestLifecycleHooks', () => { ngMocks.faster(); diff --git a/examples/TestLifecycleHooks/test.string.spec.ts b/examples/TestLifecycleHooks/test.string.spec.ts new file mode 100644 index 0000000000..c5e527719a --- /dev/null +++ b/examples/TestLifecycleHooks/test.string.spec.ts @@ -0,0 +1,93 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +import { + TargetComponent, + TargetModule, + TargetService, +} from './fixtures'; + +describe('TestLifecycleHooks:string', () => { + ngMocks.faster(); + + // Do not forget to return the promise of MockBuilder. + beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + + it('triggers lifecycle hooks correctly via MockRender w/ params', () => { + // First let's suppress detectChanges. + const fixture = MockRender( + '', + { input: '' }, + { + detectChanges: false, + }, + ); + + const service: TargetService = TestBed.get(TargetService); + + // By default nothing should be initialized, but ctor. + expect(service.ctor).toHaveBeenCalledTimes(1); // changed + expect(service.onInit).toHaveBeenCalledTimes(0); + expect(service.onDestroy).toHaveBeenCalledTimes(0); + expect(service.onChanges).toHaveBeenCalledTimes(0); + expect(service.afterViewInit).toHaveBeenCalledTimes(0); + expect(service.afterViewChecked).toHaveBeenCalledTimes(0); + expect(service.afterContentInit).toHaveBeenCalledTimes(0); + expect(service.afterContentChecked).toHaveBeenCalledTimes(0); + + // Now let's render the component. + fixture.detectChanges(); + + // This calls everything except onDestroy and onChanges. + expect(service.ctor).toHaveBeenCalledTimes(1); + expect(service.onInit).toHaveBeenCalledTimes(1); // changed + expect(service.onDestroy).toHaveBeenCalledTimes(0); + expect(service.onChanges).toHaveBeenCalledTimes(1); // changed + expect(service.afterViewInit).toHaveBeenCalledTimes(1); // changed + expect(service.afterViewChecked).toHaveBeenCalledTimes(1); // changed + expect(service.afterContentInit).toHaveBeenCalledTimes(1); // changed + expect(service.afterContentChecked).toHaveBeenCalledTimes(1); // changed + + // Let's change it. + fixture.componentInstance.input = 'change'; + fixture.detectChanges(); + + // Only OnChange, AfterViewChecked, AfterContentChecked + // should be triggered. + expect(service.ctor).toHaveBeenCalledTimes(1); + expect(service.onInit).toHaveBeenCalledTimes(1); + expect(service.onDestroy).toHaveBeenCalledTimes(0); + expect(service.onChanges).toHaveBeenCalledTimes(2); // changed + expect(service.afterViewInit).toHaveBeenCalledTimes(1); + expect(service.afterViewChecked).toHaveBeenCalledTimes(2); // changed + expect(service.afterContentInit).toHaveBeenCalledTimes(1); + expect(service.afterContentChecked).toHaveBeenCalledTimes(2); // changed + + // Let's cause more changes. + fixture.detectChanges(); + fixture.detectChanges(); + + // Only AfterViewChecked, AfterContentChecked should be triggered. + expect(service.ctor).toHaveBeenCalledTimes(1); + expect(service.onInit).toHaveBeenCalledTimes(1); + expect(service.onDestroy).toHaveBeenCalledTimes(0); + expect(service.onChanges).toHaveBeenCalledTimes(2); + expect(service.afterViewInit).toHaveBeenCalledTimes(1); + expect(service.afterViewChecked).toHaveBeenCalledTimes(4); // changed + expect(service.afterContentInit).toHaveBeenCalledTimes(1); + expect(service.afterContentChecked).toHaveBeenCalledTimes(4); // changed + + // Let's destroy it. + fixture.destroy(); + + // This all calls except onDestroy and onChanges. + expect(service.ctor).toHaveBeenCalledTimes(1); + expect(service.onInit).toHaveBeenCalledTimes(1); + expect(service.onDestroy).toHaveBeenCalledTimes(1); // changed + expect(service.onChanges).toHaveBeenCalledTimes(2); + expect(service.afterViewInit).toHaveBeenCalledTimes(1); + expect(service.afterViewChecked).toHaveBeenCalledTimes(4); + expect(service.afterContentInit).toHaveBeenCalledTimes(1); + expect(service.afterContentChecked).toHaveBeenCalledTimes(4); + }); +}); diff --git a/examples/TestLifecycleHooks/without-params.spec.ts b/examples/TestLifecycleHooks/test.type-without-params.spec.ts similarity index 65% rename from examples/TestLifecycleHooks/without-params.spec.ts rename to examples/TestLifecycleHooks/test.type-without-params.spec.ts index a08311b202..1ca3990fe0 100644 --- a/examples/TestLifecycleHooks/without-params.spec.ts +++ b/examples/TestLifecycleHooks/test.type-without-params.spec.ts @@ -1,115 +1,13 @@ -import { - AfterContentChecked, - AfterContentInit, - AfterViewChecked, - AfterViewInit, - ChangeDetectionStrategy, - Component, - Injectable, - Input, - NgModule, - OnChanges, - OnDestroy, - OnInit, -} from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; -// A dummy service we are going to replace with its mock copy and to use for assertions. -@Injectable() -class TargetService { - protected called = false; - - public afterContentChecked() { - this.called = true; - } - - public afterContentInit() { - this.called = true; - } - - public afterViewChecked() { - this.called = true; - } - - public afterViewInit() { - this.called = true; - } - - public ctor() { - this.called = true; - } - - public onChanges() { - this.called = true; - } - - public onDestroy() { - this.called = true; - } - - public onInit() { - this.called = true; - } -} - -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'target', - template: ``, -}) -class TargetComponent - implements - OnInit, - OnDestroy, - OnChanges, - AfterViewInit, - AfterViewChecked, - AfterContentInit, - AfterContentChecked { - @Input() public input: string | null = null; - - public constructor(protected readonly service: TargetService) { - this.service.ctor(); - } - - public ngAfterContentChecked(): void { - this.service.afterContentChecked(); - } - - public ngAfterContentInit(): void { - this.service.afterContentInit(); - } - - public ngAfterViewChecked(): void { - this.service.afterViewChecked(); - } - - public ngAfterViewInit(): void { - this.service.afterViewInit(); - } - - public ngOnChanges(): void { - this.service.onChanges(); - } - - public ngOnDestroy(): void { - this.service.onDestroy(); - } - - public ngOnInit(): void { - this.service.onInit(); - } -} - -@NgModule({ - declarations: [TargetComponent], - exports: [TargetComponent], - providers: [TargetService], -}) -class TargetModule {} +import { + TargetComponent, + TargetModule, + TargetService, +} from './fixtures'; -describe('TestLifecycleHooks:w/o-params', () => { +describe('TestLifecycleHooks:type-without-params', () => { ngMocks.faster(); // Do not forget to return the promise of MockBuilder. diff --git a/lib/mock-builder/mock-builder.ts b/lib/mock-builder/mock-builder.ts index c39afe3872..f62790ae38 100644 --- a/lib/mock-builder/mock-builder.ts +++ b/lib/mock-builder/mock-builder.ts @@ -103,6 +103,8 @@ const resetTestingModule = ( return TestBed; } + ngMocksUniverse.global.delete('builder:config'); + ngMocksUniverse.global.delete('builder:module'); ngMocksUniverse.global.delete('bullet:customized'); ngMocksUniverse.global.delete('bullet:reset'); applyNgMocksOverrides(TestBed); diff --git a/lib/mock-render/mock-render.ts b/lib/mock-render/mock-render.ts index 2b2e9e4953..869605690f 100644 --- a/lib/mock-render/mock-render.ts +++ b/lib/mock-render/mock-render.ts @@ -23,27 +23,17 @@ const solveOutput = (output: any): string => { return '=$event'; }; -const defineProperty = (componentInstance: any, key: string, params: any) => { - Object.defineProperty(componentInstance, key, { - ...params, - configurable: true, - enumerable: true, - ...(params.value ? { writable: true } : {}), - }); -}; - const createProperty = (pointComponentInstance: Record, key: string) => { - const def = helperMockService.extractPropertyDescriptor(Object.getPrototypeOf(pointComponentInstance), key); - const keyType = def ? undefined : typeof pointComponentInstance[key]; - - return def?.value || keyType === 'function' - ? { - value: (...args: any[]) => pointComponentInstance[key](...args), + return { + get: () => { + if (typeof pointComponentInstance[key] === 'function') { + return (...args: any[]) => pointComponentInstance[key](...args); } - : { - get: () => pointComponentInstance[key], - set: (v: any) => (pointComponentInstance[key] = v), - }; + + return pointComponentInstance[key]; + }, + set: (v: any) => (pointComponentInstance[key] = v), + }; }; const extractAllKeys = (instance: object) => [ @@ -54,16 +44,21 @@ const extractAllKeys = (instance: object) => [ const extractOwnKeys = (instance: object) => [...Object.getOwnPropertyNames(instance), ...Object.keys(instance)]; -const installProxy = (componentInstance: Record, pointComponentInstance: Record) => { - const exists = extractOwnKeys(componentInstance); +const installProxy = ( + componentInstance: Record, + pointComponentInstance?: Record, +): void => { + if (!pointComponentInstance) { + return; + } + const exists = extractOwnKeys(componentInstance); for (const key of extractAllKeys(pointComponentInstance)) { if (exists.indexOf(key) !== -1) { continue; } - defineProperty(componentInstance, key, createProperty(pointComponentInstance, key)); - + Object.defineProperty(componentInstance, key, createProperty(pointComponentInstance, key)); exists.push(key); } }; @@ -111,9 +106,7 @@ const applyParamsToFixtureInstance = ( inputs: string[], outputs: string[], ): void => { - for (const key of Object.keys(params || {})) { - instance[key] = params[key]; - } + installProxy(instance, params); for (const definition of applyParamsToFixtureInstanceGetData(params, inputs)) { const [property] = definition.split(': '); instance[property] = undefined; @@ -156,26 +149,26 @@ const tryWhen = (flag: boolean, callback: () => void) => { /** * @see https://github.com/ike18t/ng-mocks#mockrender */ -function MockRender>( +function MockRender( template: Type, - params: TComponent, + params: undefined, detectChanges?: boolean | IMockRenderOptions, -): MockedComponentFixture; +): MockedComponentFixture; /** - * Without params we shouldn't autocomplete any keys of any types. - * * @see https://github.com/ike18t/ng-mocks#mockrender */ -function MockRender>( +function MockRender( template: Type, -): MockedComponentFixture; + params: TComponent, + detectChanges?: boolean | IMockRenderOptions, +): MockedComponentFixture; /** * @see https://github.com/ike18t/ng-mocks#mockrender */ -function MockRender = Record>( - template: string, +function MockRender>( + template: Type, params: TComponent, detectChanges?: boolean | IMockRenderOptions, ): MockedComponentFixture; @@ -185,7 +178,32 @@ function MockRender * * @see https://github.com/ike18t/ng-mocks#mockrender */ -function MockRender(template: string): MockedComponentFixture; +function MockRender(template: Type): MockedComponentFixture; + +/** + * Without params we shouldn't autocomplete any keys of any types. + * + * @see https://github.com/ike18t/ng-mocks#mockrender + */ +function MockRender(template: string): MockedComponentFixture; + +/** + * @see https://github.com/ike18t/ng-mocks#mockrender + */ +function MockRender( + template: string, + params: Record, + detectChanges?: boolean | IMockRenderOptions, +): MockedComponentFixture>; + +/** + * @see https://github.com/ike18t/ng-mocks#mockrender + */ +function MockRender = Record>( + template: string, + params: TComponent, + detectChanges?: boolean | IMockRenderOptions, +): MockedComponentFixture; function MockRender>( template: string | Type, diff --git a/lib/mock-service/helper.extract-property-descriptor.spec.ts b/lib/mock-service/helper.extract-property-descriptor.spec.ts new file mode 100644 index 0000000000..4ecfa80fa4 --- /dev/null +++ b/lib/mock-service/helper.extract-property-descriptor.spec.ts @@ -0,0 +1,9 @@ +import helperExtractPropertyDescriptor from './helper.extract-property-descriptor'; + +describe('helper.extract-property-descriptor', () => { + it('returns undefined on null', () => { + expect( + helperExtractPropertyDescriptor(null, 'test'), + ).toBeUndefined(); + }); +}); diff --git a/tests-failures/mock-render-string.ts b/tests-failures/mock-render-string.ts new file mode 100644 index 0000000000..5b40743c7a --- /dev/null +++ b/tests-failures/mock-render-string.ts @@ -0,0 +1,96 @@ +import { MockRender } from 'ng-mocks'; + +declare class TargetComponent { + public readonly ro: string; + public rw: string; +} + +declare class WrongComponent { + public readonly rw1: string; +} + +// if we provide neither a type or params, +// the should be undefined. +const fixture1 = MockRender('test'); +// @ts-expect-error: fails due to unknown type. +fixture1.componentInstance.rw = '123'; +// @ts-expect-error: fails due to unknown type. +fixture1.componentInstance.rw = 123; +// @ts-expect-error: fails due to unknown type. +fixture1.componentInstance.ro = '123'; +// does not fail because it's undefined. +fixture1.componentInstance = undefined; +// @ts-expect-error: fails because it isn't defined +fixture1.point.componentInstance = new TargetComponent(); +// @ts-expect-error: fails because it isn't defined +fixture1.point.componentInstance = new WrongComponent(); + +// If we provide a type only, then it's direct proxy +// and both componentInstance and point should have its type. +const fixture2 = MockRender('test'); +// componentInstance works +fixture2.componentInstance.rw = '123'; +// @ts-expect-error: fails due to wrong type. +fixture2.componentInstance.rw = 123; +// @ts-expect-error: fails due to readonly. +fixture2.componentInstance.ro = '123'; +// @ts-expect-error: fails because it's defined. +fixture2.componentInstance = undefined; +// does not fail because of the correct type +fixture2.point.componentInstance = new TargetComponent(); +// @ts-expect-error: fails because of a wrong type +fixture2.point.componentInstance = new WrongComponent(); + +// TODO try to make it precise +// if we provide params only then point is undefined, +// and componentInstance is anything. +const fixture3 = MockRender('test', { k1: 123, k2: '123' }); +fixture3.componentInstance.k1 = 123; +fixture3.componentInstance.k1 = '123'; +fixture3.componentInstance.k2 = 123; +fixture3.componentInstance.k2 = '123'; +fixture3.componentInstance.rw = '123'; +fixture3.componentInstance.rw = 123; +fixture3.componentInstance.ro = '123'; +// @ts-expect-error: fails because params are defined. +fixture3.componentInstance = undefined; +// @ts-expect-error: fails because it isn't defined +fixture3.point.componentInstance = new TargetComponent(); +// @ts-expect-error: fails because it isn't defined +fixture3.point.componentInstance = new WrongComponent(); + +// TODO try to make it precise +// if we provide both, then componentInstance is anything, +// and point is the type. +const fixture4 = MockRender('test', { k1: 123, k2: '123' }); +fixture4.componentInstance.k1 = 123; +fixture4.componentInstance.k1 = '123'; +fixture4.componentInstance.k2 = 123; +fixture4.componentInstance.k2 = '123'; +fixture4.componentInstance.rw = '123'; +fixture4.componentInstance.rw = 123; +fixture4.componentInstance.ro = '123'; +// does not fail because of the correct type +fixture4.point.componentInstance = new TargetComponent(); +// @ts-expect-error: fails because of a wrong type +fixture4.point.componentInstance = new WrongComponent(); + +// if we provide both types, then componentInstance is the type, +// and point is a predefined type. +const fixture5 = MockRender('test', { k1: 123, k2: '123' }); +fixture5.componentInstance.k1 = 123; +// @ts-expect-error: fails due to wrong type. +fixture5.componentInstance.k1 = '123'; +// @ts-expect-error: fails due to wrong type. +fixture5.componentInstance.k2 = 123; +fixture5.componentInstance.k2 = '123'; +// @ts-expect-error: fails due missed declaration. +fixture5.componentInstance.rw = '123'; +// @ts-expect-error: fails due missed declaration. +fixture5.componentInstance.rw = 123; +// @ts-expect-error: fails due missed declaration. +fixture5.componentInstance.ro = '123'; +// does not fail because of the correct type +fixture5.point.componentInstance = new TargetComponent(); +// @ts-expect-error: fails because of a wrong type +fixture5.point.componentInstance = new WrongComponent(); diff --git a/tests-failures/mock-render-type.ts b/tests-failures/mock-render-type.ts new file mode 100644 index 0000000000..1cb2eae928 --- /dev/null +++ b/tests-failures/mock-render-type.ts @@ -0,0 +1,87 @@ +import { MockRender } from 'ng-mocks'; + +declare class TargetComponent { + public readonly ro: string; + public rw: string; +} + +declare class WrongComponent { + public readonly rw1: string; +} + +// if we provide neither a type or params, +// then the provided component should be used +const fixture1 = MockRender(TargetComponent); +// Works +fixture1.componentInstance.rw = '123'; +// @ts-expect-error: fails due to unknown type. +fixture1.componentInstance.rw = 123; +// @ts-expect-error: fails due to unknown type. +fixture1.componentInstance.ro = '123'; +// @ts-expect-error: fails because it's defined. +fixture1.componentInstance = undefined; +// does not fail because of the correct type +fixture1.point.componentInstance = new TargetComponent(); +// @ts-expect-error: fails because of a wrong type +fixture1.point.componentInstance = new WrongComponent(); + +// @ts-expect-error: fails due to wrong type. +const fixture2 = MockRender(TargetComponent); + +// if we provide params only then point is undefined, +// and componentInstance is anything. +const fixture3 = MockRender(TargetComponent, { k1: 123, k2: '123' }); +fixture3.componentInstance.k1 = 123; +// @ts-expect-error: fails due to the wrong type. +fixture3.componentInstance.k1 = '123'; +// @ts-expect-error: fails due to the wrong type. +fixture3.componentInstance.k2 = 123; +fixture3.componentInstance.k2 = '123'; +// @ts-expect-error: fails due to the unknown prop. +fixture3.componentInstance.rw = '123'; +// @ts-expect-error: fails due to the unknown prop. +fixture3.componentInstance.rw = 123; +// @ts-expect-error: fails due to the unknown prop. +fixture3.componentInstance.ro = '123'; +// @ts-expect-error: fails because params are defined. +fixture3.componentInstance = undefined; +// does not fail because of the correct type +fixture3.point.componentInstance = new TargetComponent(); +// @ts-expect-error: fails because of a wrong type +fixture3.point.componentInstance = new WrongComponent(); + +// TODO try to make it precise +// if we provide both, then componentInstance is anything, +// and point is the type. +const fixture4 = MockRender(TargetComponent, { k1: 123, k2: '123' }); +fixture4.componentInstance.k1 = 123; +fixture4.componentInstance.k1 = '123'; +fixture4.componentInstance.k2 = 123; +fixture4.componentInstance.k2 = '123'; +fixture4.componentInstance.rw = '123'; +fixture4.componentInstance.rw = 123; +fixture4.componentInstance.ro = '123'; +// does not fail because of the correct type +fixture4.point.componentInstance = new TargetComponent(); +// @ts-expect-error: fails because of a wrong type +fixture4.point.componentInstance = new WrongComponent(); + +// if we provide both types, then componentInstance is the type, +// and point is a predefined type. +const fixture5 = MockRender(TargetComponent, { k1: 123, k2: '123' }); +fixture5.componentInstance.k1 = 123; +// @ts-expect-error: fails due to wrong type. +fixture5.componentInstance.k1 = '123'; +// @ts-expect-error: fails due to wrong type. +fixture5.componentInstance.k2 = 123; +fixture5.componentInstance.k2 = '123'; +// @ts-expect-error: fails due missed declaration. +fixture5.componentInstance.rw = '123'; +// @ts-expect-error: fails due missed declaration. +fixture5.componentInstance.rw = 123; +// @ts-expect-error: fails due missed declaration. +fixture5.componentInstance.ro = '123'; +// does not fail because of the correct type +fixture5.point.componentInstance = new TargetComponent(); +// @ts-expect-error: fails because of a wrong type +fixture5.point.componentInstance = new WrongComponent(); diff --git a/tests/mock-render-param-ref/test.spect.ts b/tests/mock-render-param-ref/test.spect.ts new file mode 100644 index 0000000000..75c1b3ff8d --- /dev/null +++ b/tests/mock-render-param-ref/test.spect.ts @@ -0,0 +1,82 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +@Component({ + selector: 'target', + template: '="{{ input }}"=', +}) +class TargetComponent { + @Input() public input: string | null = null; + @Output() public readonly output = new EventEmitter(); + @Output() public readonly outputThis = new EventEmitter(); + + public emit(): void { + this.output.emit(`${this.input}`); + } + + public emitThis(): void { + this.outputThis.emit(this); + } +} + +describe('mock-render-param-ref', () => { + beforeEach(() => MockBuilder(TargetComponent)); + + // The idea is that we can control the render component + // via the passed params object. + it('keeps refs w/ params', () => { + const params = { + input: 'v1', + output: jasmine.createSpy('output'), + }; + + // By default params are set to the render component. + const fixture = MockRender(TargetComponent, params); + expect(fixture.nativeElement.innerHTML).toContain('="v1"='); + fixture.point.componentInstance.emit(); + expect(params.output).toHaveBeenCalledWith('v1'); + + // Let's assert that if we change params then + // the render component respects it. + params.input = 'v2'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain('="v2"='); + expect(fixture.componentInstance.input).toEqual('v2'); + fixture.point.componentInstance.emit(); + expect(params.output).toHaveBeenCalledWith('v2'); + + // Let's assert that if we change the render + // component, the params are updated too. + fixture.componentInstance.input = 'v3'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain('="v3"='); + expect(params.input).toEqual('v3'); + + // Let's assert that spies have the same behavior. + const currentSpy = params.output; + const newSpy = jasmine.createSpy('new'); + params.output = newSpy; + fixture.point.componentInstance.emit(); + expect(currentSpy).not.toHaveBeenCalledWith('v3'); + expect(newSpy).toHaveBeenCalledWith('v3'); + }); + + // If we don't pass params then only non inputs / outputs + // should be handles by proxy. + it('keeps refs w/o params', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.point.componentInstance).not.toBe(fixture.componentInstance); + + const spyOutput = jasmine.createSpy('output'); + fixture.componentInstance.output.subscribe(spyOutput); + fixture.componentInstance.input = 'v1'; + fixture.detectChanges(); + fixture.componentInstance.emit(); + expect(spyOutput).toHaveBeenCalledWith('v1'); + + const spyThis = jasmine.createSpy('this'); + fixture.componentInstance.outputThis.subscribe(spyThis); + fixture.componentInstance.emitThis(); + expect(spyThis).toHaveBeenCalledWith(fixture.point.componentInstance); + }); +});