diff --git a/lib/common/decorate.inputs.ts b/lib/common/decorate.inputs.ts index b82a16568c..c0f128eafc 100644 --- a/lib/common/decorate.inputs.ts +++ b/lib/common/decorate.inputs.ts @@ -1,13 +1,13 @@ import { Input } from '@angular/core'; -import { Type } from './core.types'; +import { AnyType } from './core.types'; // Looks like an A9 bug, that queries from @Component aren't processed. // Also we have to pass prototype, not the class. // The same issue happens with outputs, but time to time // (when I restart tests with refreshing browser manually). // https://github.com/ike18t/ng-mocks/issues/109 -export default function (cls: Type, inputs?: string[], exclude?: string[]) { +export default function (cls: AnyType, inputs?: string[], exclude?: string[]) { /* istanbul ignore else */ if (inputs) { for (const input of inputs) { diff --git a/lib/common/decorate.outputs.ts b/lib/common/decorate.outputs.ts index 54d39225d3..2fd9d47cb2 100644 --- a/lib/common/decorate.outputs.ts +++ b/lib/common/decorate.outputs.ts @@ -1,13 +1,13 @@ import { Output } from '@angular/core'; -import { Type } from './core.types'; +import { AnyType } from './core.types'; // Looks like an A9 bug, that queries from @Component aren't processed. // Also we have to pass prototype, not the class. // The same issue happens with outputs, but time to time // (when I restart tests with refreshing browser manually). // https://github.com/ike18t/ng-mocks/issues/109 -export default function (cls: Type, outputs?: string[]) { +export default function (cls: AnyType, outputs?: string[]) { /* istanbul ignore else */ if (outputs) { for (const output of outputs) { diff --git a/lib/common/decorate.queries.ts b/lib/common/decorate.queries.ts index 127622f288..93d7183352 100644 --- a/lib/common/decorate.queries.ts +++ b/lib/common/decorate.queries.ts @@ -1,13 +1,13 @@ import { ContentChild, ContentChildren, Query, ViewChild, ViewChildren } from '@angular/core'; -import { Type } from './core.types'; +import { AnyType } from './core.types'; // Looks like an A9 bug, that queries from @Component aren't processed. // Also we have to pass prototype, not the class. // The same issue happens with outputs, but time to time // (when I restart tests with refreshing browser manually). // https://github.com/ike18t/ng-mocks/issues/109 -export default function (cls: Type, queries?: { [key: string]: Query }) { +export default function (cls: AnyType, queries?: { [key: string]: Query }) { /* istanbul ignore else */ if (queries) { for (const key of Object.keys(queries)) { diff --git a/lib/common/mock-of.ts b/lib/common/mock-of.ts index 11c7e7da6d..80d6f52bb8 100644 --- a/lib/common/mock-of.ts +++ b/lib/common/mock-of.ts @@ -1,6 +1,6 @@ /* tslint:disable variable-name */ -import { Type } from './core.types'; +import { AnyType } from './core.types'; import { ngMocksMockConfig } from './mock'; // This helps with debugging in the browser. Decorating mock classes with this @@ -9,7 +9,7 @@ import { ngMocksMockConfig } from './mock'; // by name (which will now include the original class' name. // Additionally, if we set breakpoints, we can inspect the actual class being // replaced with a mock copy by looking into the 'mockOf' property on the class. -export const MockOf = (mockClass: Type, config?: ngMocksMockConfig) => (constructor: Type) => { +export const MockOf = (mockClass: AnyType, config?: ngMocksMockConfig) => (constructor: AnyType) => { Object.defineProperties(constructor, { mockOf: { value: mockClass }, name: { value: `MockOf${mockClass.name}` }, diff --git a/lib/mock-builder/mock-builder-promise.ts b/lib/mock-builder/mock-builder-promise.ts index 5a63622d8f..56d35033b6 100644 --- a/lib/mock-builder/mock-builder-promise.ts +++ b/lib/mock-builder/mock-builder-promise.ts @@ -20,6 +20,7 @@ import { MockService } from '../mock-service/mock-service'; import extractDep from './mock-builder-promise.extract-dep'; import skipDep from './mock-builder-promise.skip-dep'; +import { MockBuilderStash } from './mock-builder-stash'; import { IMockBuilder, IMockBuilderConfig, IMockBuilderResult } from './types'; const defaultMock = {}; // simulating Symbol @@ -35,6 +36,7 @@ export class MockBuilderPromise implements IMockBuilder { protected mockDef: Set | InjectionToken> = new Set(); protected providerDef: Map | InjectionToken, Provider> = new Map(); protected replaceDef: Set | InjectionToken> = new Set(); + protected stash: MockBuilderStash = new MockBuilderStash(); public beforeCompileComponents(callback: (testBed: typeof TestBed) => void): this { this.beforeCC.add(callback); @@ -43,30 +45,9 @@ export class MockBuilderPromise implements IMockBuilder { } public build(): NgModule { - const backup = { - builtDeclarations: ngMocksUniverse.builtDeclarations, - builtProviders: ngMocksUniverse.builtProviders, - cacheDeclarations: ngMocksUniverse.cacheDeclarations, - cacheProviders: ngMocksUniverse.cacheProviders, - config: ngMocksUniverse.config, - flags: ngMocksUniverse.flags, - touches: ngMocksUniverse.touches, - }; + this.stash.backup(); + ngMocksUniverse.flags.add('cachePipe'); - ngMocksUniverse.builtDeclarations = new Map(); - ngMocksUniverse.builtProviders = new Map(); - ngMocksUniverse.cacheDeclarations = new Map(); - ngMocksUniverse.cacheProviders = new Map(); - ngMocksUniverse.config = new Map(); - ngMocksUniverse.flags = new Set([ - 'cacheComponent', - 'cacheDirective', - 'cacheModule', - 'cachePipe', - 'cacheProvider', - 'correctModuleExports', - ]); - ngMocksUniverse.touches = new Set(); ngMocksUniverse.config.set('multi', new Set()); // collecting multi flags of providers. ngMocksUniverse.config.set('deps', new Set()); // collecting all deps of providers. ngMocksUniverse.config.set('depsSkip', new Set()); // collecting all declarations of kept modules. @@ -387,9 +368,7 @@ export class MockBuilderPromise implements IMockBuilder { overrides.set(value, override); } - for (const key of Object.keys(backup)) { - ngMocksUniverse[key] = (backup as any)[key]; - } + this.stash.restore(); return { declarations, diff --git a/lib/mock-builder/mock-builder-stash.ts b/lib/mock-builder/mock-builder-stash.ts new file mode 100644 index 0000000000..177d91dd8a --- /dev/null +++ b/lib/mock-builder/mock-builder-stash.ts @@ -0,0 +1,32 @@ +import config from '../common/core.config'; +import ngMocksUniverse from '../common/ng-mocks-universe'; + +export class MockBuilderStash { + protected data: Record = {}; + + public backup(): void { + this.data = { + builtDeclarations: ngMocksUniverse.builtDeclarations, + builtProviders: ngMocksUniverse.builtProviders, + cacheDeclarations: ngMocksUniverse.cacheDeclarations, + cacheProviders: ngMocksUniverse.cacheProviders, + config: ngMocksUniverse.config, + flags: ngMocksUniverse.flags, + touches: ngMocksUniverse.touches, + }; + + ngMocksUniverse.builtDeclarations = new Map(); + ngMocksUniverse.builtProviders = new Map(); + ngMocksUniverse.cacheDeclarations = new Map(); + ngMocksUniverse.cacheProviders = new Map(); + ngMocksUniverse.config = new Map(); + ngMocksUniverse.flags = new Set(config.flags); + ngMocksUniverse.touches = new Set(); + } + + public restore(): void { + for (const key of Object.keys(this.data)) { + ngMocksUniverse[key] = (this.data as any)[key]; + } + } +} diff --git a/lib/mock-component/mock-component.ts b/lib/mock-component/mock-component.ts index 893020ef3e..06796e5624 100644 --- a/lib/mock-component/mock-component.ts +++ b/lib/mock-component/mock-component.ts @@ -3,28 +3,20 @@ import { AfterContentInit, ChangeDetectorRef, Component, - forwardRef, Injector, - Provider, Query, TemplateRef, ViewChild, ViewContainerRef, } from '@angular/core'; import { getTestBed } from '@angular/core/testing'; -import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { flatten } from '../common/core.helpers'; import { directiveResolver } from '../common/core.reflect'; import { Type } from '../common/core.types'; -import decorateInputs from '../common/decorate.inputs'; -import decorateOutputs from '../common/decorate.outputs'; -import decorateQueries from '../common/decorate.queries'; import { getMockedNgDefOf } from '../common/func.get-mocked-ng-def-of'; import { MockControlValueAccessor } from '../common/mock-control-value-accessor'; -import { MockOf } from '../common/mock-of'; import ngMocksUniverse from '../common/ng-mocks-universe'; -import mockServiceHelper from '../mock-service/helper'; +import decorateDeclaration from '../mock/decorate-declaration'; import { MockedComponent } from './types'; @@ -89,73 +81,8 @@ export function MockComponent(component: Type): Type { - const value: Type & { __ngMocksSkip?: boolean } = forwardRef(() => ComponentMock); - value.__ngMocksSkip = true; - - return value; - })(), - }, - ], - selector, - template, - }; - - const resolutions = new Map(); - const resolveProvider = (def: Provider) => mockServiceHelper.resolveProvider(def, resolutions); - - let setNgValueAccessor: undefined | boolean; - for (const providerDef of flatten(providers || [])) { - const provide = - providerDef && typeof providerDef === 'object' && providerDef.provide ? providerDef.provide : providerDef; - if (options.providers && provide === NG_VALIDATORS) { - options.providers.push({ - multi: true, - provide, - useExisting: (() => { - const value: Type & { __ngMocksSkip?: boolean } = forwardRef(() => ComponentMock); - value.__ngMocksSkip = true; - - return value; - })(), - }); - continue; - } - if (setNgValueAccessor === undefined && options.providers && provide === NG_VALUE_ACCESSOR) { - setNgValueAccessor = false; - options.providers.push({ - multi: true, - provide, - useExisting: (() => { - const value: Type & { __ngMocksSkip?: boolean } = forwardRef(() => ComponentMock); - value.__ngMocksSkip = true; - - return value; - })(), - }); - continue; - } - - const mock = resolveProvider(providerDef); - /* istanbul ignore else */ - if (options.providers && mock) { - options.providers.push(mock); - } - } - if (setNgValueAccessor === undefined) { - setNgValueAccessor = - mockServiceHelper.extractMethodsFromPrototype(component.prototype).indexOf('writeValue') !== -1; - } - const config = ngMocksUniverse.config.get(component); - @Component(options) - @MockOf(component, { outputs, setNgValueAccessor }) class ComponentMock extends MockControlValueAccessor implements AfterContentInit { /* istanbul ignore next */ public constructor(changeDetector: ChangeDetectorRef, injector: Injector) { @@ -208,13 +135,21 @@ export function MockComponent(component: Type): Type>): Array(directive: Type): Type>; export function MockDirective(directive: Type): Type> { // We are inside of an 'it'. // It's fine to to return a mock copy or to throw an exception if it wasn't replaced with its mock copy in TestBed. @@ -58,72 +41,8 @@ export function MockDirective(directive: Type): Type { - const value: Type & { __ngMocksSkip?: boolean } = forwardRef(() => DirectiveMock); - value.__ngMocksSkip = true; - - return value; - })(), - }, - ], - selector, - }; - - const resolutions = new Map(); - const resolveProvider = (def: Provider) => mockServiceHelper.resolveProvider(def, resolutions); - - let setNgValueAccessor: undefined | boolean; - for (const providerDef of flatten(providers || [])) { - const provide = - providerDef && typeof providerDef === 'object' && providerDef.provide ? providerDef.provide : providerDef; - if (options.providers && provide === NG_VALIDATORS) { - options.providers.push({ - multi: true, - provide, - useExisting: (() => { - const value: Type & { __ngMocksSkip?: boolean } = forwardRef(() => DirectiveMock); - value.__ngMocksSkip = true; - - return value; - })(), - }); - continue; - } - if (setNgValueAccessor === undefined && options.providers && provide === NG_VALUE_ACCESSOR) { - setNgValueAccessor = false; - options.providers.push({ - multi: true, - provide, - useExisting: (() => { - const value: Type & { __ngMocksSkip?: boolean } = forwardRef(() => DirectiveMock); - value.__ngMocksSkip = true; - - return value; - })(), - }); - continue; - } - - const mock = resolveProvider(providerDef); - /* istanbul ignore else */ - if (options.providers && mock) { - options.providers.push(mock); - } - } - if (setNgValueAccessor === undefined) { - setNgValueAccessor = - mockServiceHelper.extractMethodsFromPrototype(directive.prototype).indexOf('writeValue') !== -1; - } - const config = ngMocksUniverse.config.get(directive); - @Directive(options) - @MockOf(directive, { outputs, setNgValueAccessor }) class DirectiveMock extends MockControlValueAccessor implements OnInit { /* istanbul ignore next */ public constructor( @@ -170,13 +89,25 @@ export function MockDirective(directive: Type): Type, changed?: (flag: boolean) if (!isNgInjectionToken(provider) || def !== mockDef) { resolutions.set(provider, mockDef); } - let differs = false; + let providerDiffers = false; + let defDiffers = !mockDef; + if (def && mockDef) { + defDiffers = defDiffers || def.provide !== mockDef.provide; + defDiffers = defDiffers || def.useValue !== mockDef.useValue; + defDiffers = defDiffers || def.useClass !== mockDef.useClass; + defDiffers = defDiffers || def.useExisting !== mockDef.useExisting; + defDiffers = defDiffers || def.useFactory !== mockDef.useFactory; + defDiffers = defDiffers || def.deps !== mockDef.deps; + } if (def === provider && mockDef !== def) { - differs = true; - } else if ( - def !== provider && - (!mockDef || - def.provide !== mockDef.provide || - def.useValue !== mockDef.useValue || - def.useClass !== mockDef.useClass || - def.useExisting !== mockDef.useExisting || - def.useFactory !== mockDef.useFactory || - def.deps !== mockDef.deps) - ) { - differs = true; + providerDiffers = true; + } else if (def !== provider && defDiffers) { + providerDiffers = true; } - if (changed && differs) { + if (changed && providerDiffers) { changed(true); } diff --git a/lib/mock/clone-providers.ts b/lib/mock/clone-providers.ts new file mode 100644 index 0000000000..f115bfd740 --- /dev/null +++ b/lib/mock/clone-providers.ts @@ -0,0 +1,36 @@ +import { Provider } from '@angular/core'; +import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms'; + +import { flatten } from '../common/core.helpers'; +import { AnyType } from '../common/core.types'; +import mockServiceHelper from '../mock-service/helper'; + +import toExistingProvider from './to-existing-provider'; + +export default (mockType: AnyType, providers?: any[]): { providers: Provider[]; setNgValueAccessor?: boolean } => { + const result: Provider[] = []; + let setNgValueAccessor: boolean | undefined; + const resolutions = new Map(); + + for (const providerDef of flatten(providers || [])) { + const provide = + providerDef && typeof providerDef === 'object' && providerDef.provide ? providerDef.provide : providerDef; + if (provide === NG_VALIDATORS) { + result.push(toExistingProvider(provide, mockType, true)); + continue; + } + if (setNgValueAccessor === undefined && provide === NG_VALUE_ACCESSOR) { + setNgValueAccessor = false; + result.push(toExistingProvider(provide, mockType, true)); + continue; + } + + const mock = mockServiceHelper.resolveProvider(providerDef, resolutions); + result.push(mock); + } + + return { + providers: result, + setNgValueAccessor, + }; +}; diff --git a/lib/mock/decorate-declaration.ts b/lib/mock/decorate-declaration.ts new file mode 100644 index 0000000000..70679d45c4 --- /dev/null +++ b/lib/mock/decorate-declaration.ts @@ -0,0 +1,47 @@ +import { Component, Directive, Provider, ViewChild } from '@angular/core'; + +import { AnyType } from '../common/core.types'; +import decorateInputs from '../common/decorate.inputs'; +import decorateOutputs from '../common/decorate.outputs'; +import decorateQueries from '../common/decorate.queries'; +import { MockOf } from '../common/mock-of'; +import mockServiceHelper from '../mock-service/helper'; +import cloneProviders from '../mock/clone-providers'; +import toExistingProvider from '../mock/to-existing-provider'; + +export default ( + source: AnyType, + mock: AnyType, + meta: { + inputs?: string[]; + outputs?: string[]; + providers?: Provider[]; + queries?: Record; + }, + params: T, +): T => { + const data = cloneProviders(mock, meta.providers || []); + const providers = [toExistingProvider(source, mock), ...data.providers]; + const options: T = { + ...params, + providers, + }; + + if (data.setNgValueAccessor === undefined) { + data.setNgValueAccessor = + mockServiceHelper.extractMethodsFromPrototype(source.prototype).indexOf('writeValue') !== -1; + } + MockOf(source, { + outputs: meta.outputs, + setNgValueAccessor: data.setNgValueAccessor, + })(mock); + + /* istanbul ignore else */ + if (meta.queries) { + decorateInputs(mock, meta.inputs, Object.keys(meta.queries)); + } + decorateOutputs(mock, meta.outputs); + decorateQueries(mock, meta.queries); + + return options; +}; diff --git a/lib/mock/to-existing-provider.ts b/lib/mock/to-existing-provider.ts new file mode 100644 index 0000000000..84a7dd2207 --- /dev/null +++ b/lib/mock/to-existing-provider.ts @@ -0,0 +1,14 @@ +import { forwardRef } from '@angular/core'; + +import { AnyType, Type } from '../common/core.types'; + +export default (provide: AnyType, type: AnyType, multi = false) => ({ + ...(multi ? { multi } : {}), + provide, + useExisting: (() => { + const value: Type & { __ngMocksSkip?: boolean } = forwardRef(() => type); + value.__ngMocksSkip = true; + + return value; + })(), +});