diff --git a/README.md b/README.md index 9f9f359fe5..83ae4b562a 100644 --- a/README.md +++ b/README.md @@ -779,6 +779,8 @@ const ngModule = MockBuilder().keep(MyComponent, { export: true }).mock(MyModule // We should use .keep. const ngModule = MockBuilder(MyComponent, MyModule) .keep(SomeModule) + .keep(SomeModule.forSome()) + .keep(SomeModule.forAnother()) .keep(SomeComponent) .keep(SomeDirective) .keep(SomePipe) @@ -788,6 +790,8 @@ const ngModule = MockBuilder(MyComponent, MyModule) // If we want to mock something, even a part of a kept module we should use .mock. const ngModule = MockBuilder(MyComponent, MyModule) .mock(SomeModule) + .mock(SomeModule.forSome()) + .mock(SomeModule.forAnother()) .mock(SomeComponent) .mock(SomeDirective) .mock(SomePipe) diff --git a/lib/common/lib.ts b/lib/common/lib.ts index 0366604e78..4ef96fe035 100644 --- a/lib/common/lib.ts +++ b/lib/common/lib.ts @@ -108,13 +108,8 @@ export const extendClass = (base: Type): Type => { return child; }; -export const isNgType = (object: Type, type: string): boolean => { - try { - return jitReflector.annotations(object).some(annotation => annotation.ngMetadataName === type); - } catch (error) { - return false; - } -}; +export const isNgType = (object: Type, type: string): boolean => + jitReflector.annotations(object).some(annotation => annotation.ngMetadataName === type); /** * Checks whether a class was decorated by a ng type. diff --git a/lib/common/ng-mocks-universe.ts b/lib/common/ng-mocks-universe.ts index 6cdcdb92f9..93dd4811f0 100644 --- a/lib/common/ng-mocks-universe.ts +++ b/lib/common/ng-mocks-universe.ts @@ -1,6 +1,6 @@ import { InjectionToken } from '@angular/core'; -import { AnyType } from './lib'; +import { AbstractType, Type } from './lib'; /** * Can be changed any time. @@ -14,5 +14,5 @@ export const ngMocksUniverse = { config: new Map(), flags: new Set(['cacheModule', 'cacheComponent', 'cacheDirective', 'cacheProvider']), resetOverrides: new Set(), - touches: new Set | InjectionToken>(), + touches: new Set | AbstractType | InjectionToken>(), }; diff --git a/lib/mock-builder/mock-builder.ts b/lib/mock-builder/mock-builder.ts index 41d26bfae2..0d6949a0d1 100644 --- a/lib/mock-builder/mock-builder.ts +++ b/lib/mock-builder/mock-builder.ts @@ -60,6 +60,7 @@ const defaultMock = {}; // simulating Symbol export class MockBuilderPromise implements PromiseLike { protected beforeCC: Set<(testBed: typeof TestBed) => void> = new Set(); protected configDef: Map | InjectionToken, any> = new Map(); + protected defProviders: Map | InjectionToken, Provider[]> = new Map(); protected defValue: Map | InjectionToken, any> = new Map(); protected excludeDef: Set | InjectionToken> = new Set(); protected keepDef: Set | InjectionToken> = new Set(); @@ -126,7 +127,13 @@ export class MockBuilderPromise implements PromiseLike { } for (const def of mapValues(this.mockDef)) { - if (isNgDef(def, 'c')) { + if (isNgDef(def, 'm') && this.defProviders.has(def)) { + const loProviders = this.defProviders.get(def); + const [changed, loDef] = loProviders ? MockNgDef({ providers: loProviders }) : [false, {}]; + if (changed && loDef.providers) { + this.defProviders.set(def, loDef.providers); + } + } else if (isNgDef(def, 'c')) { ngMocksUniverse.builder.set(def, MockComponent(def)); } else if (isNgDef(def, 'd')) { ngMocksUniverse.builder.set(def, MockDirective(def)); @@ -157,7 +164,9 @@ export class MockBuilderPromise implements PromiseLike { // Adding suitable leftovers. for (const def of [...mapValues(this.mockDef), ...mapValues(this.keepDef), ...mapValues(this.replaceDef)]) { - if (!isNgDef(def) || ngMocksUniverse.touches.has(def)) { + if (isNgDef(def, 'm') && this.defProviders.has(def)) { + // nothing to do + } else if (!isNgDef(def) || ngMocksUniverse.touches.has(def)) { continue; } const config = this.configDef.get(def); @@ -165,7 +174,16 @@ export class MockBuilderPromise implements PromiseLike { continue; } if (isNgDef(def, 'm')) { - imports.push(ngMocksUniverse.builder.get(def)); + const loModule = ngMocksUniverse.builder.get(def); + const loProviders = this.defProviders.has(def) ? this.defProviders.get(def) : undefined; + imports.push( + loProviders + ? { + ngModule: loModule, + providers: loProviders, + } + : loModule + ); } else { declarations.push(ngMocksUniverse.builder.get(def)); } @@ -317,11 +335,19 @@ export class MockBuilderPromise implements PromiseLike { } public keep(input: any, config?: IMockBuilderConfig): this { - const def = isNgModuleDefWithProviders(input) ? input.ngModule : input; + const { def, providers } = isNgModuleDefWithProviders(input) + ? { def: input.ngModule, providers: input.providers } + : { def: input, providers: undefined }; + const existing = this.keepDef.has(def) ? this.defProviders.get(def) : []; this.wipe(def); this.keepDef.add(def); + // a magic to support modules with providers. + if (providers) { + this.defProviders.set(def, [...existing, ...providers]); + } + if (config) { this.configDef.set(def, config); } else { @@ -338,19 +364,31 @@ export class MockBuilderPromise implements PromiseLike { config?: IMockBuilderConfig ): this; public mock(token: InjectionToken, mock?: any): this; + public mock(def: NgModuleWithProviders): this; public mock(def: AnyType, mock: IMockBuilderConfig): this; public mock(provider: AnyType, mock?: Partial): this; public mock(def: AnyType): this; - public mock(def: any, a1: any = defaultMock, a2?: any): this { + public mock(input: any, a1: any = defaultMock, a2?: any): this { + const { def, providers } = isNgModuleDefWithProviders(input) + ? { def: input.ngModule, providers: input.providers } + : { def: input, providers: undefined }; + let mock: any = a1; let config: any = a1 === defaultMock ? undefined : a1; if (isNgDef(def, 'p') && typeof a1 === 'function') { mock = a1; config = a2; } + + const existing = this.mockDef.has(def) ? this.defProviders.get(def) : []; this.wipe(def); this.mockDef.add(def); + // a magic to support modules with providers. + if (providers) { + this.defProviders.set(def, [...existing, ...providers]); + } + if (isNgDef(def, 'p') && typeof mock === 'function') { this.defValue.set(def, mock); } @@ -414,6 +452,7 @@ export class MockBuilderPromise implements PromiseLike { } private wipe(def: Type): void { + this.defProviders.delete(def); this.defValue.delete(def); this.excludeDef.delete(def); this.keepDef.delete(def); diff --git a/tests/issue-197/abstract.spec.ts b/tests/issue-197/abstract.spec.ts new file mode 100644 index 0000000000..06e85144f5 --- /dev/null +++ b/tests/issue-197/abstract.spec.ts @@ -0,0 +1,13 @@ +import { TestBed } from '@angular/core/testing'; +import { DomSanitizer } from '@angular/platform-browser'; +import { MockBuilder } from 'ng-mocks'; + +describe('issue-197:abstract', () => { + const expected = {}; + beforeEach(() => MockBuilder().mock(DomSanitizer, expected)); + + it('mocks abstract classes', () => { + const actual = TestBed.get(DomSanitizer); + expect(actual).toBe(expected); + }); +}); diff --git a/tests/issue-197/with-providers.spec.ts b/tests/issue-197/with-providers.spec.ts new file mode 100644 index 0000000000..e110264c81 --- /dev/null +++ b/tests/issue-197/with-providers.spec.ts @@ -0,0 +1,91 @@ +// tslint:disable:no-unnecessary-class + +import { Component, Injectable, NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender, NgModuleWithProviders } from 'ng-mocks'; + +@Injectable() +class DependencyService { + private readonly name: string = 'dependency'; + + public echo(): string { + return this.name; + } +} + +@NgModule({}) +class DependencyModule { + static withProviders(): NgModuleWithProviders { + return { + ngModule: DependencyModule, + providers: [ + { + provide: DependencyService, + useValue: { + echo: () => 'via-provider', + }, + }, + ], + }; + } + + public readonly service: DependencyService; + + constructor(service: DependencyService) { + this.service = service; + } +} + +@Component({ + selector: 'target', + template: '{{ service.echo() }}', +}) +class TargetComponent { + public readonly service: DependencyService; + + constructor(service: DependencyService) { + this.service = service; + } +} + +@NgModule({ + declarations: [TargetComponent], +}) +class TargetModule {} + +describe('issue-197:with-providers:manually-injection', () => { + beforeEach(() => { + const module = MockBuilder(TargetComponent, TargetModule).build(); + + return TestBed.configureTestingModule({ + declarations: module.declarations, + imports: [...module.imports, DependencyModule.withProviders()], + providers: module.providers, + }).compileComponents(); + }); + + it('creates component with provided dependencies', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toEqual('via-provider'); + }); +}); + +describe('issue-197:with-providers', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(DependencyModule.withProviders())); + + it('creates component with provided dependencies', () => { + const fixture = MockRender(TargetComponent); + + expect(fixture.nativeElement.innerHTML).toEqual('via-provider'); + }); +}); + +describe('issue-197:with-providers', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).mock(DependencyModule.withProviders())); + + it('creates component with provided dependencies', () => { + const fixture = MockRender(TargetComponent); + + expect(fixture.nativeElement.innerHTML).toEqual(''); + }); +});