From fa418d0440876a9eab55564e75e3807b9a60687c Mon Sep 17 00:00:00 2001 From: MG <m@sudo.eu> Date: Tue, 23 Mar 2021 19:27:19 +0100 Subject: [PATCH] fix(#324): smarter touches and changes --- .../lib/mock-helper/cva/mock-helper.change.ts | 9 +- .../lib/mock-helper/cva/mock-helper.touch.ts | 8 +- tests/ng-mocks-change/cdr-change.spec.ts | 166 ++++++++++++++++++ .../{cdr.spec.ts => cdr-input.spec.ts} | 4 +- .../{cdr.spec.ts => cdr-blur.spec.ts} | 4 +- tests/ng-mocks-touch/cdr-touch.spec.ts | 122 +++++++++++++ 6 files changed, 303 insertions(+), 10 deletions(-) create mode 100644 tests/ng-mocks-change/cdr-change.spec.ts rename tests/ng-mocks-change/{cdr.spec.ts => cdr-input.spec.ts} (97%) rename tests/ng-mocks-touch/{cdr.spec.ts => cdr-blur.spec.ts} (97%) create mode 100644 tests/ng-mocks-touch/cdr-touch.spec.ts 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 d87150efba..6b2b3a4e6b 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 @@ -50,11 +50,14 @@ const handleKnown = (valueAccessor: any, value: any): boolean => { return false; }; +const hasListener = (el: DebugElement): boolean => + el.listeners.filter(listener => listener.name === 'input').length > 0; + const keys = ['onChange', '_onChange', 'changeFn', '_onChangeCallback', 'onModelChange']; export default (el: DebugElement, value: any): void => { const valueAccessor = funcGetVca(el); - if (handleKnown(valueAccessor, value)) { + if (handleKnown(valueAccessor, value) || hasListener(el)) { triggerInput(el, value); return; @@ -62,8 +65,8 @@ export default (el: DebugElement, value: any): void => { for (const key of keys) { if (typeof valueAccessor[key] === 'function') { - triggerInput(el, value); - // valueAccessor[key](value); + valueAccessor.writeValue(value); + valueAccessor[key](value); return; } 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 6e4be7401d..9fdb39f58d 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 @@ -33,11 +33,14 @@ const handleKnown = (valueAccessor: any): boolean => { return false; }; +const hasListener = (el: DebugElement): boolean => + el.listeners.filter(listener => listener.name === 'focus' || listener.name === 'blur').length > 0; + const keys = ['onTouched', '_onTouched', '_cvaOnTouch', '_markAsTouched', '_onTouchedCallback', 'onModelTouched']; export default (el: DebugElement): void => { const valueAccessor = funcGetVca(el); - if (handleKnown(valueAccessor)) { + if (handleKnown(valueAccessor) || hasListener(el)) { triggerTouch(el); return; @@ -45,8 +48,7 @@ export default (el: DebugElement): void => { for (const key of keys) { if (typeof valueAccessor[key] === 'function') { - triggerTouch(el); - // valueAccessor[key](); + valueAccessor[key](); return; } diff --git a/tests/ng-mocks-change/cdr-change.spec.ts b/tests/ng-mocks-change/cdr-change.spec.ts new file mode 100644 index 0000000000..e07b1f6d3f --- /dev/null +++ b/tests/ng-mocks-change/cdr-change.spec.ts @@ -0,0 +1,166 @@ +// tslint:disable member-ordering + +import { Component, NgModule } from '@angular/core'; +import { + ControlValueAccessor, + FormControl, + FormsModule, + NG_VALUE_ACCESSOR, + ReactiveFormsModule, +} from '@angular/forms'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + providers: [ + { + multi: true, + provide: NG_VALUE_ACCESSOR, + useExisting: CvaComponent, + }, + ], + selector: 'cva', + template: ` {{ show }} `, +}) +class CvaComponent implements ControlValueAccessor { + public onChange: any = () => undefined; + public onTouched: any = () => undefined; + public show: any = null; + + 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"></cva> + <cva [(ngModel)]="value" class="ng-model"></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-change', () => { + 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-untouched ng-valid 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-untouched ng-valid ng-dirty"', + ); + }); + }); + } +}); + +describe('ng-mocks-change:cdr-change: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'); + }); +}); diff --git a/tests/ng-mocks-change/cdr.spec.ts b/tests/ng-mocks-change/cdr-input.spec.ts similarity index 97% rename from tests/ng-mocks-change/cdr.spec.ts rename to tests/ng-mocks-change/cdr-input.spec.ts index 226cf9d1cc..3db47dac97 100644 --- a/tests/ng-mocks-change/cdr.spec.ts +++ b/tests/ng-mocks-change/cdr-input.spec.ts @@ -59,7 +59,7 @@ class TargetComponent { class MyModule {} // checking how normal form works -describe('ng-mocks-change:cdr', () => { +describe('ng-mocks-change:cdr-input', () => { const dataSet: Array<[string, () => void]> = [ ['real', () => MockBuilder(TargetComponent).keep(MyModule)], [ @@ -137,7 +137,7 @@ describe('ng-mocks-change:cdr', () => { } }); -describe('ng-mocks-change:cdr:full-mock', () => { +describe('ng-mocks-change:cdr-change:full-mock', () => { const destroy$ = new Subject<void>(); beforeEach(() => MockBuilder(TargetComponent, MyModule)); diff --git a/tests/ng-mocks-touch/cdr.spec.ts b/tests/ng-mocks-touch/cdr-blur.spec.ts similarity index 97% rename from tests/ng-mocks-touch/cdr.spec.ts rename to tests/ng-mocks-touch/cdr-blur.spec.ts index 077247a806..8ed0ee55bf 100644 --- a/tests/ng-mocks-touch/cdr.spec.ts +++ b/tests/ng-mocks-touch/cdr-blur.spec.ts @@ -57,7 +57,7 @@ class TargetComponent { class MyModule {} // checking how normal form works -describe('ng-mocks-touch:cdr', () => { +describe('ng-mocks-touch:cdr-blur', () => { const dataSet: Array<[string, () => void]> = [ ['real', () => MockBuilder(TargetComponent).keep(MyModule)], [ @@ -110,7 +110,7 @@ describe('ng-mocks-touch:cdr', () => { } }); -describe('ng-mocks-touch:cdr:full-mock', () => { +describe('ng-mocks-touch:cdr-blur:full-mock', () => { beforeEach(() => MockBuilder(TargetComponent, MyModule)); it('correctly touches CVA', () => { diff --git a/tests/ng-mocks-touch/cdr-touch.spec.ts b/tests/ng-mocks-touch/cdr-touch.spec.ts new file mode 100644 index 0000000000..b10677b570 --- /dev/null +++ b/tests/ng-mocks-touch/cdr-touch.spec.ts @@ -0,0 +1,122 @@ +// tslint:disable member-ordering + +import { Component, NgModule } from '@angular/core'; +import { + ControlValueAccessor, + FormControl, + FormsModule, + NG_VALUE_ACCESSOR, + ReactiveFormsModule, +} from '@angular/forms'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +@Component({ + providers: [ + { + multi: true, + provide: NG_VALUE_ACCESSOR, + useExisting: CvaComponent, + }, + ], + selector: 'cva', + template: ` {{ show }} `, +}) +class CvaComponent implements ControlValueAccessor { + public onChange: any = () => undefined; + public onTouched: any = () => undefined; + public show: any = null; + + 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"></cva> + <cva [(ngModel)]="value" class="ng-model"></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-touch:cdr-blur', () => { + 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, () => { + beforeEach(init); + + it('correctly touches CVA', () => { + const fixture = MockRender(TargetComponent); + + const formControl = ngMocks.find('.form-control'); + expect(ngMocks.formatHtml(formControl, true)).toContain( + 'class="form-control ng-untouched ng-pristine ng-valid"', + ); + ngMocks.touch(formControl); + 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(); + expect(ngMocks.formatHtml(formControl, true)).toContain( + 'class="form-control ng-pristine ng-valid ng-touched"', + ); + + const ngModel = ngMocks.find('.ng-model'); + expect(ngMocks.formatHtml(ngModel, true)).toContain( + 'class="ng-model ng-untouched ng-pristine ng-valid"', + ); + ngMocks.touch(ngModel); + 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(); + expect(ngMocks.formatHtml(ngModel, true)).toContain( + 'class="ng-model ng-pristine ng-valid ng-touched"', + ); + }); + }); + } +}); + +describe('ng-mocks-touch:cdr-blur:full-mock', () => { + beforeEach(() => MockBuilder(TargetComponent, MyModule)); + + it('correctly touches CVA', () => { + const fixture = MockRender(TargetComponent); + const component = fixture.point.componentInstance; + + const formControl = ngMocks.find('.form-control'); + ngMocks.touch(formControl); + expect(component.control.touched).toEqual(true); + }); +});