Skip to content

Commit

Permalink
fix(#246): auto spy covers control value accessor too
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Dec 8, 2020
1 parent f9c6074 commit 4596db8
Show file tree
Hide file tree
Showing 11 changed files with 465 additions and 67 deletions.
76 changes: 76 additions & 0 deletions lib/common/mock-control-value-accessor-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// tslint:disable variable-name ban-ts-ignore

import { AsyncValidator, ControlValueAccessor, ValidationErrors, Validator } from '@angular/forms';

import { AnyType } from '../common/core.types';

import { MockControlValueAccessor } from './mock-control-value-accessor';

const appyProxy = (proxy: any, method: string, value: any, storage?: string) => {
if (proxy.instance && storage) {
proxy.instance[storage] = value;
}
if (proxy.instance && proxy.instance[method]) {
return proxy.instance[method](value);
}
};

export class MockControlValueAccessorProxy implements ControlValueAccessor {
public instance?: Partial<MockControlValueAccessor & ControlValueAccessor>;

public constructor(public readonly target?: AnyType<any>) {}

public registerOnChange(fn: any): void {
appyProxy(this, 'registerOnChange', fn, '__simulateChange');
}

public registerOnTouched(fn: any): void {
appyProxy(this, 'registerOnTouched', fn, '__simulateTouch');
}

public setDisabledState(isDisabled: boolean): void {
appyProxy(this, 'setDisabledState', isDisabled);
}

public writeValue(value: any): void {
appyProxy(this, 'writeValue', value);
}
}

export class MockValidatorProxy implements Validator {
public instance?: Partial<MockControlValueAccessor & Validator>;

public constructor(public readonly target?: AnyType<any>) {}

public registerOnValidatorChange(fn: any): void {
appyProxy(this, 'registerOnValidatorChange', fn, '__simulateValidatorChange');
}

public validate(control: any): ValidationErrors | null {
if (this.instance && this.instance.validate) {
return this.instance.validate(control);
}

return null;
}
}

export class MockAsyncValidatorProxy implements AsyncValidator {
public instance?: Partial<MockControlValueAccessor & AsyncValidator>;

public constructor(public readonly target?: AnyType<any>) {}

public registerOnValidatorChange(fn: any): void {
appyProxy(this, 'registerOnValidatorChange', fn, '__simulateValidatorChange');
}

public validate(control: any): any {
if (this.instance && this.instance.validate) {
const result: any = this.instance.validate(control);

return result === undefined ? Promise.resolve(null) : result;
}

return Promise.resolve(null);
}
}
41 changes: 12 additions & 29 deletions lib/common/mock-control-value-accessor.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,25 @@
// tslint:disable variable-name ban-ts-ignore

import { AbstractControl, ControlValueAccessor, ValidationErrors, Validator } from '@angular/forms';

import { Mock } from './mock';

export class MockControlValueAccessor extends Mock implements ControlValueAccessor, Validator {
public readonly __ngMocksMockControlValueAccessor: true = true;
export class MockControlValueAccessor extends Mock {
public get __ngMocksMockControlValueAccessor(): true {
return true;
}

// istanbul ignore next
// @ts-ignore
public __simulateChange = (value: any) => {};

// istanbul ignore next
public __simulateTouch = () => {};

// istanbul ignore next
public __simulateValidatorChange = () => {};

public registerOnChange(fn: (value: any) => void): void {
this.__simulateChange = fn;
}

public registerOnTouched(fn: () => void): void {
this.__simulateTouch = fn;
public __simulateChange(value: any) {
// nothing to do.
}

public registerOnValidatorChange(fn: () => void): void {
this.__simulateValidatorChange = fn;
// istanbul ignore next
public __simulateTouch() {
// nothing to do.
}

// @ts-ignore
public setDisabledState(isDisabled: boolean): void {}

// @ts-ignore
public validate(control: AbstractControl): ValidationErrors | null {
return null;
// istanbul ignore next
public __simulateValidatorChange() {
// nothing to do.
}

// @ts-ignore
public writeValue(value: any) {}
}
48 changes: 42 additions & 6 deletions lib/common/mock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import {
} from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';

import {
MockAsyncValidatorProxy,
MockControlValueAccessorProxy,
MockValidatorProxy,
} from '../common/mock-control-value-accessor-proxy';
import { MockComponent } from '../mock-component/mock-component';
import { MockDirective } from '../mock-directive/mock-directive';
import { MockModule } from '../mock-module/mock-module';
Expand Down Expand Up @@ -128,15 +133,19 @@ describe('Mock', () => {
});

it('should affect as MockComponent', () => {
const proxy = new MockControlValueAccessorProxy(
ChildComponentClass,
);
const instance = new (MockComponent(ChildComponentClass))();
expect(instance).toEqual(jasmine.any(ChildComponentClass));
expect((instance as any).__ngMocksMock).toEqual(true);
expect(
(instance as any).__ngMocksMockControlValueAccessor,
).toEqual(true);

proxy.instance = instance;
const spy = jasmine.createSpy('spy');
instance.registerOnChange(spy);
proxy.registerOnChange(spy);
instance.__simulateChange('test');
expect(spy).toHaveBeenCalledWith('test');

Expand All @@ -145,15 +154,19 @@ describe('Mock', () => {
});

it('should affect as MockDirective', () => {
const proxy = new MockControlValueAccessorProxy(
ChildComponentClass,
);
const instance = new (MockDirective(ChildDirectiveClass))();
expect(instance).toEqual(jasmine.any(ChildDirectiveClass));
expect((instance as any).__ngMocksMock).toEqual(true);
expect(
(instance as any).__ngMocksMockControlValueAccessor,
).toEqual(true);

proxy.instance = instance;
const spy = jasmine.createSpy('spy');
instance.registerOnChange(spy);
proxy.registerOnChange(spy);
instance.__simulateChange('test');
expect(spy).toHaveBeenCalledWith('test');

Expand Down Expand Up @@ -203,31 +216,33 @@ describe('Mock prototype', () => {
}

it('should get all mock things and in the same time respect prototype', () => {
const proxy = new MockControlValueAccessorProxy(CustomComponent);
const mockDef = MockComponent(CustomComponent);
const mock = new mockDef();
expect(mock).toEqual(jasmine.any(CustomComponent));
proxy.instance = mock;

// checking that it was processed through Mock
expect(mock.__ngMocksMock as any).toBe(true);
expect(mock.__ngMocksMockControlValueAccessor as any).toBe(true);

// checking that it was processed through MockControlValueAccessor
const spy = jasmine.createSpy('spy');
mock.registerOnChange(spy);
proxy.registerOnChange(spy);
mock.__simulateChange('test');
expect(spy).toHaveBeenCalledWith('test');

// properties are replaced with their mock coplies too
// properties are replaced with their mock objects too
expect(mock.test1).toBeUndefined();
(mock as any).test1 = 'MyCustomValue';
expect(mock.test1).toEqual('MyCustomValue');

// properties are replaced with their mock coplies too
// properties are replaced with their mock objects too
expect(mock.test2).toBeUndefined();
(mock as any).test2 = 'MyCustomValue';
expect(mock.test2).toEqual('MyCustomValue');

// properties are replaced with their mock coplies too
// properties are replaced with their mock objects too
expect(mock.test).toBeUndefined();
(mock as any).test = 'MyCustomValue';
expect(mock.test).toEqual('MyCustomValue');
Expand Down Expand Up @@ -293,4 +308,25 @@ describe('definitions', () => {
const instance: any = new TestComponent();
expect(instance.test).toEqual(false);
});

it('allows empty instance of MockControlValueAccessorProxy', () => {
const proxy = new MockControlValueAccessorProxy();
proxy.registerOnChange(undefined);
proxy.registerOnTouched(undefined);
proxy.setDisabledState(true);
proxy.setDisabledState(false);
proxy.writeValue(undefined);
});

it('allows empty instance of MockValidatorProxy', () => {
const proxy = new MockValidatorProxy();
proxy.registerOnValidatorChange(undefined);
proxy.validate(undefined);
});

it('allows empty instance of MockAsyncValidatorProxy', () => {
const proxy = new MockAsyncValidatorProxy();
proxy.registerOnValidatorChange(undefined);
proxy.validate(undefined);
});
});
57 changes: 52 additions & 5 deletions lib/common/mock.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,76 @@
// tslint:disable variable-name

import { EventEmitter, Injector, Optional } from '@angular/core';
import { NgControl } from '@angular/forms';
import { FormControlDirective, NgControl } from '@angular/forms';

import { mapValues } from '../common/core.helpers';
import { AnyType } from '../common/core.types';
import { IMockBuilderConfig } from '../mock-builder/types';
import mockHelperStub from '../mock-helper/mock-helper.stub';
import helperMockService from '../mock-service/helper.mock-service';

import { MockControlValueAccessorProxy } from './mock-control-value-accessor-proxy';
import ngMocksUniverse from './ng-mocks-universe';

const applyNgValueAccessor = (instance: Mock, injector?: Injector) => {
if (injector && instance.__ngMocksConfig && instance.__ngMocksConfig.setNgValueAccessor) {
const setValueAccessor = (instance: any, injector?: Injector) => {
if (injector && instance.__ngMocksConfig && instance.__ngMocksConfig.setControlValueAccessor) {
try {
const ngControl = (injector.get as any)(/* A5 */ NgControl, undefined, 0b1010);
if (ngControl && !ngControl.valueAccessor) {
ngControl.valueAccessor = instance;
ngControl.valueAccessor = new MockControlValueAccessorProxy(instance.constructor);
}
} catch (e) {
// nothing to do.
}
}
};

const getRelatedNgControl = (injector: Injector): FormControlDirective => {
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) {
ngControl.valueAccessor.instance = instance;
helperMockService.mock(instance, 'registerOnChange');
helperMockService.mock(instance, 'registerOnTouched');
helperMockService.mock(instance, 'setDisabledState');
helperMockService.mock(instance, 'writeValue');
}
};

// connecting to NG_VALIDATORS
// connecting to NG_ASYNC_VALIDATORS
const installValidator = (validators: any[], instance: any) => {
for (const validator of validators) {
if (!validator.instance && validator.target === instance.constructor) {
validator.instance = instance;
helperMockService.mock(instance, 'registerOnValidatorChange');
helperMockService.mock(instance, 'validate');
}
}
};

const applyNgValueAccessor = (instance: any, injector?: Injector) => {
setValueAccessor(instance, injector);

if (injector) {
try {
const ngControl: any = getRelatedNgControl(injector);
installValueAccessor(ngControl, instance);
installValidator(ngControl._rawValidators, instance);
installValidator(ngControl._rawAsyncValidators, instance);
} catch (e) {
// nothing to do.
}
}
};

const applyOutputs = (instance: Mock & Record<keyof any, any>) => {
const mockOutputs = [];
for (const output of instance.__ngMocksConfig?.outputs || []) {
Expand Down Expand Up @@ -75,7 +122,7 @@ export type ngMocksMockConfig = {
config?: IMockBuilderConfig;
init?: (instance: any) => void;
outputs?: string[];
setNgValueAccessor?: boolean;
setControlValueAccessor?: boolean;
viewChildRefs?: Map<string, string>;
};

Expand Down
Loading

0 comments on commit 4596db8

Please sign in to comment.