Skip to content

Commit

Permalink
fix(#320): full implementation of ngMocks.touch and ngMocks.change
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Mar 22, 2021
1 parent fdbd30b commit fd81409
Show file tree
Hide file tree
Showing 6 changed files with 398 additions and 26 deletions.
9 changes: 9 additions & 0 deletions libs/ng-mocks/src/lib/common/core.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
let NG_ASYNC_VALIDATORS: any | undefined;
let NG_VALIDATORS: any | undefined;
let NG_VALUE_ACCESSOR: any | undefined;
let AbstractControl: any | undefined;
let FormControl: any | undefined;
let FormControlDirective: any | undefined;
let NgControl: any | undefined;
let NgModel: any | undefined;
try {
// tslint:disable-next-line no-require-imports no-var-requires
const module = require('@angular/forms');
Expand All @@ -13,17 +16,23 @@ try {
NG_ASYNC_VALIDATORS = module.NG_ASYNC_VALIDATORS;
NG_VALIDATORS = module.NG_VALIDATORS;
NG_VALUE_ACCESSOR = module.NG_VALUE_ACCESSOR;
AbstractControl = module.AbstractControl;
FormControl = module.FormControl;
FormControlDirective = module.FormControlDirective;
NgControl = module.NgControl;
NgModel = module.NgModel;
}
} catch (e) {
// nothing to do;
}

export default {
AbstractControl,
FormControl,
FormControlDirective,
NG_ASYNC_VALIDATORS,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
NgControl,
NgModel,
};
16 changes: 13 additions & 3 deletions libs/ng-mocks/src/lib/mock-helper/cva/func.get-vca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ import coreInjector from '../../common/core.injector';
export default (el: DebugNode): Record<keyof any, any> => {
const ngControl = coreForm && coreInjector(coreForm.NgControl, el.injector);
const valueAccessor = ngControl?.valueAccessor;
if (!valueAccessor) {
throw new Error('Cannot find ControlValueAccessor on the element');
if (valueAccessor) {
return valueAccessor;
}

return valueAccessor;
const formControlDirective = coreForm && coreInjector(coreForm.FormControlDirective, el.injector);
if (formControlDirective?.form) {
return formControlDirective.form;
}

const ngModel = coreForm && coreInjector(coreForm.NgModel, el.injector);
if (ngModel) {
return ngModel;
}

throw new Error('Cannot find ControlValueAccessor on the element');
};
58 changes: 47 additions & 11 deletions libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.change.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,69 @@
import { DebugElement } from '@angular/core';

import coreForm from '../../common/core.form';
import { isMockControlValueAccessor } from '../../common/func.is-mock-control-value-accessor';
import mockHelperStubMember from '../mock-helper.stub-member';

import funcGetVca from './func.get-vca';

// default html behavior
const triggerInput = (el: DebugElement, value: any): void => {
el.triggerEventHandler('focus', {});
el.nativeElement.value = value;
const target = el.nativeElement;
el.triggerEventHandler('focus', {
target,
});

const descriptor = Object.getOwnPropertyDescriptor(target, 'value');
mockHelperStubMember(target, 'value', value);
el.triggerEventHandler('input', {
target: {
value,
},
target,
});
if (descriptor) {
Object.defineProperty(target, 'value', descriptor);
target.value = value;
}

el.triggerEventHandler('blur', {
target,
});
el.triggerEventHandler('blur', {});
};

export default (el: DebugElement, value: any): void => {
const valueAccessor = funcGetVca(el);
const handleKnown = (valueAccessor: any, value: any): boolean => {
if (coreForm && valueAccessor instanceof coreForm.AbstractControl) {
valueAccessor.setValue(value);

return true;
}

if (coreForm && valueAccessor instanceof coreForm.NgModel) {
valueAccessor.update.emit(value);

return true;
}

if (isMockControlValueAccessor(valueAccessor.instance)) {
valueAccessor.instance.__simulateChange(value);

return true;
}

return false;
};

const keys = ['onChange', '_onChange', 'changeFn', '_onChangeCallback', 'onModelChange'];

export default (el: DebugElement, value: any): void => {
const valueAccessor = funcGetVca(el);
if (handleKnown(valueAccessor, value)) {
triggerInput(el, value);

return;
}

triggerInput(el, value);
for (const key of ['onChange', '_onChange', 'changeFn', '_onChangeCallback', 'onModelChange']) {
for (const key of keys) {
if (typeof valueAccessor[key] === 'function') {
valueAccessor[key](value);
triggerInput(el, value);
// valueAccessor[key](value);

return;
}
Expand Down
49 changes: 37 additions & 12 deletions libs/ng-mocks/src/lib/mock-helper/cva/mock-helper.touch.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,52 @@
import { DebugNode } from '@angular/core';
import { DebugElement } from '@angular/core';

import coreForm from '../../common/core.form';
import { isMockControlValueAccessor } from '../../common/func.is-mock-control-value-accessor';

import funcGetVca from './func.get-vca';

export default (el: DebugNode): void => {
const valueAccessor = funcGetVca(el);
// default html behavior
const triggerTouch = (el: DebugElement): void => {
const target = el.nativeElement;
el.triggerEventHandler('focus', {
target,
});

el.triggerEventHandler('blur', {
target,
});
};

const handleKnown = (valueAccessor: any): boolean => {
if (coreForm && valueAccessor instanceof coreForm.AbstractControl) {
valueAccessor.markAsTouched();

return true;
}

if (isMockControlValueAccessor(valueAccessor.instance)) {
valueAccessor.instance.__simulateTouch();

return true;
}

return false;
};

const keys = ['onTouched', '_onTouched', '_cvaOnTouch', '_markAsTouched', '_onTouchedCallback', 'onModelTouched'];

export default (el: DebugElement): void => {
const valueAccessor = funcGetVca(el);
if (handleKnown(valueAccessor)) {
triggerTouch(el);

return;
}

for (const key of [
'onTouched',
'_onTouched',
'_cvaOnTouch',
'_markAsTouched',
'_onTouchedCallback',
'onModelTouched',
]) {
for (const key of keys) {
if (typeof valueAccessor[key] === 'function') {
valueAccessor[key]();
triggerTouch(el);
// valueAccessor[key]();

return;
}
Expand Down
168 changes: 168 additions & 0 deletions tests/ng-mocks-change/cdr.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// tslint:disable member-ordering

import { Component, HostListener, NgModule } from '@angular/core';
import {
ControlValueAccessor,
FormControl,
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
selector: 'cva',
template: ` {{ show }} `,
})
class CvaComponent implements ControlValueAccessor {
public onChange: any = () => undefined;
public onTouched: any = () => undefined;
public show: any = null;

@HostListener('input', ['$event'])
public onInput = (event: any) => {
this.show = event.target.value;
this.onChange(this.show);
};

public registerOnChange = (onChange: any) =>
(this.onChange = onChange);
public registerOnTouched = (onTouched: any) =>
(this.onTouched = onTouched);

public writeValue = (value: any) => {
this.show = value;
};
}

@Component({
selector: 'target',
template: `
<cva
[formControl]="control"
class="form-control"
ngDefaultControl
></cva>
<cva [(ngModel)]="value" class="ng-model" ngDefaultControl></cva>
`,
})
class TargetComponent {
public control = new FormControl();
public value: string | null = null;
}

@NgModule({
declarations: [CvaComponent, TargetComponent],
imports: [ReactiveFormsModule, FormsModule],
})
class MyModule {}

// checking how normal form works
describe('ng-mocks-change:cdr', () => {
const dataSet: Array<[string, () => void]> = [
['real', () => MockBuilder(TargetComponent).keep(MyModule)],
[
'mock-vca',
() =>
MockBuilder(TargetComponent)
.keep(MyModule)
.mock(CvaComponent),
],
];
for (const [label, init] of dataSet) {
describe(label, () => {
const destroy$ = new Subject<void>();

beforeEach(init);

afterAll(() => {
destroy$.next();
destroy$.complete();
});

it('correctly changes CVA', () => {
const fixture = MockRender(TargetComponent);
const component = fixture.point.componentInstance;
const spy = jasmine.createSpy('valueChange');
component.control.valueChanges
.pipe(takeUntil(destroy$))
.subscribe(spy);

const formControl = ngMocks.find('.form-control');
expect(ngMocks.formatHtml(formControl)).toEqual('');
expect(ngMocks.formatHtml(formControl, true)).toContain(
'class="form-control ng-untouched ng-pristine ng-valid"',
);
expect(spy).toHaveBeenCalledTimes(0);
ngMocks.change(formControl, '123');
expect(spy).toHaveBeenCalledTimes(1);
expect(component.control.value).toEqual('123');
expect(ngMocks.formatHtml(formControl)).toEqual('');
expect(ngMocks.formatHtml(formControl, true)).toContain(
'class="form-control ng-untouched ng-pristine ng-valid"',
);

// nothing should be rendered so far, but now we trigger the render
fixture.detectChanges();
if (label === 'real') {
expect(ngMocks.formatHtml(formControl)).toEqual('123');
}
expect(ngMocks.formatHtml(formControl, true)).toContain(
'class="form-control ng-valid ng-touched ng-dirty"',
);

const ngModel = ngMocks.find('.ng-model');
expect(ngMocks.formatHtml(ngModel)).toEqual('');
expect(ngMocks.formatHtml(ngModel, true)).toContain(
'class="ng-model ng-untouched ng-pristine ng-valid"',
);
ngMocks.change(ngModel, '123');
expect(component.value).toEqual('123');
expect(ngMocks.formatHtml(ngModel)).toEqual('');
expect(ngMocks.formatHtml(ngModel, true)).toContain(
'class="ng-model ng-untouched ng-pristine ng-valid"',
);

// nothing should be rendered so far, but now we trigger the render
fixture.detectChanges();
if (label === 'real') {
expect(ngMocks.formatHtml(ngModel)).toEqual('123');
}
expect(ngMocks.formatHtml(ngModel, true)).toContain(
'class="ng-model ng-valid ng-touched ng-dirty"',
);
});
});
}
});

describe('ng-mocks-change:cdr:full-mock', () => {
const destroy$ = new Subject<void>();

beforeEach(() => MockBuilder(TargetComponent, MyModule));

afterAll(() => {
destroy$.next();
destroy$.complete();
});

it('correctly changes CVA', () => {
const fixture = MockRender(TargetComponent);
const component = fixture.point.componentInstance;
const spy = jasmine.createSpy('valueChange');
component.control.valueChanges
.pipe(takeUntil(destroy$))
.subscribe(spy);

const formControl = ngMocks.find('.form-control');
expect(spy).toHaveBeenCalledTimes(0);
ngMocks.change(formControl, '123');
expect(spy).toHaveBeenCalledTimes(1);
expect(component.control.value).toEqual('123');

const ngModel = ngMocks.find('.ng-model');
ngMocks.change(ngModel, '123');
expect(component.value).toEqual('123');
});
});

0 comments on commit fd81409

Please sign in to comment.