Skip to content

Commit

Permalink
feat(ngMocks): supports custom method names for change and touch #3341
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Aug 20, 2022
1 parent edb2dd8 commit 5068407
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 14 deletions.
1 change: 1 addition & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
48 changes: 43 additions & 5 deletions libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)})`);
Expand All @@ -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);
Expand All @@ -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(' '),
);
};
48 changes: 43 additions & 5 deletions libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.touch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)})`);
Expand All @@ -50,13 +80,21 @@ 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]();

return;
}
}

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(' '),
);
};
4 changes: 2 additions & 2 deletions libs/ng-mocks/src/lib/mock-helper/mock-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
87 changes: 87 additions & 0 deletions tests/ng-mocks-change/3341.spec.ts
Original file line number Diff line number Diff line change
@@ -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: ` <custom [formControl]="myControl"></custom> `,
})
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);
});
});
2 changes: 1 addition & 1 deletion tests/ng-mocks-change/reactive-forms.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/,
);
});
});
87 changes: 87 additions & 0 deletions tests/ng-mocks-touch/3341.spec.ts
Original file line number Diff line number Diff line change
@@ -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: ` <custom [formControl]="myControl"></custom> `,
})
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);
});
});
2 changes: 1 addition & 1 deletion tests/ng-mocks-touch/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/,
);
});
});

0 comments on commit 5068407

Please sign in to comment.