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);
+  });
+});