Skip to content

Commit

Permalink
fix(core): providers with useExisting will be kept if their value is …
Browse files Browse the repository at this point in the history
…a kept declaration help-me-mom#3778
  • Loading branch information
satanTime committed Oct 9, 2022
1 parent 8e5c8b0 commit ac39cf8
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 26 deletions.
8 changes: 8 additions & 0 deletions libs/ng-mocks/src/lib/common/func.extract-forward-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// handles forwardRef on useExisting
export default (provide: any): any => {
if (typeof provide === 'function' && provide.__forward_ref__) {
return provide();
}

return provide;
};
3 changes: 2 additions & 1 deletion libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export class MockBuilderPromise implements IMockBuilder {
public exclude(def: any): this {
this.wipe(def);
this.excludeDef.add(def);
this.setConfigDef(def);

return this;
}
Expand Down Expand Up @@ -215,7 +216,7 @@ export class MockBuilderPromise implements IMockBuilder {
};
}

private setConfigDef(def: any, config: any): void {
private setConfigDef(def: any, config?: any): void {
if (config || !this.configDef.has(def)) {
this.configDef.set(def, config ?? this.configDefault);
}
Expand Down
13 changes: 3 additions & 10 deletions libs/ng-mocks/src/lib/mock-builder/promise/extract-dep.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
// Extracts dependency among flags of parameters.

const detectForwardRed = (provide: any): any => {
if (typeof provide === 'function' && provide.__forward_ref__) {
return provide();
}

return provide;
};
import funcExtractForwardRef from '../../common/func.extract-forward-ref';

// Extracts dependency among flags of parameters.
export default (decorators?: any[]): any => {
if (!decorators) {
return;
Expand All @@ -23,5 +16,5 @@ export default (decorators?: any[]): any => {
}
}

return detectForwardRed(provide);
return funcExtractForwardRef(provide);
};
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default ({

configDef.set(dependency, {
dependency: true,
__internal: true,
});
ngMocksUniverse.touches.add(dependency);
}
Expand Down
31 changes: 25 additions & 6 deletions libs/ng-mocks/src/lib/mock-service/helper.resolve-provider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { extractDependency } from '../common/core.helpers';
import { NG_MOCKS_INTERCEPTORS } from '../common/core.tokens';
import funcExtractForwardRef from '../common/func.extract-forward-ref';
import funcGetProvider from '../common/func.get-provider';
import { isNgInjectionToken } from '../common/func.is-ng-injection-token';
import ngMocksUniverse from '../common/ng-mocks-universe';
Expand Down Expand Up @@ -48,7 +49,7 @@ const excludeInterceptors = (provider: any, provide: any): boolean => {
if (provider.useFactory || provider.useValue) {
return true;
}
const interceptor = provider.useExisting || provider.useClass;
const interceptor = funcExtractForwardRef(provider.useExisting) || provider.useClass;
if (!ngMocksUniverse.builtProviders.has(interceptor) || ngMocksUniverse.builtProviders.get(interceptor) === null) {
return true;
}
Expand Down Expand Up @@ -149,7 +150,7 @@ const areEqualDefs = (mockDef: any, provider: any, provide: any): boolean => {

const isPreconfiguredDependency = (provider: any, provide: any): boolean => {
// we should not touch excluded providers.
if (ngMocksUniverse.builtProviders.has(provide) && ngMocksUniverse.builtProviders.get(provide) === null) {
if (ngMocksUniverse.builtProviders.get(provide) === null) {
return true;
}

Expand All @@ -160,16 +161,34 @@ const isPreconfiguredDependency = (provider: any, provide: any): boolean => {
return excludeInterceptors(provider, provide);
};

const isPreconfiguredUseExisting = (provider: any, provide: any): boolean => {
// we should not touch non-useExisting providers.
if (!provider || typeof provider !== 'object' || !provider.useExisting) {
return false;
}
if (provider.useExisting.mockOf) {
return true;
}

// skipping explicit declarations (not internally processed)
if (ngMocksUniverse.getResolution(provide) && !ngMocksUniverse.config.get(provide).__internal) {
return false;
}

return ngMocksUniverse.getResolution(funcExtractForwardRef(provider.useExisting)) === 'keep';
};

// tries to resolve a provider based on current universe state.
export default (provider: any, resolutions: Map<any, any>, changed?: () => void) => {
const { provide, multi, change } = parseProvider(provider, changed);
// we should not touch our system providers.
if (provider && typeof provider === 'object' && provider.useExisting && provider.useExisting.mockOf) {
return provider;
}
if (isPreconfiguredDependency(provider, provide)) {
return change();
}
if (isPreconfiguredUseExisting(provider, provide)) {
ngMocksUniverse.touches.add(provide);

return provider;
}
if (resolutions.has(provide)) {
return createFromResolution(provide, resolutions.get(provide));
}
Expand Down
11 changes: 2 additions & 9 deletions libs/ng-mocks/src/lib/mock/clone-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Provider } from '@angular/core';
import coreForm from '../common/core.form';
import { flatten } from '../common/core.helpers';
import { AnyType } from '../common/core.types';
import funcExtractForwardRef from '../common/func.extract-forward-ref';
import funcGetProvider from '../common/func.get-provider';
import {
MockAsyncValidatorProxy,
Expand Down Expand Up @@ -37,15 +38,7 @@ const processOwnUseExisting = (sourceType: AnyType<any>, mockType: AnyType<any>,
return undefined;
}

if (provider !== provide && provider.useExisting === sourceType) {
return toExistingProvider(provide, mockType);
}
if (
provider !== provide &&
provider.useExisting &&
provider.useExisting.__forward_ref__ &&
provider.useExisting() === sourceType
) {
if (provider !== provide && funcExtractForwardRef(provider.useExisting) === sourceType) {
return toExistingProvider(provide, mockType);
}

Expand Down
160 changes: 160 additions & 0 deletions tests/issue-3791/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Component, forwardRef, VERSION } from '@angular/core';
import {
ControlValueAccessor,
FormControl,
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms';

import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';

// A standalone CVA component
@Component(
{
selector: 'standalone-cva',
template: `<input
type="text"
[value]="value"
change="onValueChange($event.target.value)"
/>`,
standalone: true,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StandaloneCVAComponent),
multi: true,
},
],
} as never /* TODO: remove after upgrade to a14 */,
)
class StandaloneCVAComponent implements ControlValueAccessor {
public value = '';

onChange: any = () => undefined;
onTouched: any = () => undefined;
writeValue: any = () => undefined;

registerOnChange(fn: (url: string) => void): void {
this.onChange = fn;
}

registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}

setDisabledState(): void {}

onValueChange(value: string): void {
this.value = value;
this.onChange(this.value);
}
}

// @see https://github.com/help-me-mom/ng-mocks/issues/3778
// The problem is that StandaloneCVAComponent provides itself as NG_VALUE_ACCESSOR,
// whereas NG_VALUE_ACCESSOR is going to be mocked.
// The fix is to keep such NG_VALUE_ACCESSOR if its useExisting points to a kept thing.
describe('issue-3791', () => {
if (Number.parseInt(VERSION.major, 10) < 14) {
it('needs >=a14', () => {
expect(true).toBeTruthy();
});

return;
}

describe('issue', () => {
beforeEach(() =>
MockBuilder([
StandaloneCVAComponent,
FormsModule,
ReactiveFormsModule,
]),
);

it('does not fail on standard render', () => {
const fixture = MockRender(StandaloneCVAComponent);
expect(fixture.componentInstance).toBeTruthy();
});

it('does not fail as ReactiveFormsModule', () => {
const fixture = MockRender(
`<standalone-cva [formControl]="control"></standalone-cva>`,
{
control: new FormControl('test'),
},
);

expect(fixture.componentInstance).toBeTruthy();
});

it('does not fail as FormsModule', () => {
const fixture = MockRender(
`<standalone-cva [(ngModel)]="control"></standalone-cva>`,
{
value: 'test',
},
);

expect(fixture.componentInstance).toBeTruthy();
});
});

describe('.mock', () => {
beforeEach(() =>
MockBuilder([
StandaloneCVAComponent,
FormsModule,
ReactiveFormsModule,
]).mock(NG_VALUE_ACCESSOR),
);

it('provides undefined as NG_VALUE_ACCESSOR', () => {
MockRender(StandaloneCVAComponent);
const token = ngMocks.findInstance(NG_VALUE_ACCESSOR);
expect(token).toEqual([undefined as never]);
});
});

describe('.mock with a value', () => {
const mock = {
onChange: () => undefined,
onTouched: () => undefined,
writeValue: () => undefined,
} as never;

beforeEach(() =>
MockBuilder([
StandaloneCVAComponent,
FormsModule,
ReactiveFormsModule,
]).mock(NG_VALUE_ACCESSOR, [mock]),
);

it('provides a mock as NG_VALUE_ACCESSOR', () => {
MockRender(StandaloneCVAComponent);
const token = ngMocks.findInstance(NG_VALUE_ACCESSOR);
expect(token).toEqual([mock]);
});
});

describe('.exclude', () => {
beforeEach(() =>
MockBuilder([
StandaloneCVAComponent,
FormsModule,
ReactiveFormsModule,
]).exclude(NG_VALUE_ACCESSOR),
);

it('removes NG_VALUE_ACCESSOR from declarations', () => {
MockRender(StandaloneCVAComponent);
const token = ngMocks.findInstance(
NG_VALUE_ACCESSOR,
undefined,
);
expect(token).toEqual(undefined);
});
});
});

0 comments on commit ac39cf8

Please sign in to comment.