From 9415f571e0afd400495b9a6cb2f23c0c6ba281ce Mon Sep 17 00:00:00 2001 From: satanTime Date: Sat, 19 Mar 2022 19:30:42 +0100 Subject: [PATCH] feat(ngMocks.defaultConfig): config for MockBuilder #971 --- docs/articles/api/MockBuilder.md | 35 ++-- docs/articles/api/ngMocks/defaultConfig.md | 42 +++++ docs/sidebars.js | 1 + libs/ng-mocks/src/lib/common/decorate.mock.ts | 12 +- .../src/lib/common/ng-mocks-universe.ts | 4 + .../mock-builder/mock-builder.performance.ts | 5 +- .../lib/mock-builder/mock-builder.promise.ts | 6 +- .../lib/mock-builder/promise/init-universe.ts | 5 +- libs/ng-mocks/src/lib/mock-builder/types.ts | 6 +- .../mock-helper/mock-helper.default-config.ts | 20 +++ .../src/lib/mock-helper/mock-helper.object.ts | 2 + .../src/lib/mock-helper/mock-helper.ts | 15 +- package-lock.json | 162 ++++++++++++++++++ package.json | 1 + tests-e2e/package.json | 3 +- tests-e2e/src/issue-971/test.spec.ts | 68 ++++++++ tests/issue-971/test.spec.ts | 112 ++++++++++++ 17 files changed, 473 insertions(+), 26 deletions(-) create mode 100644 docs/articles/api/ngMocks/defaultConfig.md create mode 100644 libs/ng-mocks/src/lib/mock-helper/mock-helper.default-config.ts create mode 100644 tests-e2e/src/issue-971/test.spec.ts create mode 100644 tests/issue-971/test.spec.ts diff --git a/docs/articles/api/MockBuilder.md b/docs/articles/api/MockBuilder.md index 01f9001fc3..c2080d67b7 100644 --- a/docs/articles/api/MockBuilder.md +++ b/docs/articles/api/MockBuilder.md @@ -101,7 +101,9 @@ beforeEach(() => { }); ``` -## .keep() +## Chain functions + +### .keep() If we want to keep a module, component, directive, pipe or provider as it is. We should use `.keep`. @@ -119,7 +121,7 @@ beforeEach(() => { }); ``` -## .mock() +### .mock() If we want to turn anything into a mock object, even a part of a kept module we should use `.mock`. @@ -157,7 +159,7 @@ beforeEach(() => { }); ``` -## .exclude() +### .exclude() If we want to exclude something, even a part of a kept module we should use `.exclude`. @@ -173,7 +175,7 @@ beforeEach(() => { }); ``` -## .replace() +### .replace() If we want to replace something with something, we should use `.replace`. The replacement has to be decorated with the same decorator as the source. @@ -208,7 +210,7 @@ beforeEach(() => { }); ``` -## .provide() +### .provide() If we want to add or replace providers / services, we should use `.provide`. It has the same interface as a regular provider. @@ -222,7 +224,12 @@ beforeEach(() => { }); ``` -## `precise` flag +## Config + +You can customize default behavior of mock things. +Also, it can be done globally via [`ngMocks.defaultConfig()`](./ngMocks/defaultConfig.md) to avoid repetitions. + +### `precise` flag By default, when [`.mock(Service, mock)`](#mock) is used it creates a mock object via [`MockService(Service, mock)`](MockService.md). @@ -253,7 +260,7 @@ beforeEach(() => { }); ``` -## `export` flag +### `export` flag If we want to test a component, directive or pipe which, unfortunately, has not been exported, then we need to mark it with the `export` flag. @@ -271,7 +278,7 @@ beforeEach(() => { }); ``` -## `exportAll` flag +### `exportAll` flag If we want to use all the declarations of a module which have not been exported, we need to mark the module with the `exportAll` flag. Then all its imports and declarations will be exported. @@ -290,7 +297,7 @@ beforeEach(() => { }); ``` -## `dependency` flag +### `dependency` flag By default, all definitions are added to the `TestingModule` if they are not a dependency of another definition. Modules are added as imports to the `TestingModule`. @@ -324,7 +331,7 @@ beforeEach(() => { }); ``` -## `render` flag +### `render` flag When we want to render a structural directive by default, we can do that via adding the `render` flag in its config. @@ -365,7 +372,9 @@ beforeEach(() => { }); ``` -## `NG_MOCKS_GUARDS` token +## Tokens + +### `NG_MOCKS_GUARDS` token If we want to test guards, we need to [`.keep`](#keep) them, but what should we do with other guards we do not want to care about at all? The answer is to exclude `NG_MOCKS_GUARDS` token, it will **remove all the guards** from routes except the explicitly configured ones. @@ -377,7 +386,7 @@ beforeEach(() => { }); ``` -## `NG_MOCKS_INTERCEPTORS` token +### `NG_MOCKS_INTERCEPTORS` token Usually, when we want to test an interceptor, we want to avoid influences of other interceptors. To **remove all interceptors in an angular test** we need to exclude `NG_MOCKS_INTERCEPTORS` token, @@ -390,7 +399,7 @@ beforeEach(() => { }); ``` -## `NG_MOCKS_ROOT_PROVIDERS` token +### `NG_MOCKS_ROOT_PROVIDERS` token There are root services and tokens apart from provided ones in Angular applications. It might happen that in a test we want these providers to be replaced with their mocks or to be kept. diff --git a/docs/articles/api/ngMocks/defaultConfig.md b/docs/articles/api/ngMocks/defaultConfig.md new file mode 100644 index 0000000000..7ef21b1920 --- /dev/null +++ b/docs/articles/api/ngMocks/defaultConfig.md @@ -0,0 +1,42 @@ +--- +title: ngMocks.defaultConfig +description: Documentation about ngMocks.defaultConfig from ng-mocks library +--- + +Sets default config for mocks in [`MockBuilder`](../MockBuilder.md#config). + +- `ngMocks.defaultConfig( Component, config )` - sets a default config for a component +- `ngMocks.defaultConfig( Directive, config )` - sets a default config for a directive +- `ngMocks.defaultConfig( Component )` - removes config +- `ngMocks.defaultConfig( Directive )` - removes config + +The best place to do that is in `src/test.ts` for jasmine or in `src/setup-jest.ts` / `src/test-setup.ts` for `jest`. + +For example, if you have a simple structural directive which you always want to render it. +Then, you can configure it via `ngMocks.defaultConfig`. + +```ts title="src/test.ts" +// Config for a structural directive. +ngMocks.defaultConfig(StructuralDirective, { + // render the mock of the directive by default + render: true, +}); + +// Config for a component with content views. +ngMocks.defaultConfig(ViewComponent, { + render: { + // render a block by default + block1: true, + + // render a block with context + block2: { + $implicit: { + data: 'MOCK_DATA', + }, + }, + }, +}); + +// removing the config. +ngMocks.defaultMock(StructuralDirective); +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index ae21b2b24c..f4e51c0a26 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -30,6 +30,7 @@ module.exports = { label: 'ngMocks', items: [ 'api/ngMocks', + 'api/ngMocks/defaultConfig', 'api/ngMocks/defaultMock', 'api/ngMocks/globalExclude', 'api/ngMocks/globalKeep', diff --git a/libs/ng-mocks/src/lib/common/decorate.mock.ts b/libs/ng-mocks/src/lib/common/decorate.mock.ts index e2087835d9..8cdafe69d0 100644 --- a/libs/ng-mocks/src/lib/common/decorate.mock.ts +++ b/libs/ng-mocks/src/lib/common/decorate.mock.ts @@ -1,10 +1,20 @@ import coreDefineProperty from './core.define-property'; import { AnyType } from './core.types'; import { ngMocksMockConfig } from './mock'; +import ngMocksUniverse from './ng-mocks-universe'; -export default function (mock: AnyType, source: AnyType, config: ngMocksMockConfig = {}): void { +export default function (mock: AnyType, source: AnyType, configInput: ngMocksMockConfig = {}): void { coreDefineProperty(mock, 'mockOf', source); coreDefineProperty(mock, 'nameConstructor', mock.name); coreDefineProperty(mock, 'name', `MockOf${source.name}`, true); + const config = ngMocksUniverse.getConfigMock().has(source) + ? { + ...configInput, + config: { + ...ngMocksUniverse.getConfigMock().get(source), + ...configInput.config, + }, + } + : configInput; coreDefineProperty(mock.prototype, '__ngMocksConfig', config); } diff --git a/libs/ng-mocks/src/lib/common/ng-mocks-universe.ts b/libs/ng-mocks/src/lib/common/ng-mocks-universe.ts index cc57dcd862..cb4102e188 100644 --- a/libs/ng-mocks/src/lib/common/ng-mocks-universe.ts +++ b/libs/ng-mocks/src/lib/common/ng-mocks-universe.ts @@ -1,5 +1,7 @@ import { InjectionToken } from '@angular/core'; +import { IMockBuilderConfig } from '../mock-builder/types'; + import coreConfig from './core.config'; import { AnyType } from './core.types'; @@ -23,6 +25,7 @@ interface NgMocksUniverse { configInstance: Map; flags: Set; getBuildDeclaration: (def: any) => any | undefined; + getConfigMock: () => Map; getDefaults: () => Map; getLocalMocks: () => Array<[any, any]>; getOverrides: () => Map; @@ -56,6 +59,7 @@ ngMocksUniverse.global.set('flags', { ngMocksUniverse.getOverrides = globalMap('overrides'); ngMocksUniverse.getDefaults = globalMap('defaults'); +ngMocksUniverse.getConfigMock = globalMap('configMock'); const getDefaults = (def: any): [] | ['mock' | 'keep' | 'replace' | 'exclude', any?] => { { diff --git a/libs/ng-mocks/src/lib/mock-builder/mock-builder.performance.ts b/libs/ng-mocks/src/lib/mock-builder/mock-builder.performance.ts index 4def5249df..4abbc2e9a3 100644 --- a/libs/ng-mocks/src/lib/mock-builder/mock-builder.performance.ts +++ b/libs/ng-mocks/src/lib/mock-builder/mock-builder.performance.ts @@ -1,5 +1,4 @@ -import { NgModule } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import { TestBed, TestModuleMetadata } from '@angular/core/testing'; import ngMocksUniverse from '../common/ng-mocks-universe'; @@ -15,7 +14,7 @@ import requiredMetadata from './performance/required-metadata'; import { IMockBuilderResult } from './types'; export class MockBuilderPerformance extends MockBuilderPromise { - public build(): NgModule { + public build(): TestModuleMetadata { const global = ngMocksUniverse.global; // avoiding influences on cache when users extend the testing module. diff --git a/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts b/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts index 3626f47c20..e146fdf446 100644 --- a/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts +++ b/libs/ng-mocks/src/lib/mock-builder/mock-builder.promise.ts @@ -1,5 +1,5 @@ -import { NgModule, Provider } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import { Provider } from '@angular/core'; +import { TestBed, TestModuleMetadata } from '@angular/core/testing'; import { flatten, mapValues } from '../common/core.helpers'; import { Type } from '../common/core.types'; @@ -70,7 +70,7 @@ export class MockBuilderPromise implements IMockBuilder { return this; } - public build(): NgModule { + public build(): TestModuleMetadata { this.stash.backup(); ngMocksUniverse.config.set('mockNgDefResolver', new Map()); diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts index f15279c2b9..aeb15a4345 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-universe.ts @@ -28,7 +28,10 @@ export default ({ // flags to understand how to mock nested declarations. ngMocksUniverse.config.set('ngMocksDepsResolution', new Map()); for (const [k, v] of mapEntries(configDef)) { - ngMocksUniverse.config.set(k, v); + ngMocksUniverse.config.set(k, { + ...ngMocksUniverse.getConfigMock().get(k), + ...v, + }); } initKeepDef(keepDef); initReplaceDef(replaceDef, defValue); diff --git a/libs/ng-mocks/src/lib/mock-builder/types.ts b/libs/ng-mocks/src/lib/mock-builder/types.ts index d831d5c8fe..897aa8eeb4 100644 --- a/libs/ng-mocks/src/lib/mock-builder/types.ts +++ b/libs/ng-mocks/src/lib/mock-builder/types.ts @@ -1,5 +1,5 @@ -import { InjectionToken, NgModule, PipeTransform, Provider } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; +import { InjectionToken, PipeTransform, Provider } from '@angular/core'; +import { TestBed, TestModuleMetadata } from '@angular/core/testing'; import { AnyType } from '../common/core.types'; @@ -70,7 +70,7 @@ export interface IMockBuilder extends Promise { /** * @see https://ng-mocks.sudo.eu/api/MockBuilder#factory-function */ - build(): NgModule; + build(): TestModuleMetadata; /** * @see https://ng-mocks.sudo.eu/api/MockBuilder#exclude diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.default-config.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.default-config.ts new file mode 100644 index 0000000000..45198a64fd --- /dev/null +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.default-config.ts @@ -0,0 +1,20 @@ +import { InjectionToken } from '@angular/core'; + +import { flatten } from '../common/core.helpers'; +import { AnyType } from '../common/core.types'; +import ngMocksUniverse from '../common/ng-mocks-universe'; +import { IMockBuilderConfig } from '../mock-builder/types'; + +export default ( + def: AnyType | InjectionToken | string | Array | InjectionToken | string>, + config?: IMockBuilderConfig, +): void => { + const map = ngMocksUniverse.getConfigMock(); + for (const item of flatten(def)) { + if (config) { + map.set(item, config); + } else { + map.delete(item); + } + } +}; diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.object.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.object.ts index 44e18005df..854625c737 100644 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.object.ts +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.object.ts @@ -18,6 +18,7 @@ import mockHelperFormatText from './format/mock-helper.format-text'; import mockHelperAutoSpy from './mock-helper.auto-spy'; import mockHelperConsoleIgnore from './mock-helper.console-ignore'; import mockHelperConsoleThrow from './mock-helper.console-throw'; +import mockHelperDefaultConfig from './mock-helper.default-config'; import mockHelperDefaultMock from './mock-helper.default-mock'; import mockHelperFaster from './mock-helper.faster'; import mockHelperFlushTestBed from './mock-helper.flush-test-bed'; @@ -62,6 +63,7 @@ export default { } }, crawl: mockHelperCrawl, + defaultConfig: mockHelperDefaultConfig, defaultMock: mockHelperDefaultMock, event: mockHelperEvent, faster: mockHelperFaster, diff --git a/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts b/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts index 57dcbf8968..3cdfdddddb 100644 --- a/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts +++ b/libs/ng-mocks/src/lib/mock-helper/mock-helper.ts @@ -5,6 +5,7 @@ import { ComponentFixture, TestModuleMetadata } from '@angular/core/testing'; import { AnyType, DebugNodeSelector, Type } from '../common/core.types'; import { NgModuleWithProviders } from '../common/func.is-ng-module-def-with-providers'; +import { IMockBuilderConfig } from '../mock-builder/types'; import { MockedDebugElement, MockedDebugNode } from '../mock-render/types'; import { CustomMockFunction, MockedFunction } from '../mock-service/types'; @@ -53,12 +54,18 @@ export const ngMocks: { includeTextNodes?: boolean, ): void; + /** + * @see https://ng-mocks.sudo.eu/api/ngMocks/defaultConfig + */ + defaultConfig(token: string | InjectionToken | AnyType, config?: IMockBuilderConfig): void; + /** * @see https://ng-mocks.sudo.eu/api/ngMocks/defaultMock */ defaultMock( token: InjectionToken, handler?: (value: undefined | T, injector: Injector) => undefined | Partial, + config?: IMockBuilderConfig, ): void; /** @@ -67,12 +74,17 @@ export const ngMocks: { defaultMock( token: string, handler?: (value: undefined | T, injector: Injector) => undefined | Partial, + config?: IMockBuilderConfig, ): void; /** * @see https://ng-mocks.sudo.eu/api/ngMocks/defaultMock */ - defaultMock(def: AnyType, handler?: (value: T, injector: Injector) => void | Partial): void; + defaultMock( + def: AnyType, + handler?: (value: T, injector: Injector) => void | Partial, + config?: IMockBuilderConfig, + ): void; /** * @see https://ng-mocks.sudo.eu/api/ngMocks/defaultMock @@ -80,6 +92,7 @@ export const ngMocks: { defaultMock( defs: Array | InjectionToken>, handler?: (value: undefined | T, injector: Injector) => undefined | Partial, + config?: IMockBuilderConfig, ): void; /** diff --git a/package-lock.json b/package-lock.json index 8d6c1f7bf8..953f916df4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@commitlint/cli": "16.2.3", "@commitlint/config-conventional": "16.2.1", "@ng-select/ng-select": "8.1.1", + "@ngneat/spectator": "10.0.0", "@ngrx/effects": "13.0.2", "@ngrx/store": "13.0.2", "@ngrx/store-devtools": "13.0.2", @@ -3179,6 +3180,26 @@ "@angular/forms": ">=13.0.0 <14.0.0" } }, + "node_modules/@ngneat/spectator": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/spectator/-/spectator-10.0.0.tgz", + "integrity": "sha512-nMj7RC1KWyvad0FKeLyEBfF+o2ypPJyyYyE8wu18u8CXmRevgrFihWbX06sbo/qL4z4O6acRpS+uF/lKUEDAbg==", + "dev": true, + "dependencies": { + "@testing-library/dom": "^8.11.0", + "jquery": "3.6.0", + "replace-in-file": "6.2.0", + "tslib": "^2.1.0" + }, + "bin": { + "spectator-migrate": "migrate.js" + }, + "peerDependencies": { + "@angular/animations": ">= 13.0.0", + "@angular/common": ">= 13.0.0", + "@angular/router": ">= 13.0.0" + } + }, "node_modules/@ngrx/effects": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-13.0.2.tgz", @@ -6988,6 +7009,25 @@ "node": ">= 0.6.0" } }, + "node_modules/@testing-library/dom": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.3.tgz", + "integrity": "sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -7021,6 +7061,12 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "node_modules/@types/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", + "dev": true + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -8006,6 +8052,15 @@ "integrity": "sha1-oMoMvCmltz6Dbuvhy/bF4OTrgvk=", "dev": true }, + "node_modules/aria-query": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", + "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, "node_modules/array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -10223,6 +10278,12 @@ "buffer-indexof": "^1.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz", + "integrity": "sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw==", + "dev": true + }, "node_modules/dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", @@ -13092,6 +13153,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -14001,6 +14068,15 @@ "node": ">=10" } }, + "node_modules/lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -19287,6 +19363,23 @@ "jsesc": "bin/jsesc" } }, + "node_modules/replace-in-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.2.0.tgz", + "integrity": "sha512-Im2AF9G/qgkYneOc9QwWwUS/efyyonTUBvzXS2VXuxPawE5yQIjT/e6x4CTijO0Quq48lfAujuo+S89RR2TP2Q==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "glob": "^7.1.6", + "yargs": "^16.2.0" + }, + "bin": { + "replace-in-file": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -24691,6 +24784,18 @@ "tslib": "^2.3.1" } }, + "@ngneat/spectator": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@ngneat/spectator/-/spectator-10.0.0.tgz", + "integrity": "sha512-nMj7RC1KWyvad0FKeLyEBfF+o2ypPJyyYyE8wu18u8CXmRevgrFihWbX06sbo/qL4z4O6acRpS+uF/lKUEDAbg==", + "dev": true, + "requires": { + "@testing-library/dom": "^8.11.0", + "jquery": "3.6.0", + "replace-in-file": "6.2.0", + "tslib": "^2.1.0" + } + }, "@ngrx/effects": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-13.0.2.tgz", @@ -27480,6 +27585,22 @@ "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==", "dev": true }, + "@testing-library/dom": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.3.tgz", + "integrity": "sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -27510,6 +27631,12 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "@types/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", + "dev": true + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -28386,6 +28513,12 @@ "integrity": "sha1-oMoMvCmltz6Dbuvhy/bF4OTrgvk=", "dev": true }, + "aria-query": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", + "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", + "dev": true + }, "array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -30083,6 +30216,12 @@ "buffer-indexof": "^1.0.0" } }, + "dom-accessibility-api": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz", + "integrity": "sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw==", + "dev": true + }, "dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", @@ -32163,6 +32302,12 @@ } } }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -32851,6 +32996,12 @@ "yallist": "^4.0.0" } }, + "lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "dev": true + }, "magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -36683,6 +36834,17 @@ } } }, + "replace-in-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.2.0.tgz", + "integrity": "sha512-Im2AF9G/qgkYneOc9QwWwUS/efyyonTUBvzXS2VXuxPawE5yQIjT/e6x4CTijO0Quq48lfAujuo+S89RR2TP2Q==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "glob": "^7.1.6", + "yargs": "^16.2.0" + } + }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", diff --git a/package.json b/package.json index ed9de04475..809e991ccb 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,7 @@ "@commitlint/cli": "16.2.3", "@commitlint/config-conventional": "16.2.1", "@ng-select/ng-select": "8.1.1", + "@ngneat/spectator": "10.0.0", "@ngrx/effects": "13.0.2", "@ngrx/store": "13.0.2", "@ngrx/store-devtools": "13.0.2", diff --git a/tests-e2e/package.json b/tests-e2e/package.json index aeba91f5e0..5b6fe92963 100644 --- a/tests-e2e/package.json +++ b/tests-e2e/package.json @@ -11,7 +11,7 @@ "build:es5": "npm run build -- --ts-config ./tsconfig.es5ivy.app.json --prod", "build:es2015": "npm run build -- --ts-config ./tsconfig.es2015ivy.app.json --prod", "test": "npm run test:jasmine", - "test:debug": "npm run test:jasmine:es2015:ivy -- --browsers=Chrome --watch", + "test:debug": "npm run test:jasmine:es2015 -- --browsers=Chrome --watch", "test:jasmine": "npm run test:jasmine:es5 && npm run test:jasmine:es2015", "test:jasmine:es5": "npm run ng test -- --ts-config ./tsconfig.es5ivy.spec.json --progress=false", "test:jasmine:es2015": "npm run ng test -- --ts-config ./tsconfig.es2015ivy.spec.json --progress=false", @@ -32,6 +32,7 @@ "@angular/platform-browser-dynamic": "13.3.0", "@angular/router": "13.3.0", "@ng-select/ng-select": "8.1.1", + "@ngneat/spectator": "10.0.0", "@ngrx/effects": "13.0.2", "@ngrx/store": "13.0.2", "@ngrx/store-devtools": "13.0.2", diff --git a/tests-e2e/src/issue-971/test.spec.ts b/tests-e2e/src/issue-971/test.spec.ts new file mode 100644 index 0000000000..0c55e4d214 --- /dev/null +++ b/tests-e2e/src/issue-971/test.spec.ts @@ -0,0 +1,68 @@ +import { Component, Injectable, NgModule } from '@angular/core'; +import { createComponentFactory, Spectator } from '@ngneat/spectator'; +import { MockBuilder, ngMocks } from 'ng-mocks'; + +@Injectable() +class TargetService { + public readonly name: string = 'target'; +} + +@Component({ + selector: 'target', + template: `:{{ service.name }}:`, +}) +class TargetComponent { + public constructor(public readonly service: TargetService) {} +} + +@NgModule({ + declarations: [TargetComponent], + providers: [TargetService], +}) +class TargetModule {} + +ngMocks.defaultMock(TargetService, () => ({ + name: 'mock', +})); + +// ngMocks.guts and MockBuilder.build should be compatible with createComponentFactory. +// @see https://github.com/ike18t/ng-mocks/issues/971#issuecomment-902467692 +describe('issue-971', () => { + describe('ngMocks.guts', () => { + let spectator: Spectator; + + const dependencies = ngMocks.guts( + null, + TargetModule, + TargetComponent, + ); + const createComponent = createComponentFactory({ + component: TargetComponent, + ...dependencies, + }); + + beforeEach(() => (spectator = createComponent())); + + it('applies mocks', () => { + expect(ngMocks.formatText(spectator.fixture)).toEqual(':mock:'); + }); + }); + + describe('MockBuilder', () => { + let spectator: Spectator; + + const dependencies = MockBuilder(null, TargetModule) + .exclude(TargetComponent) + .build(); + const createComponent = createComponentFactory({ + component: TargetComponent, + ...dependencies, + }); + + beforeEach(() => (spectator = createComponent())); + + it('applies mocks', () => { + expect(ngMocks.formatText(spectator.fixture)).toEqual(':mock:'); + }); + }); +}); diff --git a/tests/issue-971/test.spec.ts b/tests/issue-971/test.spec.ts new file mode 100644 index 0000000000..07b807d27a --- /dev/null +++ b/tests/issue-971/test.spec.ts @@ -0,0 +1,112 @@ +import { + Component, + Directive, + Input, + NgModule, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + MockBuilder, + MockDirective, + MockRender, + ngMocks, +} from 'ng-mocks'; + +@Directive({ + selector: '[show]', +}) +export class ShowDirective { + public constructor( + protected templateRef: TemplateRef, + protected viewContainerRef: ViewContainerRef, + ) {} + + @Input('show') public set setValue(value: any) { + if (value) { + this.viewContainerRef.createEmbeddedView(this.templateRef); + } else { + this.viewContainerRef.clear(); + } + } +} + +@Directive({ + selector: '[hide]', +}) +export class HideDirective { + public constructor( + protected templateRef: TemplateRef, + protected viewContainerRef: ViewContainerRef, + ) {} + + @Input('hide') public set setValue(value: any) { + if (value) { + this.viewContainerRef.createEmbeddedView(this.templateRef); + } else { + this.viewContainerRef.clear(); + } + } +} + +@Component({ + selector: 'target', + template: ` + :show:{{ content }}: + :hide:{{ content }}: + `, +}) +export class TargetComponent { + @Input() public readonly content: string | null = null; + @Input() public readonly flag: boolean | null = null; +} + +@NgModule({ + declarations: [TargetComponent, ShowDirective, HideDirective], +}) +class TargetModule {} + +// sets config +ngMocks.defaultConfig(ShowDirective, { + render: true, +}); + +// removes config +ngMocks.defaultConfig(HideDirective, { + render: true, +}); +ngMocks.defaultConfig(HideDirective); + +// @see https://github.com/ike18t/ng-mocks/issues/971#issuecomment-902467724 +describe('issue-971:MockBuilder', () => { + beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + + it('renders structural directives', () => { + const fixture = MockRender(TargetComponent, { + content: 'target', + }); + + expect(ngMocks.formatText(fixture)).toEqual(':show:target:'); + }); +}); + +describe('issue-971:TestBed', () => { + beforeEach(() => + TestBed.configureTestingModule({ + declarations: [ + TargetComponent, + MockDirective(ShowDirective), + MockDirective(HideDirective), + ], + }), + ); + + it('renders structural directives', () => { + const fixture = MockRender(TargetComponent, { + content: 'target', + }); + + expect(ngMocks.formatText(fixture)).toEqual(':show:target:'); + }); +});