Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ngMocks): supports custom method names for change and touch #3341 #3383

Merged
merged 1 commit into from
Aug 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/,
);
});
});