From 5068407265531af88ad3d7679c7ef028965b1763 Mon Sep 17 00:00:00 2001 From: satanTime Date: Sat, 20 Aug 2022 17:23:16 +0200 Subject: [PATCH] feat(ngMocks): supports custom method names for change and touch #3341 --- .eslintrc.yml | 1 + .../lib/mock-helper/cva/mock-helper.change.ts | 48 ++++++++-- .../lib/mock-helper/cva/mock-helper.touch.ts | 48 ++++++++-- .../src/lib/mock-helper/mock-helper.ts | 4 +- tests/ng-mocks-change/3341.spec.ts | 87 +++++++++++++++++++ tests/ng-mocks-change/reactive-forms.spec.ts | 2 +- tests/ng-mocks-touch/3341.spec.ts | 87 +++++++++++++++++++ tests/ng-mocks-touch/test.spec.ts | 2 +- 8 files changed, 265 insertions(+), 14 deletions(-) create mode 100644 tests/ng-mocks-change/3341.spec.ts create mode 100644 tests/ng-mocks-touch/3341.spec.ts diff --git a/.eslintrc.yml b/.eslintrc.yml index 042f7a89c5..669d7d323b 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -181,6 +181,7 @@ overrides: max-lines-per-function: off unicorn/consistent-function-scoping: off unicorn/prefer-logical-operator-over-ternary: off + '@typescript-eslint/no-empty-function': off - files: - '*.js' diff --git a/libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.change.ts b/libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.change.ts index 3b83ea160a..6248046d5f 100644 --- a/libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.change.ts +++ b/libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.change.ts @@ -4,6 +4,7 @@ import coreForm from '../../common/core.form'; import { DebugNodeSelector } from '../../common/core.types'; import { isMockControlValueAccessor } from '../../common/func.is-mock-control-value-accessor'; import helperDefinePropertyDescriptor from '../../mock-service/helper.define-property-descriptor'; +import helperExtractMethodsFromPrototype from '../../mock-service/helper.extract-methods-from-prototype'; import mockHelperTrigger from '../events/mock-helper.trigger'; import mockHelperFind from '../find/mock-helper.find'; import funcGetLastFixture from '../func.get-last-fixture'; @@ -53,9 +54,38 @@ const handleKnown = (valueAccessor: any, value: any): boolean => { const hasListener = (el: DebugElement): boolean => el.listeners.some(listener => listener.name === 'input' || listener.name === 'change'); -const keys = ['onChange', '_onChange', 'changeFn', '_onChangeCallback', 'onModelChange']; - -export default (selector: DebugNodeSelector, value: any): void => { +const keys = [ + 'onChange', + 'onChangeCallback', + 'onChangeCb', + 'onChangeClb', + 'onChangeFn', + + '_onChange', + '_onChangeCallback', + '_onChangeCb', + '_onChangeClb', + '_onChangeFn', + + 'changeFn', + '_changeFn', + + 'onModelChange', + + 'cvaOnChange', + 'cvaOnChangeCallback', + 'cvaOnChangeCb', + 'cvaOnChangeClb', + 'cvaOnChangeFn', + + '_cvaOnChange', + '_cvaOnChangeCallback', + '_cvaOnChangeCb', + '_cvaOnChangeClb', + '_cvaOnChangeFn', +]; + +export default (selector: DebugNodeSelector, value: any, methodName?: string): void => { const el = mockHelperFind(funcGetLastFixture(), selector, undefined); if (!el) { throw new Error(`Cannot find an element via ngMocks.change(${funcParseFindArgsName(selector)})`); @@ -68,7 +98,7 @@ export default (selector: DebugNodeSelector, value: any): void => { return; } - for (const key of keys) { + for (const key of methodName ? [methodName] : keys) { if (typeof valueAccessor[key] === 'function') { valueAccessor.writeValue(value); valueAccessor[key](value); @@ -77,5 +107,13 @@ export default (selector: DebugNodeSelector, value: any): void => { } } - throw new Error('Unsupported type of ControlValueAccessor'); + const methods = helperExtractMethodsFromPrototype(valueAccessor); + throw new Error( + [ + 'Unsupported type of ControlValueAccessor,', + `please ensure it has '${methodName || 'onChange'}' method.`, + `If it is a 3rd-party library, please provide the correct name of the method in the 'methodName' parameter.`, + 'Possible Names: ' + methods.join(', ') + '.', + ].join(' '), + ); }; diff --git a/libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.touch.ts b/libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.touch.ts index 6853245d0a..e8d2468f04 100644 --- a/libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.touch.ts +++ b/libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.touch.ts @@ -3,6 +3,7 @@ import { DebugElement } from '@angular/core'; import coreForm from '../../common/core.form'; import { DebugNodeSelector } from '../../common/core.types'; import { isMockControlValueAccessor } from '../../common/func.is-mock-control-value-accessor'; +import helperExtractMethodsFromPrototype from '../../mock-service/helper.extract-methods-from-prototype'; import mockHelperTrigger from '../events/mock-helper.trigger'; import mockHelperFind from '../find/mock-helper.find'; import funcGetLastFixture from '../func.get-last-fixture'; @@ -35,9 +36,38 @@ const handleKnown = (valueAccessor: any): boolean => { const hasListener = (el: DebugElement): boolean => el.listeners.some(listener => listener.name === 'focus' || listener.name === 'blur'); -const keys = ['onTouched', '_onTouched', '_cvaOnTouch', '_markAsTouched', '_onTouchedCallback', 'onModelTouched']; - -export default (sel: DebugElement | DebugNodeSelector): void => { +const keys = [ + 'onTouched', + 'onTouchedCallback', + 'onTouchedCb', + 'onTouchedClb', + 'onTouchedFn', + + '_onTouched', + '_onTouchedCallback', + '_onTouchedCb', + '_onTouchedClb', + '_onTouchedFn', + + 'markAsTouched', + '_markAsTouched', + + 'onModelTouched', + + 'cvaOnTouch', + 'cvaOnTouchCallback', + 'cvaOnTouchCb', + 'cvaOnTouchClb', + 'cvaOnTouchFn', + + '_cvaOnTouch', + '_cvaOnTouchCallback', + '_cvaOnTouchCb', + '_cvaOnTouchClb', + '_cvaOnTouchFn', +]; + +export default (sel: DebugElement | DebugNodeSelector, methodName?: string): void => { const el = mockHelperFind(funcGetLastFixture(), sel, undefined); if (!el) { throw new Error(`Cannot find an element via ngMocks.touch(${funcParseFindArgsName(sel)})`); @@ -50,7 +80,7 @@ export default (sel: DebugElement | DebugNodeSelector): void => { return; } - for (const key of keys) { + for (const key of methodName ? [methodName] : keys) { if (typeof valueAccessor[key] === 'function') { valueAccessor[key](); @@ -58,5 +88,13 @@ export default (sel: DebugElement | DebugNodeSelector): void => { } } - throw new Error('Unsupported type of ControlValueAccessor'); + const methods = helperExtractMethodsFromPrototype(valueAccessor); + throw new Error( + [ + 'Unsupported type of ControlValueAccessor,', + `please ensure it has '${methodName || 'onTouched'}' method.`, + `If it is a 3rd-party library, please provide the correct name of the method in the 'methodName' parameter.`, + 'Possible Names: ' + methods.join(', ') + '.', + ].join(' '), + ); }; diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts index 1913672bf3..14a4a94429 100644 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts @@ -152,14 +152,14 @@ export const ngMocks: { * * @see https://ng-mocks.sudo.eu/api/ngMocks/change */ - change(elSelector: DebugNodeSelector, value: any): void; + change(elSelector: DebugNodeSelector, value: any, methodName?: string): void; /** * ngMocks.touch triggers ControlValueAccessor touch. * * @see https://ng-mocks.sudo.eu/api/ngMocks/touch */ - touch(elSelector: DebugNode | DebugNodeSelector): void; + touch(elSelector: DebugNode | DebugNodeSelector, methodName?: string): void; /** * ngMocks.click properly simulates a click on an element. diff --git a/tests/ng-mocks-change/3341.spec.ts b/tests/ng-mocks-change/3341.spec.ts new file mode 100644 index 0000000000..bd1750c9b1 --- /dev/null +++ b/tests/ng-mocks-change/3341.spec.ts @@ -0,0 +1,87 @@ +import { Component, Directive, NgModule } from '@angular/core'; +import { + ControlValueAccessor, + FormControl, + NG_VALUE_ACCESSOR, + ReactiveFormsModule, +} from '@angular/forms'; + +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +@Directive({ + providers: [ + { + multi: true, + provide: NG_VALUE_ACCESSOR, + useExisting: CvaDirective, + }, + ], + selector: 'custom', +}) +class CvaDirective implements ControlValueAccessor { + public registerOnChange = (fn: never) => + (this.customChangeClb = fn); + public registerOnTouched = (fn: never) => + (this.customTouchedClb = fn); + public setDisabledState = () => undefined; + public writeValue = () => undefined; + + public customChangeClb() {} + public customTouchedClb() {} +} + +@Component({ + selector: 'my', + template: ` `, +}) +class MyComponent { + public readonly myControl = new FormControl(); +} + +@NgModule({ + declarations: [MyComponent, CvaDirective], + exports: [MyComponent], + imports: [ReactiveFormsModule], +}) +class MyModule {} + +// @see https://github.com/help-me-mom/ng-mocks/issues/3341 +describe('ng-mocks-change:3341', () => { + beforeEach(() => MockBuilder(MyModule)); + + it('throws error about the default method', () => { + MockRender(MyComponent); + const cvaEl = ngMocks.find('custom'); + + expect(() => ngMocks.change(cvaEl, 123)).toThrowError( + /please ensure it has 'onChange' method/, + ); + }); + + it('throws error with suggestions', () => { + MockRender(MyComponent); + const cvaEl = ngMocks.find('custom'); + + expect(() => ngMocks.change(cvaEl, 123)).toThrowError( + /customChangeClb, customTouchedClb/, + ); + }); + + it('throws error about the wrongly provided method', () => { + MockRender(MyComponent); + const cvaEl = ngMocks.find('custom'); + + expect(() => + ngMocks.change(cvaEl, 123, 'triggerChange'), + ).toThrowError(/please ensure it has 'triggerChange' method/); + }); + + it('triggers change correctly', () => { + const component = MockRender(MyComponent).point.componentInstance; + const cvaEl = ngMocks.find('custom'); + + expect(component.myControl.value).toEqual(null); + ngMocks.change(cvaEl, 123, 'customChangeClb'); + expect(component.myControl.value).toEqual(123); + }); +}); diff --git a/tests/ng-mocks-change/reactive-forms.spec.ts b/tests/ng-mocks-change/reactive-forms.spec.ts index e62929fb17..a3f5b8df5e 100644 --- a/tests/ng-mocks-change/reactive-forms.spec.ts +++ b/tests/ng-mocks-change/reactive-forms.spec.ts @@ -108,7 +108,7 @@ describe('ng-mocks-change:reactive-forms:mock', () => { const valueAccessorEl = ngMocks.find('custom'); expect(() => ngMocks.change(valueAccessorEl, 123)).toThrowError( - 'Unsupported type of ControlValueAccessor', + /Unsupported type of ControlValueAccessor/, ); }); }); diff --git a/tests/ng-mocks-touch/3341.spec.ts b/tests/ng-mocks-touch/3341.spec.ts new file mode 100644 index 0000000000..f63267a143 --- /dev/null +++ b/tests/ng-mocks-touch/3341.spec.ts @@ -0,0 +1,87 @@ +import { Component, Directive, NgModule } from '@angular/core'; +import { + ControlValueAccessor, + FormControl, + NG_VALUE_ACCESSOR, + ReactiveFormsModule, +} from '@angular/forms'; + +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +@Directive({ + providers: [ + { + multi: true, + provide: NG_VALUE_ACCESSOR, + useExisting: CvaDirective, + }, + ], + selector: 'custom', +}) +class CvaDirective implements ControlValueAccessor { + public registerOnChange = (fn: never) => + (this.customChangeClb = fn); + public registerOnTouched = (fn: never) => + (this.customTouchedClb = fn); + public setDisabledState = () => undefined; + public writeValue = () => undefined; + + public customChangeClb() {} + public customTouchedClb() {} +} + +@Component({ + selector: 'my', + template: ` `, +}) +class MyComponent { + public readonly myControl = new FormControl(); +} + +@NgModule({ + declarations: [MyComponent, CvaDirective], + exports: [MyComponent], + imports: [ReactiveFormsModule], +}) +class MyModule {} + +// @see https://github.com/help-me-mom/ng-mocks/issues/3341 +describe('ng-mocks-touch:3341', () => { + beforeEach(() => MockBuilder(MyModule)); + + it('throws error about the default method', () => { + MockRender(MyComponent); + const cvaEl = ngMocks.find('custom'); + + expect(() => ngMocks.touch(cvaEl)).toThrowError( + /please ensure it has 'onTouched' method/, + ); + }); + + it('throws error with suggestions', () => { + MockRender(MyComponent); + const cvaEl = ngMocks.find('custom'); + + expect(() => ngMocks.touch(cvaEl)).toThrowError( + /customChangeClb, customTouchedClb/, + ); + }); + + it('throws error about the wrongly provided method', () => { + MockRender(MyComponent); + const cvaEl = ngMocks.find('custom'); + + expect(() => ngMocks.touch(cvaEl, 'triggerTouch')).toThrowError( + /please ensure it has 'triggerTouch' method/, + ); + }); + + it('triggers touch correctly', () => { + const component = MockRender(MyComponent).point.componentInstance; + const cvaEl = ngMocks.find('custom'); + + expect(component.myControl.touched).toEqual(false); + ngMocks.touch(cvaEl, 'customTouchedClb'); + expect(component.myControl.touched).toEqual(true); + }); +}); diff --git a/tests/ng-mocks-touch/test.spec.ts b/tests/ng-mocks-touch/test.spec.ts index 1fde3183d2..6d9d5a82b5 100644 --- a/tests/ng-mocks-touch/test.spec.ts +++ b/tests/ng-mocks-touch/test.spec.ts @@ -95,7 +95,7 @@ describe('ng-mocks-touch:mock', () => { const valueAccessorEl = ngMocks.find('custom'); expect(() => ngMocks.touch(valueAccessorEl)).toThrowError( - 'Unsupported type of ControlValueAccessor', + /Unsupported type of ControlValueAccessor/, ); }); });