diff --git a/docs/articles/guides/libraries/ngrx.md b/docs/articles/guides/libraries/ngrx.md new file mode 100644 index 0000000000..b172de3ce7 --- /dev/null +++ b/docs/articles/guides/libraries/ngrx.md @@ -0,0 +1,77 @@ +--- +title: How to test usage of ngrx in Angular applications +sidebar_label: NGRX +--- + +`ng-mocks` perfectly mocks `NGRX` modules. However, there are might be issues if some of them should be kept. + +Besides `StoreModule`, which is an entry point to configure effects and reducers, +under the hood `NGRX` uses four other modules, and these modules should be configured in `MockBuilder`: + +- `StoreRootModule` +- `StoreFeatureModule` +* `EffectsRootModule` +* `EffectsFeatureModule` + +Or a simple value is to rely on `.ngModule` property: + +- `StoreModule.forRoot().ngModule` +- `StoreModule.forFeature().ngModule` +* `EffectsModule.forRoot().ngModule` +* `EffectsModule.forFeature().ngModule` + +Let's imagine that we want to keep `StoreModule` in a test, +then we should pass the modules into [`.keep`](../../api/MockBuilder.md#keep): + +```ts +beforeEach(() => + MockBuilder(MyComponent, MyModule) + .keep(StoreRootModule) + .keep(StoreFeatureModule) + .keep(EffectsRootModule) + .keep(EffectsFeatureModule) + ); +``` + +or + +```ts +beforeEach(() => + MockBuilder(MyComponent, MyModule) + .keep(StoreModule.forRoot().ngModule) + .keep(StoreModule.forFeature().ngModule) + .keep(EffectsModule.forRoot().ngModule) + .keep(EffectsModule.forFeature().ngModule) +); +``` + +Please pay attention that `MyModule` or its imports should import `.forRoot` and `.forFeature`, +otherwise we need to call [`.keep`](../../api/MockBuilder.md#keep) as usually: + +```ts +beforeEach(() => + MockBuilder(MyComponent, MyModule) + .keep(StoreModule.forRoot()) + .keep(StoreModule.forFeature()) + .keep(EffectsModule.forRoot()) + .keep(EffectsModule.forFeature()) +); +``` + +## Lazy loaded modules with `.forFeature()` + +When we want to test a lazy loaded module, it means there is no import of `.forRoot()` calls, +because they are in a parent module. +Therefore, the `.forRoot()` calls should be added manually without any special configuration: + +```ts +beforeEach(() => + MockBuilder(SomeComponent, LazyLoadedModule) + // providing root tools + .keep(StoreModule.forRoot()) + .keep(EffectsModule.forRoot()) + // keeping lazy loaded module definitions + .keep(StoreFeatureModule) + .keep(EffectsFeatureModule) +); +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index a6c32f771c..66a438c05b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -143,7 +143,12 @@ module.exports = { type: 'category', label: 'Testing libraries', collapsed: false, - items: ['guides/libraries/ng-select', 'guides/libraries/angular-material', 'guides/libraries/primeng'], + items: [ + 'guides/libraries/ng-select', + 'guides/libraries/angular-material', + 'guides/libraries/primeng', + 'guides/libraries/ngrx', + ], }, ], }; diff --git a/e2e/a11/package-lock.json b/e2e/a11/package-lock.json index 921f8d6324..2adb9b6fb0 100644 --- a/e2e/a11/package-lock.json +++ b/e2e/a11/package-lock.json @@ -5406,6 +5406,16 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", @@ -8136,6 +8146,13 @@ } } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -12330,6 +12347,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, "nanoid": { "version": "3.1.22", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", @@ -17106,7 +17130,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -17817,7 +17845,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/e2e/am/package-lock.json b/e2e/am/package-lock.json index 5b96a60133..2cfe33ba47 100644 --- a/e2e/am/package-lock.json +++ b/e2e/am/package-lock.json @@ -5386,6 +5386,16 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", @@ -8048,6 +8058,13 @@ } } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -12145,6 +12162,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, "nanoid": { "version": "3.1.22", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", @@ -16776,7 +16800,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -17487,7 +17515,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/extract-dep.ts b/libs/ng-mocks/src/lib/mock-builder/promise/extract-dep.ts index 416fc5a428..8999820341 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/extract-dep.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/extract-dep.ts @@ -1,4 +1,13 @@ // Extracts dependency among flags of parameters. + +const detectForwardRed = (provide: any): any => { + if (typeof provide === 'function' && provide.__forward_ref__) { + return provide(); + } + + return provide; +}; + export default (decorators?: any[]): any => { if (!decorators) { return; @@ -14,5 +23,5 @@ export default (decorators?: any[]): any => { } } - return provide; + return detectForwardRed(provide); }; diff --git a/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts b/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts index a753c8d6fc..eb85a8abc0 100644 --- a/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts +++ b/libs/ng-mocks/src/lib/mock-builder/promise/init-ng-modules.ts @@ -1,4 +1,5 @@ -import { mapValues } from '../../common/core.helpers'; +import { flatten, mapValues } from '../../common/core.helpers'; +import funcGetProvider from '../../common/func.get-provider'; import { isNgDef } from '../../common/func.is-ng-def'; import ngMocksUniverse from '../../common/ng-mocks-universe'; @@ -6,27 +7,34 @@ import initModule from './init-module'; import skipInitModule from './skip-init-module'; import { BuilderData, NgMeta } from './types'; +const handleDef = ({ imports, declarations }: NgMeta, def: any, defProviders: Map): void => { + if (isNgDef(def, 'm')) { + const extendedDef = initModule(def, defProviders); + imports.push(extendedDef); + + // adding providers to touches + if (typeof extendedDef === 'object' && extendedDef.providers) { + for (const provider of flatten(extendedDef.providers)) { + ngMocksUniverse.touches.add(funcGetProvider(provider)); + } + } + } else { + declarations.push(ngMocksUniverse.getBuildDeclaration(def)); + } + + ngMocksUniverse.touches.add(def); +}; + export default ({ configDef, keepDef, mockDef, replaceDef }: BuilderData, defProviders: Map): NgMeta => { - const { imports, declarations, providers }: NgMeta = { imports: [], declarations: [], providers: [] }; + const meta: NgMeta = { imports: [], declarations: [], providers: [] }; // Adding suitable leftovers. for (const def of [...mapValues(mockDef), ...mapValues(keepDef), ...mapValues(replaceDef)]) { if (skipInitModule(def, configDef)) { continue; } - - if (isNgDef(def, 'm')) { - imports.push(initModule(def, defProviders)); - } else { - declarations.push(ngMocksUniverse.getBuildDeclaration(def)); - } - - ngMocksUniverse.touches.add(def); + handleDef(meta, def, defProviders); } - return { - declarations, - imports, - providers, - }; + return meta; }; diff --git a/package-lock.json b/package-lock.json index 1720a82afd..df159e0005 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4461,6 +4461,24 @@ "tslib": "^2.0.0" } }, + "@ngrx/effects": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-11.1.1.tgz", + "integrity": "sha512-KLfGSjlmlPUMlMEQkdD6tnJCs/dLSBJC6hZhCnobpDrBR9YMpoDDjM1t0Veg+Z50dL6AGO/T4dllRjd3BJuWiw==", + "dev": true, + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngrx/store": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-11.1.1.tgz", + "integrity": "sha512-tkeKoyYo631hLJ1I8+bm9EWoi7E0A3i4IMjvf956Vpu5IdMnP6d0HW3lKU/ruhFD5YOXAHcUgEIWyfxxILABag==", + "dev": true, + "requires": { + "tslib": "^2.0.0" + } + }, "@ngtools/webpack": { "version": "11.2.12", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-11.2.12.tgz", @@ -8452,6 +8470,16 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", @@ -11466,6 +11494,13 @@ } } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -14926,6 +14961,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true + }, "nanoid": { "version": "3.1.22", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", @@ -23539,7 +23581,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -24334,7 +24380,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/package.json b/package.json index bf9f809b1e..61c8daab84 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,8 @@ "@commitlint/cli": "12.1.1", "@commitlint/config-conventional": "12.1.1", "@ng-select/ng-select": "6.1.0", + "@ngrx/effects": "11.1.1", + "@ngrx/store": "11.1.1", "@semantic-release/changelog": "5.0.1", "@semantic-release/exec": "5.0.0", "@semantic-release/git": "9.0.0", @@ -164,7 +166,6 @@ "karma-typescript": "5.5.1", "lint-staged": "10.5.4", "npm": "6.14.13", - "postcss": "8.2.13", "prettier": "2.2.1", "primeng": "11.4.0", "puppeteer": "9.1.1", diff --git a/tests-angular/e2e/package-lock.json b/tests-angular/e2e/package-lock.json deleted file mode 100644 index 4b60a7f71b..0000000000 --- a/tests-angular/e2e/package-lock.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "e2e-angular", - "lockfileVersion": 1 -} diff --git a/tests-angular/e2e/package.json b/tests-angular/e2e/package.json index 359bbed800..69e08d7269 100644 --- a/tests-angular/e2e/package.json +++ b/tests-angular/e2e/package.json @@ -34,6 +34,8 @@ "@angular/platform-browser-dynamic": "11.2.13", "@angular/router": "11.2.13", "@ng-select/ng-select": "6.1.0", + "@ngrx/effects": "11.1.1", + "@ngrx/store": "11.1.1", "@types/jasmine": "3.7.0", "@types/node": "12.20.12", "jasmine-core": "3.7.1", diff --git a/tests-angular/e2e/src/issue-312/only-feature.spec.ts b/tests-angular/e2e/src/issue-312/only-feature.spec.ts new file mode 100644 index 0000000000..a63c450333 --- /dev/null +++ b/tests-angular/e2e/src/issue-312/only-feature.spec.ts @@ -0,0 +1,129 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + Injectable, + NgModule, + OnInit, +} from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + Actions, + createEffect, + EffectsFeatureModule, + EffectsModule, + ofType, +} from '@ngrx/effects'; +import { + createAction, + createFeatureSelector, + createReducer, + on, + props, + Store, + StoreFeatureModule, + StoreModule, +} from '@ngrx/store'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; +import { mapTo } from 'rxjs/operators'; + +const setValue = createAction( + 'set-value', + props<{ + value: string; + }>(), +); + +const resetValue = createAction('reset-value'); + +const myReducer = { + featureKey: 'test', + reducer: createReducer( + '', + on(setValue, (state, { value }) => value), + ), +}; + +const selectValue = createFeatureSelector(myReducer.featureKey); + +@Injectable() +class MyEffects { + public readonly reset$ = createEffect(() => + this.actions$.pipe( + ofType(resetValue), + mapTo(setValue({ value: '' })), + ), + ); + + public constructor(private readonly actions$: Actions) {} +} + +@Component({ + selector: 'target', + template: '{{ value$ | async }}', +}) +class MyComponent implements OnInit { + public value$ = this.store.select(selectValue); + + public constructor(private readonly store: Store) {} + + public ngOnInit(): void { + this.store.dispatch(setValue({ value: 'target' })); + } + + public reset(): void { + this.store.dispatch(resetValue()); + } +} + +@NgModule({ + declarations: [MyComponent], + exports: [MyComponent], + imports: [ + CommonModule, + EffectsModule.forFeature([MyEffects]), + StoreModule.forFeature(myReducer.featureKey, myReducer.reducer), + ], +}) +class MyModule {} + +describe('issue-312:only-feature', () => { + describe('real', () => { + beforeEach(() => { + return TestBed.configureTestingModule({ + imports: [ + MyModule, + StoreModule.forRoot({}), + EffectsModule.forRoot(), + ], + }).compileComponents(); + }); + + it('providers root modules correctly', () => { + const fixture = MockRender(MyComponent); + expect(ngMocks.formatText(fixture)).toEqual('target'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual(''); + }); + }); + + describe('builder', () => { + beforeEach(() => + MockBuilder(MyComponent, MyModule) + .keep(StoreModule.forRoot({})) + .keep(EffectsModule.forRoot()) + .keep(StoreFeatureModule) + .keep(EffectsFeatureModule), + ); + + it('providers root modules correctly', () => { + const fixture = MockRender(MyComponent); + expect(ngMocks.formatText(fixture)).toEqual('target'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual(''); + }); + }); +}); diff --git a/tests-angular/e2e/src/issue-312/test.spec.ts b/tests-angular/e2e/src/issue-312/test.spec.ts new file mode 100644 index 0000000000..a8220a75a9 --- /dev/null +++ b/tests-angular/e2e/src/issue-312/test.spec.ts @@ -0,0 +1,171 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + Injectable, + NgModule, + OnInit, +} from '@angular/core'; +import { + Actions, + createEffect, + EffectsFeatureModule, + EffectsModule, + EffectsRootModule, + ofType, +} from '@ngrx/effects'; +import { + createAction, + createFeatureSelector, + createReducer, + on, + props, + Store, + StoreFeatureModule, + StoreModule, + StoreRootModule, +} from '@ngrx/store'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; +import { mapTo } from 'rxjs/operators'; + +const setValue = createAction( + 'set-value', + props<{ + value: string; + }>(), +); + +const resetValue = createAction('reset-value'); + +const myReducer = { + featureKey: 'test', + reducer: createReducer( + '', + on(setValue, (state, { value }) => value), + ), +}; + +const selectValue = createFeatureSelector(myReducer.featureKey); + +@Injectable() +class MyEffects { + public readonly reset$ = createEffect(() => + this.actions$.pipe( + ofType(resetValue), + mapTo(setValue({ value: '' })), + ), + ); + + public constructor(private readonly actions$: Actions) {} +} + +@Component({ + selector: 'target', + template: '{{ value$ | async }}', +}) +class MyComponent implements OnInit { + public value$ = this.store.select(selectValue); + + public constructor(private readonly store: Store) {} + + public ngOnInit(): void { + this.store.dispatch(setValue({ value: 'target' })); + } + + public reset(): void { + this.store.dispatch(resetValue()); + } +} + +@NgModule({ + declarations: [MyComponent], + imports: [ + CommonModule, + EffectsModule.forRoot(), + EffectsModule.forFeature([MyEffects]), + StoreModule.forRoot({}), + StoreModule.forFeature(myReducer.featureKey, myReducer.reducer), + ], +}) +class MyModule {} + +describe('issue-312', () => { + describe('real', () => { + beforeEach(() => MockBuilder(MyComponent).keep(MyModule)); + + it('renders value and resets it', () => { + const fixture = MockRender(MyComponent); + expect(ngMocks.formatText(fixture)).toEqual('target'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual(''); + }); + }); + + describe('explicitly keep store in a mock module', () => { + beforeEach(() => + MockBuilder(MyComponent, MyModule) + .keep(StoreRootModule) + .keep(StoreFeatureModule) + .keep(EffectsRootModule) + .keep(EffectsFeatureModule), + ); + + it('renders value and resets it', () => { + const fixture = MockRender(MyComponent); + expect(ngMocks.formatText(fixture)).toEqual('target'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual(''); + }); + }); + + describe('keep store via module with providers', () => { + beforeEach(() => + MockBuilder(MyComponent, MyModule) + .keep(StoreModule.forRoot({})) + .keep( + StoreModule.forFeature( + myReducer.featureKey, + myReducer.reducer, + ), + ) + .keep(EffectsModule.forRoot()) + .keep(EffectsModule.forFeature()), + ); + + it('renders value and resets it', () => { + const fixture = MockRender(MyComponent); + expect(ngMocks.formatText(fixture)).toEqual('target'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual(''); + }); + }); + + describe('keep store via module from providers', () => { + beforeEach(() => + MockBuilder(MyComponent, MyModule) + .keep(StoreModule.forRoot({}).ngModule) + .keep( + StoreModule.forFeature( + myReducer.featureKey, + myReducer.reducer, + ).ngModule, + ) + .keep(EffectsModule.forRoot().ngModule) + .keep(EffectsModule.forFeature().ngModule), + ); + + it('renders value and resets it', () => { + const fixture = MockRender(MyComponent); + expect(ngMocks.formatText(fixture)).toEqual('target'); + + fixture.point.componentInstance.reset(); + fixture.detectChanges(); + expect(ngMocks.formatText(fixture)).toEqual(''); + }); + }); +}); diff --git a/tests/issue-312/test.spec.ts b/tests/issue-312/test.spec.ts new file mode 100644 index 0000000000..d51eb88dcb --- /dev/null +++ b/tests/issue-312/test.spec.ts @@ -0,0 +1,146 @@ +import { + Component, + forwardRef, + Inject, + Injectable as InjectableSource, + NgModule, + Optional, + VERSION, +} from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + MockBuilder, + MockRender, + NgModuleWithProviders, +} 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); +} + +@Injectable({ + providedIn: 'root', +}) +class RootService { + public readonly name = 'RootService'; +} + +@Injectable() +class StandardService { + public readonly name = 'StandardService'; +} + +@Injectable() +class ProvidedService { + public readonly name = 'ProvidedService'; +} + +@NgModule() +class ServiceModule { + public static forRoot(): NgModuleWithProviders { + return { + ngModule: ServiceModule, + providers: [ProvidedService], + }; + } +} + +@Injectable({ + providedIn: ServiceModule, +}) +class ModuleService { + public readonly name = 'ModuleService'; +} + +@Component({ + selector: 'target', + template: `target`, +}) +class TargetComponent { + public constructor( + @Optional() public readonly root: RootService, + @Optional() + @Inject(StandardService) + public readonly standard: StandardService | null, + @Optional() public readonly provided: ProvidedService, + @Optional() + @Inject(forwardRef(() => ModuleService)) + public readonly module: ModuleService, + ) {} +} + +@NgModule({ + declarations: [TargetComponent], + exports: [TargetComponent], + imports: [ServiceModule.forRoot()], +}) +class TargetModule {} + +// the idea is that all of the services have been injected besides StandardService. +describe('issue-312', () => { + beforeEach(() => { + if (parseInt(VERSION.major, 10) <= 5) { + pending('Need Angular > 5'); + } + }); + + describe('default', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [TargetModule], + }).compileComponents(), + ); + + it('detects injected services', () => { + const component = MockRender(TargetComponent).point + .componentInstance; + expect(component.root).toEqual(jasmine.any(RootService)); + expect(component.root.name).toEqual('RootService'); + expect(component.standard).toEqual(null); + expect(component.provided).toEqual( + jasmine.any(ProvidedService), + ); + expect(component.provided.name).toEqual('ProvidedService'); + expect(component.module).toEqual(jasmine.any(ModuleService)); + expect(component.module.name).toEqual('ModuleService'); + }); + }); + + describe('keep', () => { + beforeEach(() => MockBuilder(TargetComponent).keep(TargetModule)); + + it('detects injected services', () => { + const component = MockRender(TargetComponent).point + .componentInstance; + expect(component.root).toEqual(jasmine.any(RootService)); + expect(component.root.name).toEqual('RootService'); + expect(component.standard).toEqual(null); + expect(component.provided).toEqual( + jasmine.any(ProvidedService), + ); + expect(component.provided.name).toEqual('ProvidedService'); + expect(component.module).toEqual(jasmine.any(ModuleService)); + expect(component.module.name).toEqual('ModuleService'); + }); + }); + + describe('mock', () => { + beforeEach(() => MockBuilder(TargetComponent).mock(TargetModule)); + + it('detects injected services', () => { + const component = MockRender(TargetComponent).point + .componentInstance; + expect(component.root).toEqual(jasmine.any(RootService)); + expect(component.root.name).toBeUndefined(); + expect(component.standard).toEqual(null); + expect(component.provided).toEqual( + jasmine.any(ProvidedService), + ); + expect(component.provided.name).toBeUndefined(); + expect(component.module).toEqual(jasmine.any(ModuleService)); + expect(component.module.name).toBeUndefined(); + }); + }); +});