diff --git a/README.md b/README.md index 51321435bf..4d952fa563 100644 --- a/README.md +++ b/README.md @@ -869,6 +869,12 @@ a type of `MockedModule` and provides: - base primitives instead of tokens with a `useValue` definition - mocked copies of tokens with a `useValue` definition +If you get an error like: "**Component is part of the declaration of 2 modules**", +then consider usage of [`ngMocks.guts`](#ngmocks) or [`MockBuilder`](#mockbuilder). +The issue here is, that you are trying to mock a module that has a declaration which is specified in `TestBed`, +therefore, it should be handled in a better way which excludes it ([`ngMocks.guts`](#ngmocks)), +or handles internals vs externals properly ([`MockBuilder`](#mockbuilder)). + Let's imagine an Angular application where `TargetComponent` depends on a module of `DependencyModule` and we would like to mock in a test. @@ -1281,6 +1287,14 @@ The good thing here is that commonly the dependencies have been declared or impo tested thing is. Therefore, with help of `MockBuilder` we can quite easily define a testing module, where everything in the module will be mocked except the tested thing: `MockBuilder(TheThing, ItsModule)`. +MockBuilder tends to provide **a simple instrument to mock Angular dependencies**, does it in isolated scopes, +and has a rich toolkit that supports: + +- respect of internal vs external declarations (precise exports) +- detection and creation of mocked copies for root providers +- replacement of modules and declarations in any depth +- exclusion of modules, declarations and providers in any depth +
Click to see a code sample demonstrating ease of mocking in Angular tests

@@ -1517,7 +1531,16 @@ beforeEach(() => .keep(SomeModuleComponentDirectivePipeProvider1, { dependency: true, }) - .mock(SomeModuleComponentDirectivePipeProvider1, { + .mock(SomeModuleComponentDirectivePipe, { + dependency: true, + }) + // Pass the same def as a mocked instance, if you want only to + // specify the config. + .mock(SomeProvider, SomeProvider, { + dependency: true, + }) + // Or provide a mocked instance together with the config. + .mock(SomeProvider, mockedInstance, { dependency: true, }) .replace(SomeModuleComponentDirectivePipeProvider1, anything1, { diff --git a/lib/common/lib.ts b/lib/common/lib.ts index db361ec929..6329dbceac 100644 --- a/lib/common/lib.ts +++ b/lib/common/lib.ts @@ -104,9 +104,7 @@ export const extendClass = (base: Type): Type => { (window as any).ngMocksParent = undefined; // the next step is to respect constructor parameters as the parent class. - child.parameters = jitReflector - .parameters(parent) - .map(parameter => ngMocksUniverse.cacheMocks.get(parameter) || parameter); + child.parameters = jitReflector.parameters(parent); return child; }; @@ -115,15 +113,40 @@ export const isNgType = (declaration: Type, type: string): boolean => jitReflector.annotations(declaration).some(annotation => annotation.ngMetadataName === type); /** - * Checks whether a class was decorated by a ng type. - * m - module. - * c - component. - * d - directive. - * p - pipe. + * Checks whether a class was decorated by @NgModule. + * + * @see https://github.com/ike18t/ng-mocks#isngdef + */ +export function isNgDef(declaration: any, ngType: 'm'): declaration is Type; + +/** + * Checks whether a class was decorated by @Component. + * + * @see https://github.com/ike18t/ng-mocks#isngdef + */ +export function isNgDef(declaration: any, ngType: 'c'): declaration is Type; + +/** + * Checks whether a class was decorated by @Directive. + * + * @see https://github.com/ike18t/ng-mocks#isngdef + */ +export function isNgDef(declaration: any, ngType: 'd'): declaration is Type; + +/** + * Checks whether a class was decorated by @Pipe. + * + * @see https://github.com/ike18t/ng-mocks#isngdef */ -export function isNgDef(declaration: any, ngType: 'm' | 'c' | 'd'): declaration is Type; export function isNgDef(declaration: any, ngType: 'p'): declaration is Type; + +/** + * Checks whether a class was decorated by a ng type. + * + * @see https://github.com/ike18t/ng-mocks#isngdef + */ export function isNgDef(declaration: any): declaration is Type; + export function isNgDef(declaration: any, ngType?: string): declaration is Type { const isModule = (!ngType || ngType === 'm') && isNgType(declaration, 'NgModule'); const isComponent = (!ngType || ngType === 'c') && isNgType(declaration, 'Component'); @@ -133,35 +156,63 @@ export function isNgDef(declaration: any, ngType?: string): declaration is Type< } /** - * Checks whether a class is a mock of a class that was decorated by a ng type. - * m - module. - * c - component. - * d - directive. - * p - pipe. + * Checks whether the declaration is a mocked one and derives from the specified module. + * + * @see https://github.com/ike18t/ng-mocks#ismockedngdefof */ export function isMockedNgDefOf(declaration: any, type: Type, ngType: 'm'): declaration is Type>; + +/** + * Checks whether the declaration is a mocked one and derives from the specified component. + * + * @see https://github.com/ike18t/ng-mocks#ismockedngdefof + */ export function isMockedNgDefOf( declaration: any, type: Type, ngType: 'c' ): declaration is Type>; + +/** + * Checks whether the declaration is a mocked one and derives from the specified directive. + * + * @see https://github.com/ike18t/ng-mocks#ismockedngdefof + */ export function isMockedNgDefOf( declaration: any, type: Type, ngType: 'd' ): declaration is Type>; + +/** + * Checks whether the declaration is a mocked one and derives from the specified pipe. + * + * @see https://github.com/ike18t/ng-mocks#ismockedngdefof + */ export function isMockedNgDefOf( declaration: any, type: Type, ngType: 'p' ): declaration is Type>; + +/** + * Checks whether the declaration is a mocked one and derives from the specified type. + * + * @see https://github.com/ike18t/ng-mocks#ismockedngdefof + */ export function isMockedNgDefOf(declaration: any, type: Type): declaration is Type; + export function isMockedNgDefOf(declaration: any, type: Type, ngType?: any): declaration is Type { return ( typeof declaration === 'function' && declaration.mockOf === type && (ngType ? isNgDef(declaration, ngType) : true) ); } +/** + * Checks whether a variable is a real token. + * + * @see https://github.com/ike18t/ng-mocks#isnginjectiontoken + */ export const isNgInjectionToken = (token: any): token is InjectionToken => typeof token === 'object' && token.ngMetadataName === 'InjectionToken'; @@ -170,21 +221,44 @@ export const isNgModuleDefWithProviders = (declaration: any): declaration is NgM declaration.ngModule !== undefined && isNgDef(declaration.ngModule, 'm'); /** - * Checks whether an object is an instance of a mocked class that was decorated by a ng type. - * m - module. - * c - component. - * d - directive. - * p - pipe. + * Checks whether the instance derives from a mocked module. + * + * @see https://github.com/ike18t/ng-mocks#ismockof */ export function isMockOf(instance: any, declaration: Type, ngType: 'm'): instance is MockedModule; + +/** + * Checks whether the instance derives from a mocked component. + * + * @see https://github.com/ike18t/ng-mocks#ismockof + */ export function isMockOf(instance: any, declaration: Type, ngType: 'c'): instance is MockedComponent; + +/** + * Checks whether the instance derives from a mocked directive. + * + * @see https://github.com/ike18t/ng-mocks#ismockof + */ export function isMockOf(instance: any, declaration: Type, ngType: 'd'): instance is MockedDirective; + +/** + * Checks whether the instance derives from a mocked pipe. + * + * @see https://github.com/ike18t/ng-mocks#ismockof + */ export function isMockOf( instance: any, declaration: Type, ngType: 'p' ): instance is MockedPipe; + +/** + * Checks whether the instance derives from a mocked type. + * + * @see https://github.com/ike18t/ng-mocks#ismockof + */ export function isMockOf(instance: any, declaration: Type): instance is T; + export function isMockOf(instance: any, declaration: Type, ngType?: any): instance is T { return ( typeof instance === 'object' && @@ -195,17 +269,40 @@ export function isMockOf(instance: any, declaration: Type, ngType?: any): } /** - * Returns a def of a mocked class based on another mock class or a source class that was decorated by a ng type. - * m - module. - * c - component. - * d - directive. - * p - pipe. + * Returns a def of a mocked module based on a mocked module or a source module. + * + * @see https://github.com/ike18t/ng-mocks#getmockedngdefof */ export function getMockedNgDefOf(declaration: Type, type: 'm'): Type>; + +/** + * Returns a def of a mocked component based on a mocked component or a source component. + * + * @see https://github.com/ike18t/ng-mocks#getmockedngdefof + */ export function getMockedNgDefOf(declaration: Type, type: 'c'): Type>; + +/** + * Returns a def of a mocked directive based on a mocked directive or a source directive. + * + * @see https://github.com/ike18t/ng-mocks#getmockedngdefof + */ export function getMockedNgDefOf(declaration: Type, type: 'd'): Type>; + +/** + * Returns a def of a mocked pipe based on a mocked pipe or a source pipe. + * + * @see https://github.com/ike18t/ng-mocks#getmockedngdefof + */ export function getMockedNgDefOf(declaration: Type, type: 'p'): Type>; + +/** + * Returns a def of a mocked class based on a mocked class or a source class decorated by a ng type. + * + * @see https://github.com/ike18t/ng-mocks#getmockedngdefof + */ export function getMockedNgDefOf(declaration: Type): Type; + export function getMockedNgDefOf(declaration: any, type?: any): any { const source = declaration.mockOf ? declaration.mockOf : declaration; const mocks = getTestBedInjection(NG_MOCKS); @@ -237,11 +334,46 @@ export function getMockedNgDefOf(declaration: any, type?: any): any { throw new Error(`There is no mock for ${source.name}`); } +/** + * Returns an original type. + * + * @see https://github.com/ike18t/ng-mocks#getsourceofmock + */ export function getSourceOfMock(declaration: Type>): Type; + +/** + * Returns an original type. + * + * @see https://github.com/ike18t/ng-mocks#getsourceofmock + */ export function getSourceOfMock(declaration: Type>): Type; + +/** + * Returns an original type. + * + * @see https://github.com/ike18t/ng-mocks#getsourceofmock + */ export function getSourceOfMock(declaration: Type>): Type; + +/** + * Returns an original type. + * + * @see https://github.com/ike18t/ng-mocks#getsourceofmock + */ export function getSourceOfMock(declaration: Type>): Type; + +/** + * Returns an original type. + * + * @see https://github.com/ike18t/ng-mocks#getsourceofmock + */ export function getSourceOfMock(declaration: Type): Type; + +/** + * Returns an original type. + * + * @see https://github.com/ike18t/ng-mocks#getsourceofmock + */ export function getSourceOfMock(declaration: any): Type { return typeof declaration === 'function' && declaration.mockOf ? declaration.mockOf : declaration; } diff --git a/lib/common/ng-mocks-universe.ts b/lib/common/ng-mocks-universe.ts index e449e69d66..034c3b3063 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 { AbstractType, Type } from './lib'; +import { AnyType } from './lib'; /** * Can be changed any time. @@ -14,5 +14,5 @@ export const ngMocksUniverse = { config: new Map(), flags: new Set(['cacheModule', 'cacheComponent', 'cacheDirective', 'cacheProvider']), global: new Map(), - touches: new Set | AbstractType | InjectionToken>(), + touches: new Set | InjectionToken>(), }; diff --git a/lib/mock-builder/mock-builder-promise.ts b/lib/mock-builder/mock-builder-promise.ts index 34fb9a8eec..27fa1de0cf 100644 --- a/lib/mock-builder/mock-builder-promise.ts +++ b/lib/mock-builder/mock-builder-promise.ts @@ -1,5 +1,7 @@ +import { DOCUMENT } from '@angular/common'; import { InjectionToken, NgModule, PipeTransform, Provider } from '@angular/core'; import { MetadataOverride, TestBed } from '@angular/core/testing'; +import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser'; import { AnyType, @@ -16,7 +18,7 @@ import { Type, } from '../common/lib'; import { ngMocksUniverse } from '../common/ng-mocks-universe'; -import { directiveResolver, ngModuleResolver } from '../common/reflect'; +import { directiveResolver, jitReflector, ngModuleResolver } from '../common/reflect'; import { MockComponent } from '../mock-component/mock-component'; import { MockDirective } from '../mock-directive/mock-directive'; import { MockModule, MockNgDef, MockProvider } from '../mock-module/mock-module'; @@ -102,6 +104,7 @@ export class MockBuilderPromise implements PromiseLike { 'correctModuleExports', ]); ngMocksUniverse.touches = new Set(); + ngMocksUniverse.config.set('multi', new Set()); for (const def of mapValues(this.keepDef)) { ngMocksUniverse.builder.set(def, def); @@ -209,6 +212,7 @@ export class MockBuilderPromise implements PromiseLike { continue; } if (isNgInjectionToken(def)) { + ngMocksUniverse.touches.add(def); continue; } providers.push(def); @@ -241,6 +245,78 @@ export class MockBuilderPromise implements PromiseLike { providers.push(provider); } + // Adding missed providers + const parameters = new Set(); + if (ngMocksUniverse.touches.size) { + const touchedDefs = mapValues(ngMocksUniverse.touches); + for (const def of touchedDefs) { + // Analyzing parameters. + for (const decorators of jitReflector.parameters(def)) { + if (!decorators) { + continue; + } + let provide: any; + for (const decorator of decorators) { + if (decorator && typeof decorator === 'object' && decorator.token) { + provide = decorator.token; + } + } + if (!provide && decorators[0]) { + provide = decorators[0]; + } + if (!provide) { + continue; + } + if (provide === DOCUMENT) { + continue; + } + if (provide === EVENT_MANAGER_PLUGINS) { + continue; + } + if (ngMocksUniverse.touches.has(provide)) { + continue; + } + + // 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'); + if (typeof provide === 'function' && skip) { + continue; + } + if (typeof provide === 'function' && touchedDefs.indexOf(provide) === -1) { + touchedDefs.push(provide); + } + parameters.add(provide); + } + } + } + + // Adding missed providers. + if (parameters.size) { + const parametersMap = new Map(); + const providersSkip = new Set(); + // Excluding manually provided values. + for (const provider of flatten(providers)) { + const provide = + typeof provider === 'object' && (provider as any).provide ? (provider as any).provide : provider; + providersSkip.add(provide); + } + for (const parameter of mapValues(parameters)) { + if (providersSkip.has(parameter)) { + continue; + } + + const mock = mockServiceHelper.resolveProvider(parameter, parametersMap); + if (mock) { + providers.push(mock); + } else if (isNgInjectionToken(parameter)) { + const multi = ngMocksUniverse.config.has('multi') && ngMocksUniverse.config.get('multi').has(parameter); + providers.push(mockServiceHelper.useFactory(parameter, () => (multi ? [] : undefined))); + } + } + } + const mocks = new Map(); for (const [key, value] of [ ...mapEntries(ngMocksUniverse.builder), @@ -373,6 +449,7 @@ export class MockBuilderPromise implements PromiseLike { config?: IMockBuilderConfig ): this; public mock(token: InjectionToken, mock: any, config: IMockBuilderConfig): this; + public mock(provider: AnyType, mock: AnyType, config: IMockBuilderConfig): this; public mock(provider: AnyType, mock: Partial, config: IMockBuilderConfig): this; public mock(provider: AnyType, mock: AnyType, config: IMockBuilderConfig): this; public mock(token: InjectionToken, mock?: any): this; diff --git a/lib/mock-builder/mock-builder.ts b/lib/mock-builder/mock-builder.ts index 7819f6fc6f..3f195de271 100644 --- a/lib/mock-builder/mock-builder.ts +++ b/lib/mock-builder/mock-builder.ts @@ -8,6 +8,9 @@ import { ngMocks } from '../mock-helper/mock-helper'; import { MockBuilderPerformance } from './mock-builder-performance'; import { MockBuilderPromise } from './mock-builder-promise'; +/** + * @see https://github.com/ike18t/ng-mocks#mockbuilder + */ export function MockBuilder( keepDeclaration?: AnyType | InjectionToken | null | undefined, itsModuleToMock?: AnyType | null | undefined diff --git a/lib/mock-component/mock-component.ts b/lib/mock-component/mock-component.ts index 6aca783d18..cee4ba5816 100644 --- a/lib/mock-component/mock-component.ts +++ b/lib/mock-component/mock-component.ts @@ -14,7 +14,7 @@ import { import { getTestBed } from '@angular/core/testing'; import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { AbstractType, flatten, getMockedNgDefOf, MockControlValueAccessor, MockOf, Type } from '../common'; +import { AnyType, flatten, getMockedNgDefOf, MockControlValueAccessor, MockOf, Type } from '../common'; import { decorateInputs, decorateOutputs, decorateQueries } from '../common/decorate'; import { ngMocksUniverse } from '../common/ng-mocks-universe'; import { directiveResolver } from '../common/reflect'; @@ -43,12 +43,11 @@ export function MockComponent( metaData: core.Directive ): Type>; +/** + * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-component + */ export function MockComponent( - component: Type, - metaData?: core.Directive -): Type>; -export function MockComponent( - component: AbstractType, + component: AnyType, metaData?: core.Directive ): Type>; export function MockComponent( diff --git a/lib/mock-directive/mock-directive.ts b/lib/mock-directive/mock-directive.ts index 5eb59326b0..32ae0078cd 100644 --- a/lib/mock-directive/mock-directive.ts +++ b/lib/mock-directive/mock-directive.ts @@ -13,7 +13,7 @@ import { import { getTestBed } from '@angular/core/testing'; import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { AbstractType, flatten, getMockedNgDefOf, MockControlValueAccessor, MockOf, Type } from '../common'; +import { AnyType, flatten, getMockedNgDefOf, MockControlValueAccessor, MockOf, Type } from '../common'; import { decorateInputs, decorateOutputs, decorateQueries } from '../common/decorate'; import { ngMocksUniverse } from '../common/ng-mocks-universe'; import { directiveResolver } from '../common/reflect'; @@ -41,8 +41,10 @@ export function MockDirectives(...directives: Array>): Array(directive: Type): Type>; -export function MockDirective(directive: AbstractType): Type>; +/** + * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-directive + */ +export function MockDirective(directive: AnyType): Type>; export function MockDirective(directive: Type): Type> { // We are inside of an 'it'. // It's fine to to return a mock or to throw an exception if it wasn't mocked in TestBed. diff --git a/lib/mock-helper/mock-helper.ts b/lib/mock-helper/mock-helper.ts index 53b5b576ad..ca0b459fce 100644 --- a/lib/mock-helper/mock-helper.ts +++ b/lib/mock-helper/mock-helper.ts @@ -89,7 +89,13 @@ export const MockHelper: { }, }; +/** + * @see https://github.com/ike18t/ng-mocks#ngmocks + */ export const ngMocks: { + /** + * @see https://github.com/ike18t/ng-mocks#making-angular-tests-faster + */ faster(): void; find(debugElement: MockedDebugElement | ComponentFixture, component: Type): MockedDebugElement; diff --git a/lib/mock-instance/mock-instance.ts b/lib/mock-instance/mock-instance.ts index 37061b3811..8fab18083b 100644 --- a/lib/mock-instance/mock-instance.ts +++ b/lib/mock-instance/mock-instance.ts @@ -3,6 +3,9 @@ import { Injector } from '@angular/core'; import { AbstractType, Type } from '../common'; import { ngMocksUniverse } from '../common/ng-mocks-universe'; +/** + * @see https://github.com/ike18t/ng-mocks#mockinstance + */ export function MockInstance( declaration: Type | AbstractType, config?: { diff --git a/lib/mock-module/mock-module.ts b/lib/mock-module/mock-module.ts index c9f2d1454e..c0590fc613 100644 --- a/lib/mock-module/mock-module.ts +++ b/lib/mock-module/mock-module.ts @@ -1,7 +1,8 @@ import { CommonModule } from '@angular/common'; import { core } from '@angular/compiler'; -import { ApplicationModule, NgModule, Provider } from '@angular/core'; +import { ApplicationModule, APP_INITIALIZER, NgModule, Provider } from '@angular/core'; import { getTestBed } from '@angular/core/testing'; +import { EVENT_MANAGER_PLUGINS, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; import { extendClass, @@ -24,14 +25,14 @@ import { MockService, mockServiceHelper } from '../mock-service'; export type MockedModule = T & Mock & {}; -const neverMockProvidedFunction = ['DomRendererFactory2', 'DomSharedStylesHost', 'EventManager', 'RendererFactory2']; -const neverMockToken = [ - // RouterModule - 'InjectionToken Application Initializer', - // BrowserModule - 'InjectionToken EventManagerPlugins', - 'InjectionToken HammerGestureConfig', +const neverMockProvidedFunction = [ + 'DomRendererFactory2', + 'DomSharedStylesHost', + 'EventManager', + 'Injector', + 'RendererFactory2', ]; +const neverMockToken = [APP_INITIALIZER, EVENT_MANAGER_PLUGINS, HAMMER_GESTURE_CONFIG]; /** * Can be changed any time. @@ -44,7 +45,7 @@ export function MockProvider(provider: any): Provider | undefined { if (typeof provide === 'function' && neverMockProvidedFunction.indexOf(provide.name) !== -1) { return provider; } - if (isNgInjectionToken(provide) && neverMockToken.indexOf(provide.toString()) !== -1) { + if (isNgInjectionToken(provide) && neverMockToken.indexOf(provide) !== -1) { return undefined; } @@ -101,6 +102,9 @@ export function MockProvider(provider: any): Provider | undefined { // If a testing module / component requires omitted tokens then they should be provided manually // during creation of TestBed module. if (provider.multi) { + if (ngMocksUniverse.config.has('multi')) { + (ngMocksUniverse.config.get('multi') as Set).add(provide); + } return undefined; } @@ -144,7 +148,14 @@ export function MockProvider(provider: any): Provider | undefined { return mockedProvider; } +/** + * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-module + */ export function MockModule(module: Type): Type; + +/** + * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-module + */ export function MockModule(module: NgModuleWithProviders): NgModuleWithProviders; export function MockModule(module: any): any { let ngModule: Type; diff --git a/lib/mock-pipe/mock-pipe.ts b/lib/mock-pipe/mock-pipe.ts index 3eb259a2e0..75763e4d1f 100644 --- a/lib/mock-pipe/mock-pipe.ts +++ b/lib/mock-pipe/mock-pipe.ts @@ -2,7 +2,7 @@ import { core } from '@angular/compiler'; import { Pipe, PipeTransform } from '@angular/core'; import { getTestBed } from '@angular/core/testing'; -import { AbstractType, getMockedNgDefOf, Mock, MockOf, Type } from '../common'; +import { AnyType, getMockedNgDefOf, Mock, MockOf, Type } from '../common'; import { ngMocksUniverse } from '../common/ng-mocks-universe'; import { pipeResolver } from '../common/reflect'; @@ -14,12 +14,11 @@ export function MockPipes(...pipes: Array>): Array undefined; +/** + * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-pipe + */ export function MockPipe( - pipe: Type, - transform?: TPipe['transform'] -): Type>; -export function MockPipe( - pipe: AbstractType, + pipe: AnyType, transform?: TPipe['transform'] ): Type>; export function MockPipe( diff --git a/lib/mock-render/mock-render.ts b/lib/mock-render/mock-render.ts index f44a2fc109..2d63503171 100644 --- a/lib/mock-render/mock-render.ts +++ b/lib/mock-render/mock-render.ts @@ -47,24 +47,38 @@ function solveOutput(output: any): string { return '=$event'; } +/** + * @see https://github.com/ike18t/ng-mocks#mockrender + */ function MockRender( template: Type, params: TComponent, detectChanges?: boolean | IMockRenderOptions ): MockedComponentFixture; -// without params we shouldn't autocomplete any keys of any types. +/** + * Without params we shouldn't autocomplete any keys of any types. + * + * @see https://github.com/ike18t/ng-mocks#mockrender + */ function MockRender>( template: Type ): MockedComponentFixture; +/** + * @see https://github.com/ike18t/ng-mocks#mockrender + */ function MockRender( template: string, params: TComponent, detectChanges?: boolean | IMockRenderOptions ): MockedComponentFixture; -// without params we shouldn't autocomplete any keys of any types. +/** + * Without params we shouldn't autocomplete any keys of any types. + * + * @see https://github.com/ike18t/ng-mocks#mockrender + */ function MockRender(template: string): MockedComponentFixture; function MockRender( diff --git a/lib/mock-service/mock-service.ts b/lib/mock-service/mock-service.ts index e05c78c01d..fc64b3eb26 100644 --- a/lib/mock-service/mock-service.ts +++ b/lib/mock-service/mock-service.ts @@ -1,3 +1,5 @@ +// tslint:disable:no-misused-new unified-signatures + import { Injector, Provider } from '@angular/core'; import { isNgInjectionToken, NG_GUARDS, NG_INTERCEPTORS } from '../common'; @@ -334,8 +336,6 @@ const mockServiceHelperPrototype = { } } - ngMocksUniverse.touches.add(provider); - // Then we check decisions whether we should keep or replace a def. if (!mockedDef && ngMocksUniverse.builder.has(provider)) { mockedDef = ngMocksUniverse.builder.get(provider); @@ -388,6 +388,12 @@ const mockServiceHelperPrototype = { if (changed && differs) { changed(true); } + + // Touching only when we really provide a value. + if (mockedDef) { + ngMocksUniverse.touches.add(provider); + } + return multi && typeof mockedDef === 'object' ? { ...mockedDef, multi } : mockedDef; }, @@ -431,9 +437,19 @@ export const mockServiceHelper: { useFactory(def: D, instance: () => I): Provider; } = getGlobal().ngMocksMockServiceHelper; +/** + * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-service + */ export function MockService(service?: boolean | number | string | null, mockNamePrefix?: string): undefined; + +/** + * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-service + */ export function MockService(service: new (...args: any[]) => T, mockNamePrefix?: string): T; -// tslint:disable-next-line:no-misused-new unified-signatures + +/** + * @see https://github.com/ike18t/ng-mocks#how-to-mock-a-service + */ export function MockService(service: object, mockNamePrefix?: string): T; export function MockService(service: any, mockNamePrefix?: string): any { // mocking all methods / properties of a class / object. diff --git a/package.json b/package.json index a9f6eeb738..abae0f9582 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,8 @@ "peerDependencies": { "@angular/compiler": ">=5.x <=11.x", "@angular/core": ">=5.x <=11.x", - "@angular/forms": ">=5.x <=11.x" + "@angular/forms": ">=5.x <=11.x", + "@angular/platform-browser": ">=5.x <=11.x" }, "devDependencies": { "@angular/animations": "^10.2.0", diff --git a/tests/module-with-factory-tokens/test.spec.ts b/tests/module-with-factory-tokens/test.spec.ts index 995bc2e767..f1790cefac 100644 --- a/tests/module-with-factory-tokens/test.spec.ts +++ b/tests/module-with-factory-tokens/test.spec.ts @@ -6,7 +6,7 @@ import { MY_TOKEN_MULTI, MY_TOKEN_SINGLE, TargetComponent, TargetModule } from ' // Because all tokens have factories the test should render them correctly. // There's no way to specify multi in a factory, so we don't get an array. describe('module-with-factory-tokens:real', () => { - beforeEach(() => MockBuilder().keep(TargetModule)); + beforeEach(() => MockBuilder().keep(TargetModule).keep(MY_TOKEN_SINGLE).keep(MY_TOKEN_MULTI)); it('renders all tokens', () => { if (parseInt(VERSION.major, 10) <= 5) { @@ -94,8 +94,10 @@ describe('module-with-factory-tokens:mock-2', () => { // the tokens will used as they are with their factories. // Unfortunately it's quite tough to guess which tokens we can keep, mocks or omit and now // a user is responsible to specify tokens for his mock. +// UPD 2020-10-28: it has been fixed. Now all missed tokens are added to the TestModuleMeta, +// therefore we have to keep them. describe('module-with-factory-tokens:mock-3', () => { - beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(MY_TOKEN_SINGLE).keep(MY_TOKEN_MULTI)); it('renders all tokens', () => { if (parseInt(VERSION.major, 10) <= 5) { diff --git a/tests/module-with-tokens/test.spec.ts b/tests/module-with-tokens/test.spec.ts index 19756b2b5b..ef2c362b9e 100644 --- a/tests/module-with-tokens/test.spec.ts +++ b/tests/module-with-tokens/test.spec.ts @@ -64,11 +64,17 @@ describe('module-with-tokens:mock-2', () => { // the tokens will be omitted from the final mock and injection will fail. // Unfortunately it's quite tough to guess which tokens we can keep, mocks or omit and now // a user is responsible to specify tokens for his mock. +// UPD 2020-10-28: it has been fixed. Now all missed tokens are added to the TestModuleMeta. describe('module-with-tokens:mock-3', () => { beforeEach(() => MockBuilder(TargetComponent, TargetModule)); - it('fails to render all tokens', () => { - expect(() => MockRender(TargetComponent)).toThrowError(/InjectionToken/); + it('does not fail to render all tokens', () => { + expect(() => MockRender(TargetComponent)).not.toThrowError(/InjectionToken/); + }); + + it('renders mocked tokens with respect of multi flag', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toContain('[]'); }); }); diff --git a/tests/replace-service-wherever/test.spec.ts b/tests/replace-service-wherever/test.spec.ts new file mode 100644 index 0000000000..b4e3fc2360 --- /dev/null +++ b/tests/replace-service-wherever/test.spec.ts @@ -0,0 +1,82 @@ +import { Component, Injectable, NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +@Injectable() +class TargetService { + public called = false; + public readonly name = 'target'; + + public echo(): string { + this.called = true; + return this.name; + } +} + +@Injectable() +class ReplacementService { + public called = false; + public readonly name = 'replacement'; + + public echo(): string { + this.called = true; + return this.name; + } +} + +@Component({ + selector: 'target', + template: `{{ service.name }} {{ service.called ? 'called' : '' }}`, +}) +class TargetComponent { + public readonly service: TargetService; + + constructor(service: TargetService) { + this.service = service; + } +} + +@NgModule({ + declarations: [TargetComponent], + exports: [TargetComponent], + providers: [TargetService], +}) +class TargetModule { + protected service: TargetService; + + constructor(service: TargetService) { + this.service = service; + this.service.echo(); + } +} + +describe('replace-service-wherever:real', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents() + ); + + it('uses service everywhere', () => { + const fixture = TestBed.createComponent(TargetComponent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain('target'); + expect(fixture.nativeElement.innerHTML).toContain('called'); + }); +}); + +describe('replace-service-wherever:mock', () => { + beforeEach(() => + MockBuilder(TargetComponent, TargetModule).provide({ + provide: TargetService, + useClass: ReplacementService, + }) + ); + + it('uses service everywhere', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toContain('replacement'); + // The module is mocked, its ctor does nothing. + expect(fixture.nativeElement.innerHTML).not.toContain('called'); + }); +}); diff --git a/tests/root-provider-with-root-dep/test.spec.ts b/tests/root-provider-with-root-dep/test.spec.ts new file mode 100644 index 0000000000..96926e52dd --- /dev/null +++ b/tests/root-provider-with-root-dep/test.spec.ts @@ -0,0 +1,80 @@ +import { Component, Inject, Injectable as InjectableSource, InjectionToken, NgModule, VERSION } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } 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); +} + +// Thanks A5. +const TOKEN = new (InjectionToken as any)('TOKEN', { + factory: () => 'token', +}); + +@Injectable({ + providedIn: 'root', +}) +class TargetService { + public readonly name: string; + public readonly name2: string; + public readonly name3: string; + + constructor(@Inject(TOKEN) name: string, @Inject(TOKEN) name2: string, @Inject(TOKEN) name3: string) { + this.name = name; + this.name2 = name2; + this.name3 = name3; + } +} + +@Component({ + selector: 'target', + template: ` "name:{{ service ? service.name : '' }}" `, +}) +class TargetComponent { + public readonly service: TargetService; + + constructor(service: TargetService) { + this.service = service; + } +} + +@NgModule({ + declarations: [TargetComponent], + exports: [TargetComponent], +}) +class TargetModule {} + +describe('root-provider-with-root-dep', () => { + beforeEach(() => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + } + }); + + describe('real', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents() + ); + + it('finds tokens', () => { + const fixture = TestBed.createComponent(TargetComponent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain('"name:token"'); + }); + }); + + describe('mock', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + + it('mocks service', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toContain('"name:"'); + // A nested token as a dependency should be mocked. + expect(TestBed.get(TOKEN)).toBeUndefined(); + }); + }); +}); diff --git a/tests/root-provider-with-string-dep/test.spec.ts b/tests/root-provider-with-string-dep/test.spec.ts new file mode 100644 index 0000000000..5261ca0297 --- /dev/null +++ b/tests/root-provider-with-string-dep/test.spec.ts @@ -0,0 +1,49 @@ +import { Component, Inject, NgModule, VERSION } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + +@Component({ + selector: 'target', + template: ` "name:{{ name }}" `, +}) +class TargetComponent { + public readonly name: string; + + constructor(@Inject('name') service: string) { + this.name = name; + } +} + +@NgModule({ + declarations: [TargetComponent], + exports: [TargetComponent], +}) +class TargetModule {} + +describe('root-provider-with-string-dep', () => { + beforeEach(() => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + } + }); + + describe('real', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents() + ); + + it('finds tokens', () => { + expect(() => TestBed.createComponent(TargetComponent)).toThrowError(/No provider for name!/); + }); + }); + + describe('mock', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + + it('mocks service', () => { + expect(() => MockRender(TargetComponent)).toThrowError(/No provider for name!/); + }); + }); +}); diff --git a/tests/root-providers/test.spec.ts b/tests/root-providers/test.spec.ts new file mode 100644 index 0000000000..c9d45b1243 --- /dev/null +++ b/tests/root-providers/test.spec.ts @@ -0,0 +1,228 @@ +// tslint:disable:ban-ts-ignore + +import { + Component, + Inject, + Injectable as InjectableSource, + InjectionToken, + Injector, + NgModule, + Optional, + SkipSelf, + VERSION, +} from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +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); +} + +// Thanks A5. +const TOKEN = new (InjectionToken as any)('TOKEN', { + factory: () => 'token', +}); + +@Injectable() +class ModuleService { + public readonly name = 'module'; +} + +@Injectable({ + providedIn: 'root', +}) +class TargetService { + public readonly name = 'service'; +} + +@Injectable({ + providedIn: 'root', +}) +class FakeService { + public readonly name = 'fake'; +} + +@Injectable({ + providedIn: 'any', +}) +class ProvidedService { + public readonly name = 'provided'; +} + +@Component({ + selector: 'target', + template: ` + "service:{{ service.name }}" "fake:{{ fake.name }}" "injected:{{ injected.name }}" "provided:{{ provided.name }}" + "token:{{ token }}" + `, +}) +class TargetComponent { + public readonly fake: TargetService; + public readonly injected: TargetService; + public readonly provided: ProvidedService; + public readonly service: TargetService; + public readonly token: string; + + constructor( + @Inject(FakeService) fake: TargetService, + @Optional() @Inject(TOKEN) @SkipSelf() token: string, + @Optional() @SkipSelf() service: TargetService, + @Inject(TOKEN) @Optional() @SkipSelf() token2: string, + provided: ProvidedService, + injector: Injector + ) { + this.fake = fake; + this.service = service; + this.injected = injector.get(TargetService); + this.provided = provided; + this.token = token; + } +} + +@Component({ + selector: 'module', + template: `{{ module.name }}`, +}) +class ModuleComponent { + public readonly module: ModuleService; + + constructor(module: ModuleService) { + this.module = module; + } +} + +@NgModule({ + declarations: [TargetComponent, ModuleComponent], + exports: [TargetComponent], + imports: [BrowserModule, BrowserAnimationsModule], + providers: [ProvidedService], +}) +class TargetModule {} + +describe('root-providers', () => { + beforeEach(() => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + } + }); + + describe('real', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents() + ); + + it('finds tokens', () => { + const fixture = TestBed.createComponent(TargetComponent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain('"service:service"'); + expect(fixture.nativeElement.innerHTML).toContain('"fake:fake"'); + expect(fixture.nativeElement.innerHTML).toContain('"injected:service"'); + expect(fixture.nativeElement.innerHTML).toContain('"provided:provided"'); + expect(fixture.nativeElement.innerHTML).toContain('"token:token"'); + }); + + it('fails', () => { + expect(() => TestBed.createComponent(ModuleComponent)).toThrowError(/-> ModuleService/); + }); + }); + + describe('mock', () => { + ngMocks.faster(); + + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(ModuleComponent)); + + it('mocks providers', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toContain('"service:"'); + expect(fixture.nativeElement.innerHTML).toContain('"fake:"'); + expect(fixture.nativeElement.innerHTML).toContain('"injected:"'); + expect(fixture.nativeElement.innerHTML).toContain('"provided:"'); + expect(fixture.nativeElement.innerHTML).toContain('"token:"'); + }); + + it('fails', () => { + expect(() => MockRender(ModuleComponent)).toThrowError(/-> ModuleService/); + }); + }); + + describe('mock as dependency', () => { + ngMocks.faster(); + + beforeEach(() => + MockBuilder(TargetComponent, TargetModule).mock(TargetService, TargetService, { + dependency: true, + }) + ); + + it('mocks providers', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toContain('"service:"'); + expect(fixture.nativeElement.innerHTML).toContain('"fake:"'); + expect(fixture.nativeElement.innerHTML).toContain('"injected:"'); + expect(fixture.nativeElement.innerHTML).toContain('"provided:"'); + expect(fixture.nativeElement.innerHTML).toContain('"token:"'); + }); + }); + + describe('keep', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule).keep(TargetService)); + + it('mocks providers', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toContain('"service:service"'); + expect(fixture.nativeElement.innerHTML).toContain('"fake:"'); + expect(fixture.nativeElement.innerHTML).toContain('"injected:service"'); + expect(fixture.nativeElement.innerHTML).toContain('"provided:"'); + expect(fixture.nativeElement.innerHTML).toContain('"token:"'); + }); + }); + + describe('keep via component module, but mocks root providers', () => { + beforeEach(() => MockBuilder(TargetModule)); + + it('mocks providers', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toContain('"service:"'); + expect(fixture.nativeElement.innerHTML).toContain('"fake:"'); + expect(fixture.nativeElement.innerHTML).toContain('"injected:"'); + expect(fixture.nativeElement.innerHTML).toContain('"provided:provided"'); // It is in the module. + expect(fixture.nativeElement.innerHTML).toContain('"token:"'); + }); + }); + + describe('keep via component module, and keeps root providers', () => { + beforeEach(() => MockBuilder(TargetModule).keep(TargetService).keep(TOKEN)); + + it('mocks providers', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toContain('"service:service"'); + expect(fixture.nativeElement.innerHTML).toContain('"fake:"'); + expect(fixture.nativeElement.innerHTML).toContain('"injected:service"'); + expect(fixture.nativeElement.innerHTML).toContain('"provided:provided"'); // It is in the module. + expect(fixture.nativeElement.innerHTML).toContain('"token:token"'); + }); + }); + + describe('keep as dependency', () => { + beforeEach(() => + MockBuilder(TargetComponent, TargetModule).keep(TargetService, { + dependency: true, + }) + ); + + it('mocks providers', () => { + const fixture = MockRender(TargetComponent); + expect(fixture.nativeElement.innerHTML).toContain('"service:service"'); + expect(fixture.nativeElement.innerHTML).toContain('"fake:"'); + expect(fixture.nativeElement.innerHTML).toContain('"injected:service"'); + expect(fixture.nativeElement.innerHTML).toContain('"provided:"'); + expect(fixture.nativeElement.innerHTML).toContain('"token:"'); + }); + }); +});