From f85f497f0dab32363e67980a9f1dc37b0b321208 Mon Sep 17 00:00:00 2001 From: MG Date: Fri, 26 Feb 2021 19:09:39 +0100 Subject: [PATCH] fix(#305): better injection of NgControl closes #305 --- libs/ng-mocks/src/lib/common/core.form.ts | 29 ++++++ ...unc.is-mock-control-value-accessor.spec.ts | 16 +--- .../lib/common/func.is-mock-validator.spec.ts | 14 +-- libs/ng-mocks/src/lib/common/mock.ts | 75 ++++++---------- .../src/lib/mock-component/mock-component.ts | 17 +++- .../src/lib/mock-directive/mock-directive.ts | 22 +++-- .../lib/mock-helper/crawl/el-def-get-node.ts | 3 +- .../mock-helper/crawl/el-def-get-parent.ts | 1 - libs/ng-mocks/src/lib/mock/clone-providers.ts | 35 ++------ tests-angular/e2e/src/issue-305/test.spec.ts | 61 +++++++++++++ tests/issue-305/forms.spec.ts | 73 +++++++++++++++ tests/issue-305/overrides.spec.ts | 63 +++++++++++++ tests/issue-305/reactive-forms.spec.ts | 89 +++++++++++++++++++ 13 files changed, 382 insertions(+), 116 deletions(-) create mode 100644 libs/ng-mocks/src/lib/common/core.form.ts create mode 100644 tests-angular/e2e/src/issue-305/test.spec.ts create mode 100644 tests/issue-305/forms.spec.ts create mode 100644 tests/issue-305/overrides.spec.ts create mode 100644 tests/issue-305/reactive-forms.spec.ts diff --git a/libs/ng-mocks/src/lib/common/core.form.ts b/libs/ng-mocks/src/lib/common/core.form.ts new file mode 100644 index 000000000..708d330f6 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/core.form.ts @@ -0,0 +1,29 @@ +// tslint:disable variable-name + +let NG_ASYNC_VALIDATORS: any | undefined; +let NG_VALIDATORS: any | undefined; +let NG_VALUE_ACCESSOR: any | undefined; +let FormControlDirective: any | undefined; +let NgControl: any | undefined; +try { + // tslint:disable-next-line no-require-imports no-var-requires + const module = require('@angular/forms'); + // istanbul ignore else + if (module) { + NG_ASYNC_VALIDATORS = module.NG_ASYNC_VALIDATORS; + NG_VALIDATORS = module.NG_VALIDATORS; + NG_VALUE_ACCESSOR = module.NG_VALUE_ACCESSOR; + FormControlDirective = module.FormControlDirective; + NgControl = module.NgControl; + } +} catch (e) { + // nothing to do; +} + +export default { + FormControlDirective, + NG_ASYNC_VALIDATORS, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + NgControl, +}; diff --git a/libs/ng-mocks/src/lib/common/func.is-mock-control-value-accessor.spec.ts b/libs/ng-mocks/src/lib/common/func.is-mock-control-value-accessor.spec.ts index fc71917b8..b8e9345c5 100644 --- a/libs/ng-mocks/src/lib/common/func.is-mock-control-value-accessor.spec.ts +++ b/libs/ng-mocks/src/lib/common/func.is-mock-control-value-accessor.spec.ts @@ -1,9 +1,7 @@ import { Component, Directive, Injector } from '@angular/core'; -import { NgControl } from '@angular/forms'; import { MockComponent } from '../mock-component/mock-component'; import { MockDirective } from '../mock-directive/mock-directive'; -import { ngMocks } from '../mock-helper/mock-helper'; import { MockService } from '../mock-service/mock-service'; import { isMockControlValueAccessor } from './func.is-mock-control-value-accessor'; @@ -40,12 +38,7 @@ describe('isMockControlValueAccessor', () => { const ngControl = {}; const injector = MockService(Injector); - ngMocks.stub(injector, 'get'); - spyOn(injector, 'get') - .withArgs(NgControl, undefined, 0b1010) - .and.returnValue(ngControl); - - const instanceInjected = new mockClass(null, injector); + const instanceInjected = new mockClass(null, injector, ngControl); expect(isMockControlValueAccessor(instanceInjected)).toEqual( true, ); @@ -63,12 +56,7 @@ describe('isMockControlValueAccessor', () => { const ngControl = {}; const injector = MockService(Injector); - ngMocks.stub(injector, 'get'); - spyOn(injector, 'get') - .withArgs(NgControl, undefined, 0b1010) - .and.returnValue(ngControl); - - const instanceInjected = new mockClass(injector); + const instanceInjected = new mockClass(injector, ngControl); expect(isMockControlValueAccessor(instanceInjected)).toEqual( true, ); diff --git a/libs/ng-mocks/src/lib/common/func.is-mock-validator.spec.ts b/libs/ng-mocks/src/lib/common/func.is-mock-validator.spec.ts index e47b1bce4..d98e49eba 100644 --- a/libs/ng-mocks/src/lib/common/func.is-mock-validator.spec.ts +++ b/libs/ng-mocks/src/lib/common/func.is-mock-validator.spec.ts @@ -7,7 +7,6 @@ import { import { AbstractControl, AsyncValidator, - NgControl, NG_ASYNC_VALIDATORS, NG_VALIDATORS, ValidationErrors, @@ -72,12 +71,7 @@ describe('isMockValidator', () => { valueAccessor: {}, }; const injector = MockService(Injector); - ngMocks.stub(injector, 'get'); - spyOn(injector, 'get') - .withArgs(NgControl, undefined, 0b1010) - .and.returnValue(ngControl); - - const instanceInjected = new mockClass(null, injector); + const instanceInjected = new mockClass(injector, ngControl); expect(isMockValidator(instanceInjected)).toEqual(true); }); @@ -95,11 +89,7 @@ describe('isMockValidator', () => { }; const injector = MockService(Injector); ngMocks.stub(injector, 'get'); - spyOn(injector, 'get') - .withArgs(NgControl, undefined, 0b1010) - .and.returnValue(ngControl); - - const instanceInjected = new mockClass(injector); + const instanceInjected = new mockClass(injector, ngControl); expect(isMockValidator(instanceInjected)).toEqual(true); }); }); diff --git a/libs/ng-mocks/src/lib/common/mock.ts b/libs/ng-mocks/src/lib/common/mock.ts index c0c213c4d..243a43d79 100644 --- a/libs/ng-mocks/src/lib/common/mock.ts +++ b/libs/ng-mocks/src/lib/common/mock.ts @@ -1,6 +1,6 @@ // tslint:disable variable-name -import { EventEmitter, Injector, Optional } from '@angular/core'; +import { EventEmitter, Injector, Optional, Self } from '@angular/core'; import { IMockBuilderConfig } from '../mock-builder/types'; import mockHelperStub from '../mock-helper/mock-helper.stub'; @@ -8,32 +8,18 @@ import mockInstanceApply from '../mock-instance/mock-instance-apply'; import helperMockService from '../mock-service/helper.mock-service'; import coreDefineProperty from './core.define-property'; +import coreForm from './core.form'; import { mapValues } from './core.helpers'; import { AnyType } from './core.types'; import funcIsMock from './func.is-mock'; import { MockControlValueAccessorProxy } from './mock-control-value-accessor-proxy'; import ngMocksUniverse from './ng-mocks-universe'; -let FormControlDirective: any | undefined; -let NgControl: any | undefined; -try { - // tslint:disable-next-line no-require-imports no-var-requires - const module = require('@angular/forms'); - // istanbul ignore else - if (module) { - FormControlDirective = module.FormControlDirective; - NgControl = module.NgControl; - } -} catch (e) { - // nothing to do; -} - -const setValueAccessor = (instance: MockConfig, injector?: Injector) => { - if (injector && instance.__ngMocksConfig && instance.__ngMocksConfig.setControlValueAccessor) { +const setValueAccessor = (instance: any, ngControl?: any) => { + if (ngControl && instance.__ngMocksConfig && instance.__ngMocksConfig.setControlValueAccessor) { try { - const ngControl = (injector.get as any)(/* A5 */ NgControl, undefined, 0b1010); if (ngControl && !ngControl.valueAccessor) { - ngControl.valueAccessor = new MockControlValueAccessorProxy(instance.constructor); + ngControl.valueAccessor = new MockControlValueAccessorProxy(instance.__ngMocksCtor); } } catch (e) { // nothing to do. @@ -41,18 +27,9 @@ const setValueAccessor = (instance: MockConfig, injector?: Injector) => { } }; -// any because of optional @angular/forms -const getRelatedNgControl = (injector: Injector): any => { - try { - return (injector.get as any)(/* A5 */ NgControl, undefined, 0b1010); - } catch (e) { - return (injector.get as any)(/* A5 */ FormControlDirective, undefined, 0b1010); - } -}; - // connecting to NG_VALUE_ACCESSOR const installValueAccessor = (ngControl: any, instance: any) => { - if (!ngControl.valueAccessor.instance && ngControl.valueAccessor.target === instance.constructor) { + if (!ngControl.valueAccessor.instance && ngControl.valueAccessor.target === instance.__ngMocksCtor) { ngControl.valueAccessor.instance = instance; helperMockService.mock(instance, 'registerOnChange'); helperMockService.mock(instance, 'registerOnTouched'); @@ -66,7 +43,7 @@ const installValueAccessor = (ngControl: any, instance: any) => { // connecting to NG_ASYNC_VALIDATORS const installValidator = (validators: any[], instance: any) => { for (const validator of validators) { - if (!validator.instance && validator.target === instance.constructor) { + if (!validator.instance && validator.target === instance.__ngMocksCtor) { validator.instance = instance; helperMockService.mock(instance, 'registerOnValidatorChange'); helperMockService.mock(instance, 'validate'); @@ -75,21 +52,18 @@ const installValidator = (validators: any[], instance: any) => { } }; -const applyNgValueAccessor = (instance: any, injector?: Injector) => { - setValueAccessor(instance, injector); +const applyNgValueAccessor = (instance: any, ngControl: any) => { + setValueAccessor(instance, ngControl); - if (injector) { - try { - const ngControl: any = getRelatedNgControl(injector); - // istanbul ignore else - if (ngControl) { - installValueAccessor(ngControl, instance); - installValidator(ngControl._rawValidators, instance); - installValidator(ngControl._rawAsyncValidators, instance); - } - } catch (e) { - // nothing to do. + try { + // istanbul ignore else + if (ngControl) { + installValueAccessor(ngControl, instance); + installValidator(ngControl._rawValidators, instance); + installValidator(ngControl._rawAsyncValidators, instance); } + } catch (e) { + // nothing to do. } }; @@ -175,16 +149,20 @@ export interface MockConfig { export class Mock { protected __ngMocksConfig!: ngMocksMockConfig; - public constructor(injector?: Injector) { + public constructor( + injector: Injector | null = null, + ngControl: any | null = null, // NgControl + ) { const mockOf = (this.constructor as any).mockOf; coreDefineProperty(this, '__ngMocksInjector', injector); + coreDefineProperty(this, '__ngMocksCtor', this.constructor); for (const key of this.__ngMocksConfig.queryScanKeys || /* istanbul ignore next */ []) { coreDefineProperty(this, `__ngMocksVcr_${key}`, undefined); } // istanbul ignore else if (funcIsMock(this)) { - applyNgValueAccessor(this, injector); + applyNgValueAccessor(this, ngControl); applyOutputs(this); applyPrototype(this, Object.getPrototypeOf(this)); applyMethods(this, mockOf.prototype); @@ -194,8 +172,11 @@ export class Mock { // and faking prototype Object.setPrototypeOf(this, mockOf.prototype); - applyOverrides(this, mockOf, injector); + applyOverrides(this, mockOf, injector ?? undefined); } } -coreDefineProperty(Mock, 'parameters', [[Injector, new Optional()]]); +coreDefineProperty(Mock, 'parameters', [ + [Injector, new Optional()], + [coreForm.NgControl || /* istanbul ignore next */ (() => undefined), new Optional(), new Self()], +]); diff --git a/libs/ng-mocks/src/lib/mock-component/mock-component.ts b/libs/ng-mocks/src/lib/mock-component/mock-component.ts index ecd8065f5..f111f3194 100644 --- a/libs/ng-mocks/src/lib/mock-component/mock-component.ts +++ b/libs/ng-mocks/src/lib/mock-component/mock-component.ts @@ -4,13 +4,16 @@ import { Component, EmbeddedViewRef, Injector, + Optional, QueryList, + Self, TemplateRef, ViewContainerRef, } from '@angular/core'; import { getTestBed } from '@angular/core/testing'; import coreDefineProperty from '../common/core.define-property'; +import coreForm from '../common/core.form'; import { extendClass } from '../common/core.helpers'; import coreReflectDirectiveResolve from '../common/core.reflect.directive-resolve'; import { Type } from '../common/core.types'; @@ -160,8 +163,12 @@ const mixHide = (instance: MockConfig & Record, changeDetector: class ComponentMockBase extends LegacyControlValueAccessor implements AfterContentInit { // istanbul ignore next - public constructor(changeDetector: ChangeDetectorRef, injector: Injector) { - super(injector); + public constructor( + injector: Injector, + ngControl: any, // NgControl + changeDetector: ChangeDetectorRef, + ) { + super(injector, ngControl); if (funcIsMock(this)) { mixRender(this, changeDetector); mixHide(this, changeDetector); @@ -186,7 +193,11 @@ class ComponentMockBase extends LegacyControlValueAccessor implements AfterConte } } -coreDefineProperty(ComponentMockBase, 'parameters', [[ChangeDetectorRef], [Injector]]); +coreDefineProperty(ComponentMockBase, 'parameters', [ + [Injector], + [coreForm.NgControl || /* istanbul ignore next */ (() => undefined), new Optional(), new Self()], + [ChangeDetectorRef], +]); const decorateClass = (component: Type, mock: Type): void => { const meta = coreReflectDirectiveResolve(component); diff --git a/libs/ng-mocks/src/lib/mock-directive/mock-directive.ts b/libs/ng-mocks/src/lib/mock-directive/mock-directive.ts index ddf87bf59..3537679b7 100644 --- a/libs/ng-mocks/src/lib/mock-directive/mock-directive.ts +++ b/libs/ng-mocks/src/lib/mock-directive/mock-directive.ts @@ -5,12 +5,14 @@ import { Injector, OnInit, Optional, + Self, TemplateRef, ViewContainerRef, } from '@angular/core'; import { getTestBed } from '@angular/core/testing'; import coreDefineProperty from '../common/core.define-property'; +import coreForm from '../common/core.form'; import { extendClass } from '../common/core.helpers'; import coreReflectDirectiveResolve from '../common/core.reflect.directive-resolve'; import { Type } from '../common/core.types'; @@ -25,12 +27,13 @@ class DirectiveMockBase extends LegacyControlValueAccessor implements OnInit { // istanbul ignore next public constructor( injector: Injector, - vcr: ViewContainerRef, + ngControl: any, // NgControl cdr: ChangeDetectorRef, - element?: ElementRef, - template?: TemplateRef, + vcr: ViewContainerRef, + element: ElementRef | null = null, + template: TemplateRef | null = null, ) { - super(injector); + super(injector, ngControl); this.__ngMocksInstall(vcr, cdr, element, template); } @@ -51,8 +54,8 @@ class DirectiveMockBase extends LegacyControlValueAccessor implements OnInit { private __ngMocksInstall( vcr: ViewContainerRef, cdr: ChangeDetectorRef, - element?: ElementRef, - template?: TemplateRef, + element: ElementRef | null, + template: TemplateRef | null, ): void { // Basically any directive on ng-template is treated as structural, even it does not control render process. // In our case we do not if we should render it or not and due to this we do nothing. @@ -76,10 +79,11 @@ class DirectiveMockBase extends LegacyControlValueAccessor implements OnInit { coreDefineProperty(DirectiveMockBase, 'parameters', [ [Injector], - [ViewContainerRef], + [coreForm.NgControl || /* istanbul ignore next */ (() => undefined), new Optional(), new Self()], [ChangeDetectorRef], - [ElementRef, new Optional()], - [TemplateRef, new Optional()], + [ViewContainerRef], + [ElementRef, new Optional(), new Self()], + [TemplateRef, new Optional(), new Self()], ]); const decorateClass = (directive: Type, mock: Type): void => { diff --git a/libs/ng-mocks/src/lib/mock-helper/crawl/el-def-get-node.ts b/libs/ng-mocks/src/lib/mock-helper/crawl/el-def-get-node.ts index 3a44734fb..9dbdc92f1 100644 --- a/libs/ng-mocks/src/lib/mock-helper/crawl/el-def-get-node.ts +++ b/libs/ng-mocks/src/lib/mock-helper/crawl/el-def-get-node.ts @@ -3,8 +3,7 @@ import detectTextNode from './detect-text-node'; export default (node: any) => { return detectTextNode(node) ? undefined - : (undefined as any) || - node.injector._tNode || // ivy + : node.injector._tNode || // ivy node.injector.elDef || // classic undefined; }; diff --git a/libs/ng-mocks/src/lib/mock-helper/crawl/el-def-get-parent.ts b/libs/ng-mocks/src/lib/mock-helper/crawl/el-def-get-parent.ts index bb4871756..45664ab3c 100644 --- a/libs/ng-mocks/src/lib/mock-helper/crawl/el-def-get-parent.ts +++ b/libs/ng-mocks/src/lib/mock-helper/crawl/el-def-get-parent.ts @@ -50,7 +50,6 @@ const scanViewRef = (node: DebugElement) => { export default (node: any) => { return ( - (undefined as any) || node.injector._tNode?.parent || // ivy node.injector.elDef?.parent || // classic scanViewRef(node) || diff --git a/libs/ng-mocks/src/lib/mock/clone-providers.ts b/libs/ng-mocks/src/lib/mock/clone-providers.ts index ad83183fc..173c4d802 100644 --- a/libs/ng-mocks/src/lib/mock/clone-providers.ts +++ b/libs/ng-mocks/src/lib/mock/clone-providers.ts @@ -1,5 +1,6 @@ -import { InjectionToken, Provider } from '@angular/core'; +import { Provider } from '@angular/core'; +import coreForm from '../common/core.form'; import { flatten } from '../common/core.helpers'; import { AnyType } from '../common/core.types'; import funcGetProvider from '../common/func.get-provider'; @@ -13,37 +14,15 @@ import helperMockService from '../mock-service/helper.mock-service'; import toExistingProvider from './to-existing-provider'; import toFactoryProvider from './to-factory-provider'; -// tslint:disable variable-name -let NG_ASYNC_VALIDATORS: InjectionToken | undefined; -let NG_VALIDATORS: InjectionToken | undefined; -let NG_VALUE_ACCESSOR: InjectionToken | undefined; -let FormControlDirective: any | undefined; -let NgControl: any | undefined; -// tslint:enable variable-name -try { - // tslint:disable-next-line no-require-imports no-var-requires - const module = require('@angular/forms'); - // istanbul ignore else - if (module) { - NG_ASYNC_VALIDATORS = module.NG_ASYNC_VALIDATORS; - NG_VALIDATORS = module.NG_VALIDATORS; - NG_VALUE_ACCESSOR = module.NG_VALUE_ACCESSOR; - FormControlDirective = module.FormControlDirective; - NgControl = module.NgControl; - } -} catch (e) { - // nothing to do; -} - const processTokens = (mockType: AnyType, provider: any) => { const provide = funcGetProvider(provider); - if (NG_VALIDATORS && provide === NG_VALIDATORS) { + if (coreForm.NG_VALIDATORS && provide === coreForm.NG_VALIDATORS) { return toFactoryProvider(provide, () => new MockValidatorProxy(mockType)); } - if (NG_ASYNC_VALIDATORS && provide === NG_ASYNC_VALIDATORS) { + if (coreForm.NG_ASYNC_VALIDATORS && provide === coreForm.NG_ASYNC_VALIDATORS) { return toFactoryProvider(provide, () => new MockAsyncValidatorProxy(mockType)); } - if (NG_VALUE_ACCESSOR && provide === NG_VALUE_ACCESSOR) { + if (coreForm.NG_VALUE_ACCESSOR && provide === coreForm.NG_VALUE_ACCESSOR) { return toFactoryProvider(provide, () => new MockControlValueAccessorProxy(mockType)); } @@ -54,7 +33,7 @@ const processOwnUseExisting = (sourceType: AnyType, mockType: AnyType, const provide = funcGetProvider(provider); // Check tests/issue-302/test.spec.ts - if (provide === NgControl || provide === FormControlDirective) { + if (provide === coreForm.NgControl || provide === coreForm.FormControlDirective) { return undefined; } @@ -106,7 +85,7 @@ export default ( for (const provider of flatten(providers || /* istanbul ignore next */ [])) { const provide = funcGetProvider(provider); - if (provide === NG_VALUE_ACCESSOR) { + if (provide === coreForm.NG_VALUE_ACCESSOR) { setControlValueAccessor = false; } const mock = processProvider(sourceType, mockType, provider, resolutions); diff --git a/tests-angular/e2e/src/issue-305/test.spec.ts b/tests-angular/e2e/src/issue-305/test.spec.ts new file mode 100644 index 000000000..8db1c03a3 --- /dev/null +++ b/tests-angular/e2e/src/issue-305/test.spec.ts @@ -0,0 +1,61 @@ +import { Component, NgModule } from '@angular/core'; +import { + DefaultValueAccessor, + FormControl, + ReactiveFormsModule, +} from '@angular/forms'; +import { MatInput, MatInputModule } from '@angular/material/input'; +import { + isMockControlValueAccessor, + MockBuilder, + MockRender, + ngMocks, +} from 'ng-mocks'; + +@Component({ + selector: 'my', + template: ` + + `, +}) +class MyComponent { + public readonly myControl = new FormControl(); +} + +@NgModule({ + declarations: [MyComponent], + exports: [MyComponent], + imports: [ReactiveFormsModule, MatInputModule], +}) +class MyModule {} + +describe('issue-305', () => { + beforeEach(() => + MockBuilder(MyComponent, MyModule) + .keep(ReactiveFormsModule) + .mock(DefaultValueAccessor), + ); + + it('correctly mocks matInput', () => { + MockRender(MyComponent); + + // MatInput does not implement ControlValueAccessor + const matInput = ngMocks.get( + ngMocks.find(['data-testid', 'inputControl']), + MatInput, + ); + expect(isMockControlValueAccessor(matInput)).toEqual(false); + + // DefaultValueAccessor does implement ControlValueAccessor + const valueAccessor = ngMocks.get( + ngMocks.find(['data-testid', 'inputControl']), + DefaultValueAccessor, + ); + expect(isMockControlValueAccessor(valueAccessor)).toEqual(true); + }); +}); diff --git a/tests/issue-305/forms.spec.ts b/tests/issue-305/forms.spec.ts new file mode 100644 index 000000000..43fe94554 --- /dev/null +++ b/tests/issue-305/forms.spec.ts @@ -0,0 +1,73 @@ +import { Component, NgModule } from '@angular/core'; +import { DefaultValueAccessor, FormsModule } from '@angular/forms'; +import { + isMockControlValueAccessor, + MockBuilder, + MockRender, + ngMocks, +} from 'ng-mocks'; + +@Component({ + selector: 'my', + template: ` + + `, +}) +class MyComponent { + public value: number | null = null; +} + +@NgModule({ + declarations: [MyComponent], + exports: [MyComponent], + imports: [FormsModule], +}) +class MyModule {} + +// checking how normal form works +describe('issue-305:forms:real', () => { + beforeEach(() => MockBuilder(MyComponent).keep(MyModule)); + + it('correctly mocks CVA', () => { + const component = MockRender(MyComponent).point.componentInstance; + + // DefaultValueAccessor does implement ControlValueAccessor + const valueAccessor = ngMocks.get( + ngMocks.find(['data-testid', 'inputControl']), + DefaultValueAccessor, + ); + + // normal change + expect(component.value).toEqual(null); + valueAccessor.onChange(123); + expect(component.value).toEqual(123); + }); +}); + +// a mock version should behavior similarly but via our own interface +describe('issue-305:forms:mock', () => { + beforeEach(() => + MockBuilder(MyComponent) + .keep(MyModule) + .mock(DefaultValueAccessor), + ); + + it('correctly mocks CVA', () => { + const fixture = MockRender(MyComponent); + + const component = fixture.point.componentInstance; + + // DefaultValueAccessor does implement ControlValueAccessor + const valueAccessor = ngMocks.get( + ngMocks.find(['data-testid', 'inputControl']), + DefaultValueAccessor, + ); + + // normal change + expect(component.value).toEqual(null); + if (isMockControlValueAccessor(valueAccessor)) { + valueAccessor.__simulateChange(123); + } + expect(component.value).toEqual(123); + }); +}); diff --git a/tests/issue-305/overrides.spec.ts b/tests/issue-305/overrides.spec.ts new file mode 100644 index 000000000..85baae76a --- /dev/null +++ b/tests/issue-305/overrides.spec.ts @@ -0,0 +1,63 @@ +import { Component, NgModule } from '@angular/core'; +import { + DefaultValueAccessor, + FormControl, + ReactiveFormsModule, +} from '@angular/forms'; +import { MockBuilder, MockInstance, MockRender } from 'ng-mocks'; + +@Component({ + selector: 'my', + template: ` + + `, +}) +class MyComponent { + public readonly myControl = new FormControl(); +} + +@NgModule({ + declarations: [MyComponent], + exports: [MyComponent], + imports: [ReactiveFormsModule], +}) +class MyModule {} + +describe('issue-305:overrides', () => { + beforeEach(() => + MockBuilder(MyComponent) + .keep(MyModule) + .mock(DefaultValueAccessor), + ); + + it('correctly overrides CVA', () => { + const registerOnChange = jasmine.createSpy('registerOnChange'); + const registerOnTouched = jasmine.createSpy('registerOnTouched'); + const setDisabledState = jasmine.createSpy('setDisabledState'); + const writeValue = jasmine.createSpy('writeValue'); + + MockInstance(DefaultValueAccessor, () => ({ + registerOnChange, + registerOnTouched, + setDisabledState, + writeValue, + })); + + const fixture = MockRender(MyComponent); + + expect(registerOnChange).toHaveBeenCalled(); + expect(registerOnTouched).toHaveBeenCalled(); + expect(writeValue).toHaveBeenCalledWith(null); + + const component = fixture.point.componentInstance; + expect(writeValue).not.toHaveBeenCalledWith(123); + component.myControl.setValue(123); + fixture.detectChanges(); + expect(writeValue).toHaveBeenCalledWith(123); + + expect(setDisabledState).not.toHaveBeenCalled(); + component.myControl.disable(); + fixture.detectChanges(); + expect(setDisabledState).toHaveBeenCalled(); + }); +}); diff --git a/tests/issue-305/reactive-forms.spec.ts b/tests/issue-305/reactive-forms.spec.ts new file mode 100644 index 000000000..bf6d699a7 --- /dev/null +++ b/tests/issue-305/reactive-forms.spec.ts @@ -0,0 +1,89 @@ +import { Component, NgModule } from '@angular/core'; +import { + DefaultValueAccessor, + FormControl, + ReactiveFormsModule, +} from '@angular/forms'; +import { + isMockControlValueAccessor, + MockBuilder, + MockRender, + ngMocks, +} from 'ng-mocks'; + +@Component({ + selector: 'my', + template: ` + + `, +}) +class MyComponent { + public readonly myControl = new FormControl(); +} + +@NgModule({ + declarations: [MyComponent], + exports: [MyComponent], + imports: [ReactiveFormsModule], +}) +class MyModule {} + +// checking how normal form works +describe('issue-305:reactive-forms:real', () => { + beforeEach(() => MockBuilder(MyComponent).keep(MyModule)); + + it('correctly mocks CVA', () => { + const component = MockRender(MyComponent).point.componentInstance; + + // DefaultValueAccessor does implement ControlValueAccessor + const valueAccessor = ngMocks.get( + ngMocks.find(['data-testid', 'inputControl']), + DefaultValueAccessor, + ); + + // normal touch + expect(component.myControl.touched).toEqual(false); + valueAccessor.onTouched(); + expect(component.myControl.touched).toEqual(true); + + // normal change + expect(component.myControl.value).toEqual(null); + valueAccessor.onChange(123); + expect(component.myControl.value).toEqual(123); + }); +}); + +// a mock version should behavior similarly but via our own interface +describe('issue-305:reactive-forms:mock', () => { + beforeEach(() => + MockBuilder(MyComponent) + .keep(MyModule) + .mock(DefaultValueAccessor), + ); + + it('correctly mocks CVA', () => { + const fixture = MockRender(MyComponent); + + const component = fixture.point.componentInstance; + + // DefaultValueAccessor does implement ControlValueAccessor + const valueAccessor = ngMocks.get( + ngMocks.find(['data-testid', 'inputControl']), + DefaultValueAccessor, + ); + + // normal touch + expect(component.myControl.touched).toEqual(false); + if (isMockControlValueAccessor(valueAccessor)) { + valueAccessor.__simulateTouch(); + } + expect(component.myControl.touched).toEqual(true); + + // normal change + expect(component.myControl.value).toEqual(null); + if (isMockControlValueAccessor(valueAccessor)) { + valueAccessor.__simulateChange(123); + } + expect(component.myControl.value).toEqual(123); + }); +});