Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/articles/troubleshooting/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,19 @@ ngMocks.defaultMock(TitleStrategy, () => MockService(DefaultTitleStrategy));

## Error: NG01052: formGroup expects a FormGroup instance. Please pass one in.

Usually, that happens when you are using `FormBuilder` in your code,
Usually, that happens when you are using `FormBuilder` or `NonNullableFormBuilder` in your code,
and [`MockBuilder`](../api/MockBuilder.md) in your test,
with intentions to keep `FormsModule` or `ReactiveFormsModule` and to mock the rest.

Since Angular `15.1.0`, `FormBuilder` is a `root` provider,
Since Angular `15.1.0`, `FormBuilder` and `NonNullableFormBuilder` are `root` providers,
and, therefore, should be explicitly kept along with `FormsModule` or `ReactiveFormsModule`.

```ts
beforeEach(() =>
MockBuilder(FormComponent)
.keep(FormsModule)
.keep(FormBuilder) // <-- add that
// or .keep(NonNullableFormBuilder)
);
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { InjectionToken } from '@angular/core';

import getRootProviderKeepProvider from './get-root-provider-keep-provider';

const createInjectable = (metadata?: any) => {
const target = () => undefined;

if (metadata !== undefined) {
(target as any).decorators = [
{
args: [metadata],
type: {
prototype: {
ngMetadataName: 'Injectable',
},
},
},
];
}

return target;
};

describe('get-root-provider-keep-provider', () => {
it('returns undefined for non functions', () => {
expect(getRootProviderKeepProvider({})).toBeUndefined();
});

it('returns undefined for injectables without factory metadata', () => {
expect(
getRootProviderKeepProvider(createInjectable({})),
).toBeUndefined();
});

it('keeps factory providers without deps', () => {
const useFactory = () => 'value';
const target = createInjectable({
useFactory,
});

expect(getRootProviderKeepProvider(target)).toEqual({
provide: target,
useFactory,
});
});

it('keeps factory providers with deps', () => {
const dependency = new InjectionToken('dependency');
const useFactory = () => 'value';
const target = createInjectable({
deps: [dependency],
useFactory,
});

expect(getRootProviderKeepProvider(target)).toEqual({
deps: [dependency],
provide: target,
useFactory,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import collectDeclarations from '../../resolve/collect-declarations';

export default (parameter: any): undefined | any => {
if (typeof parameter !== 'function') {
return undefined;
}

const injectable = collectDeclarations(parameter).Injectable;
if (injectable?.useFactory) {
return {
...(injectable.deps === undefined ? {} : { deps: injectable.deps }),
provide: parameter,
useFactory: injectable.useFactory,
};
}

return undefined;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { InjectionToken } from '@angular/core';

import { NG_MOCKS_ROOT_PROVIDERS } from '../../common/core.tokens';
import ngMocksUniverse from '../../common/ng-mocks-universe';

import handleRootProviders from './handle-root-providers';

const createInjectable = (metadata?: any) => {
const target = () => undefined;

if (metadata !== undefined) {
if (metadata.providedIn !== undefined) {
(target as any).ɵprov = {
providedIn: metadata.providedIn,
};
}
(target as any).decorators = [
{
args: [metadata],
type: {
prototype: {
ngMetadataName: 'Injectable',
},
},
},
];
}

return target;
};

const restoreMap = (target: Map<any, any>, source: Map<any, any>) => {
target.clear();
for (const [key, value] of source) {
target.set(key, value);
}
};

const restoreSet = (target: Set<any>, source: Set<any>) => {
target.clear();
for (const value of source) {
target.add(value);
}
};

describe('handle-root-providers', () => {
let builtProviders: Map<any, any>;
let config: Map<any, any>;
let touches: Set<any>;

beforeEach(() => {
builtProviders = new Map(ngMocksUniverse.builtProviders);
config = new Map(ngMocksUniverse.config);
touches = new Set(ngMocksUniverse.touches);

ngMocksUniverse.config.set('ngMocksDeps', new Set());
ngMocksUniverse.config.set('ngMocksDepsSkip', new Set());
});

afterEach(() => {
restoreMap(ngMocksUniverse.builtProviders, builtProviders);
restoreMap(ngMocksUniverse.config, config);
restoreSet(ngMocksUniverse.touches, touches);
});

const handle = (
parameter: any,
options?: { keepDef?: Set<any>; resolution?: any },
) => {
ngMocksUniverse.config.set('ngMocksDeps', new Set([parameter]));

const ngModule: any = {
declarations: [],
imports: [],
providers: [],
};
const resolutionMap = new Map<any, any>();
if (options?.resolution !== undefined) {
resolutionMap.set(parameter, options.resolution);
}

handleRootProviders(
ngModule,
{
keepDef: options?.keepDef ?? new Set(),
mockDef: new Set(),
} as any,
{
get: (def: any) => resolutionMap.get(def),
has: (def: any) => resolutionMap.has(def),
set: (def: any, value: any) => {
resolutionMap.set(def, value);
},
} as any,
);

return ngModule.providers;
};

it('skips inferred parameters when root providers are explicitly kept', () => {
const token = new InjectionToken('token');

expect(
handle(token, {
keepDef: new Set([NG_MOCKS_ROOT_PROVIDERS]),
}),
).toEqual([]);
});

it('reconstructs keep providers for root injectables with factories', () => {
const useFactory = () => 'value';
const target = createInjectable({
providedIn: 'root',
useFactory,
});

ngMocksUniverse.builtProviders.set(target, target);

expect(handle(target)).toEqual([
{
provide: target,
useFactory,
},
]);
});

it('falls back to the original parameter without factory metadata', () => {
const target = createInjectable({
providedIn: 'root',
});

ngMocksUniverse.builtProviders.set(target, target);

expect(handle(target)).toEqual([target]);
});

it('uses resolved providers when they differ from the parameter', () => {
const token = new InjectionToken('token');
const provider = {
provide: token,
useValue: 'value',
};

expect(
handle(token, {
resolution: provider,
}),
).toEqual([provider]);
});

it('creates empty-array factories for multi tokens without resolved providers', () => {
const token = new InjectionToken('token');

ngMocksUniverse.builtProviders.set(token, null);
ngMocksUniverse.config.set('ngMocksMulti', new Set([token]));

expect(handle(token)).toEqual([
jasmine.objectContaining({
deps: jasmine.any(Array),
provide: token,
useFactory: jasmine.any(Function),
}),
]);
expect(handle(token)[0].useFactory()).toEqual([]);
});

it('creates undefined factories for non-multi injection tokens without resolved providers', () => {
const token = new InjectionToken('token');

ngMocksUniverse.builtProviders.set(token, null);

expect(handle(token)).toEqual([
jasmine.objectContaining({
deps: jasmine.any(Array),
provide: token,
useFactory: jasmine.any(Function),
}),
]);
expect(handle(token)[0].useFactory()).toBeUndefined();
});

it('ignores unresolved non-token parameters', () => {
const target = createInjectable({
providedIn: 'root',
});

ngMocksUniverse.builtProviders.set(target, null);

expect(handle(target)).toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ngMocksUniverse from '../../common/ng-mocks-universe';
import helperResolveProvider from '../../mock-service/helper.resolve-provider';
import helperUseFactory from '../../mock-service/helper.use-factory';

import getRootProviderKeepProvider from './get-root-provider-keep-provider';
import getRootProviderParameters from './get-root-provider-parameters';
import { BuilderData, NgMeta } from './types';

Expand All @@ -17,7 +18,7 @@ export default (ngModule: NgMeta, { keepDef, mockDef }: BuilderData, resolutions
for (const parameter of mapValues(parameters)) {
const mock = helperResolveProvider(parameter, resolutions);
if (mock) {
ngModule.providers.push(mock);
ngModule.providers.push(mock === parameter ? (getRootProviderKeepProvider(parameter) ?? mock) : mock);
} else if (isNgInjectionToken(parameter)) {
const multi =
ngMocksUniverse.config.has('ngMocksMulti') && ngMocksUniverse.config.get('ngMocksMulti').has(parameter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ngMocksUniverse from '../../common/ng-mocks-universe';
import markExported from '../../mock/mark-exported';
import markProviders from '../../mock-module/mark-providers';

import getRootProviderKeepProvider from './get-root-provider-keep-provider';
import initModule from './init-module';
import { BuilderData, NgMeta } from './types';

Expand Down Expand Up @@ -45,7 +46,7 @@ const handleDef = ({ imports, declarations, providers }: NgMeta, def: any, defPr
if (isNgDef(def, 'i') || !isNgDef(def)) {
const mock = ngMocksUniverse.builtProviders.get(def);
if (mock && typeof mock !== 'string' && isNgDef(mock, 't') === false) {
providers.push(mock);
providers.push(mock === def ? (getRootProviderKeepProvider(def) ?? mock) : mock);
touched = true;
}
}
Expand Down
Loading
Loading