Skip to content

Commit

Permalink
fix(#305): better injection of NgControl
Browse files Browse the repository at this point in the history
closes #305
  • Loading branch information
satanTime committed Feb 27, 2021
1 parent c39f8c2 commit f85f497
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 116 deletions.
29 changes: 29 additions & 0 deletions 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,
};
@@ -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';
Expand Down Expand Up @@ -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,
);
Expand All @@ -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,
);
Expand Down
14 changes: 2 additions & 12 deletions libs/ng-mocks/src/lib/common/func.is-mock-validator.spec.ts
Expand Up @@ -7,7 +7,6 @@ import {
import {
AbstractControl,
AsyncValidator,
NgControl,
NG_ASYNC_VALIDATORS,
NG_VALIDATORS,
ValidationErrors,
Expand Down Expand Up @@ -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);
});

Expand All @@ -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);
});
});
75 changes: 28 additions & 47 deletions libs/ng-mocks/src/lib/common/mock.ts
@@ -1,58 +1,35 @@
// 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';
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.
}
}
};

// 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');
Expand All @@ -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');
Expand All @@ -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.
}
};

Expand Down Expand Up @@ -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);
Expand All @@ -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()],
]);
17 changes: 14 additions & 3 deletions libs/ng-mocks/src/lib/mock-component/mock-component.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -160,8 +163,12 @@ const mixHide = (instance: MockConfig & Record<keyof any, any>, 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);
Expand All @@ -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<any>, mock: Type<any>): void => {
const meta = coreReflectDirectiveResolve(component);
Expand Down
22 changes: 13 additions & 9 deletions libs/ng-mocks/src/lib/mock-directive/mock-directive.ts
Expand Up @@ -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';
Expand All @@ -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<any>,
vcr: ViewContainerRef,
element: ElementRef | null = null,
template: TemplateRef<any> | null = null,
) {
super(injector);
super(injector, ngControl);
this.__ngMocksInstall(vcr, cdr, element, template);
}

Expand All @@ -51,8 +54,8 @@ class DirectiveMockBase extends LegacyControlValueAccessor implements OnInit {
private __ngMocksInstall(
vcr: ViewContainerRef,
cdr: ChangeDetectorRef,
element?: ElementRef,
template?: TemplateRef<any>,
element: ElementRef | null,
template: TemplateRef<any> | 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.
Expand All @@ -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<any>, mock: Type<any>): void => {
Expand Down
3 changes: 1 addition & 2 deletions libs/ng-mocks/src/lib/mock-helper/crawl/el-def-get-node.ts
Expand Up @@ -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;
};
Expand Up @@ -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) ||
Expand Down

0 comments on commit f85f497

Please sign in to comment.