diff --git a/libs/ng-mocks/src/lib/common/core.reflect.provided-in.spec.ts b/libs/ng-mocks/src/lib/common/core.reflect.provided-in.spec.ts new file mode 100644 index 0000000000..7a63b24584 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/core.reflect.provided-in.spec.ts @@ -0,0 +1,18 @@ +import coreReflectProvidedIn from './core.reflect.provided-in'; + +describe('core.reflect.provided-in', () => { + it('covers ngInjectableDef', () => { + expect( + coreReflectProvidedIn({ + ngInjectableDef: {}, + }), + ).toEqual(undefined); + expect( + coreReflectProvidedIn({ + ngInjectableDef: { + providedIn: 'root', + }, + }), + ).toEqual('root'); + }); +}); diff --git a/libs/ng-mocks/src/lib/common/core.reflect.provided-in.ts b/libs/ng-mocks/src/lib/common/core.reflect.provided-in.ts new file mode 100644 index 0000000000..116cdb99e9 --- /dev/null +++ b/libs/ng-mocks/src/lib/common/core.reflect.provided-in.ts @@ -0,0 +1,5 @@ +import { AnyType } from './core.types'; + +export default (declaration: any): undefined | AnyType | string => { + return declaration?.ɵprov?.providedIn ?? declaration?.ngInjectableDef?.providedIn; +}; diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/get-root-provider-parameters.ts b/libs/ng-mocks/src/lib/mock-builder/promise/get-root-provider-parameters.ts index 14d1e9ad12..f1ceeac54e 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/get-root-provider-parameters.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/get-root-provider-parameters.ts @@ -6,6 +6,7 @@ import addDefToRootProviderParameters from './add-def-to-root-provider-parameter import checkRootProviderDependency from './check-root-provider-dependency'; import extractDep from './extract-dep'; import getRootProvidersData from './get-root-providers-data'; +import handleProvidedInDependency from './handle-provided-in-dependency'; import skipRootProviderDependency from './skip-root-provider-dependency'; import { BuilderData } from './types'; @@ -19,6 +20,7 @@ export default (mockDef: BuilderData['mockDef']): Set => { for (const decorators of coreReflectJit().parameters(def)) { const provide: any = extractDep(decorators); + handleProvidedInDependency(provide); if (skipRootProviderDependency(provide)) { continue; } diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/handle-provided-in-dependency.ts b/libs/ng-mocks/src/lib/mock-builder/promise/handle-provided-in-dependency.ts new file mode 100644 index 0000000000..737bf07067 --- /dev/null +++ b/libs/ng-mocks/src/lib/mock-builder/promise/handle-provided-in-dependency.ts @@ -0,0 +1,17 @@ +import coreReflectProvidedIn from '../../common/core.reflect.provided-in'; +import ngMocksUniverse from '../../common/ng-mocks-universe'; + +export default (provide: any): void => { + if (ngMocksUniverse.touches.has(provide)) { + return; + } + + const providedIn = coreReflectProvidedIn(provide); + if (!providedIn) { + return; + } + + if (ngMocksUniverse.config.get('ngMocksDepsSkip').has(providedIn)) { + ngMocksUniverse.config.get('ngMocksDepsSkip').add(provide); + } +}; diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/skip-dep.ts b/libs/ng-mocks/src/lib/mock-builder/promise/skip-dep.ts index a84df2d337..3bf6f0697a 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/skip-dep.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/skip-dep.ts @@ -1,6 +1,7 @@ import { DOCUMENT } from '@angular/common'; import coreConfig from '../../common/core.config'; +import coreReflectProvidedIn from '../../common/core.reflect.provided-in'; import { isNgInjectionToken } from '../../common/func.is-ng-injection-token'; import ngMocksUniverse from '../../common/ng-mocks-universe'; @@ -25,9 +26,8 @@ export default (provide: any): boolean => { } // Empty providedIn or things for a platform have to be skipped. - let skip = !provide.ɵprov?.providedIn || provide.ɵprov.providedIn === 'platform'; - // istanbul ignore next: A6 - skip = skip && (!provide.ngInjectableDef?.providedIn || provide.ngInjectableDef.providedIn === 'platform'); + const providedIn = coreReflectProvidedIn(provide); + const skip = !providedIn || providedIn === 'platform'; if (typeof provide === 'function' && skip) { return true; } diff --git a/tests/issue-377/e2e.spec.ts b/tests/issue-377/e2e.spec.ts new file mode 100644 index 0000000000..4f902c79af --- /dev/null +++ b/tests/issue-377/e2e.spec.ts @@ -0,0 +1,91 @@ +import { + Component, + Inject, + Injectable as InjectableSource, + InjectionToken, + NgModule, + VERSION, +} from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +// Because of A5 we need to cast Injectable to any type. +// But because of A10+ we need to do it via a middle function. +function Injectable(...args: any[]): any { + return InjectableSource(...args); +} + +@NgModule({}) +class TargetModule {} + +@Injectable({ + providedIn: TargetModule, +} as any) +class TargetService { + private readonly name = 'target'; + + public echo(): string { + return this.name; + } +} + +// TODO Remove any with A5 +const TOKEN = new (InjectionToken as any)('TOKEN', { + factory: () => 'TOKEN', + providedIn: TargetModule, +}); + +@Component({ + selector: 'target', + template: `service:{{ service.echo() }} token:{{ token }}`, +}) +class TargetComponent { + public constructor( + public readonly service: TargetService, + @Inject(TOKEN) public readonly token: string, + ) {} +} + +describe('issue-377', () => { + beforeEach(() => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + } + }); + + describe('expected', () => { + beforeEach(() => + TestBed.configureTestingModule({ + declarations: [TargetComponent], + imports: [TargetModule], + }).compileComponents(), + ); + + it('sets TestBed correctly', () => { + const fixture = MockRender(TargetComponent); + expect(ngMocks.formatText(fixture)).toEqual( + 'service:target token:TOKEN', + ); + }); + }); + + describe('keep', () => { + beforeEach(() => MockBuilder(TargetComponent).keep(TargetModule)); + + it('sets TestBed correctly', () => { + const fixture = MockRender(TargetComponent); + expect(ngMocks.formatText(fixture)).toEqual( + 'service:target token:TOKEN', + ); + }); + }); + + describe('mock', () => { + beforeEach(() => MockBuilder(TargetComponent).mock(TargetModule)); + + it('sets TestBed correctly', () => { + const fixture = MockRender(TargetComponent); + expect(ngMocks.formatText(fixture)).toEqual('service: token:'); + }); + }); +}); diff --git a/tests/issue-377/test.spec.ts b/tests/issue-377/test.spec.ts new file mode 100644 index 0000000000..7cf6c9a284 --- /dev/null +++ b/tests/issue-377/test.spec.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +@Component({ + selector: 'app-form', + template: ` +
+ +
+ `, +}) +class FormComponent { + public form = this.fb.group({ + name: [], + }); + + public constructor(private readonly fb: FormBuilder) {} +} + +describe('issue-377:classic', () => { + beforeEach(() => + TestBed.configureTestingModule({ + declarations: [FormComponent], + imports: [ReactiveFormsModule], + }).compileComponents(), + ); + + it('sets TestBed correctly', () => { + expect(() => MockRender(FormComponent)).not.toThrow(); + }); +}); + +describe('issue-377:mock', () => { + beforeEach(() => + MockBuilder(FormComponent).keep(ReactiveFormsModule), + ); + + it('sets TestBed correctly', () => { + expect(() => MockRender(FormComponent)).not.toThrow(); + }); +});