diff --git a/jest.config.js b/jest.config.js index b204aef..b6f7774 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,7 @@ module.exports = { setupFilesAfterEnv: ['/setupJest.ts'], rootDir: path.resolve('.'), testMatch: ['/src/**/*.spec.ts'], - collectCoverageFrom: ['/src/lib/**/*.ts'], + collectCoverageFrom: ['/src/lib/**/*.ts', '/src/jest/**/*.ts' ], coverageReporters: ['json', 'lcovonly', 'lcov', 'text', 'html'], coveragePathIgnorePatterns: ['/node_modules/'], globals: { diff --git a/src/jest/package.json b/src/jest/package.json new file mode 100644 index 0000000..4bbd7d0 --- /dev/null +++ b/src/jest/package.json @@ -0,0 +1,3 @@ +{ + "ngPackage": {} +} diff --git a/src/jest/src/jest-helpers.ts b/src/jest/src/jest-helpers.ts new file mode 100644 index 0000000..45be1aa --- /dev/null +++ b/src/jest/src/jest-helpers.ts @@ -0,0 +1,29 @@ +import { of, Subject } from 'rxjs'; +import { Store } from '@ngxs/store'; +import { TestBed } from '@angular/core/testing'; + +class NgxsJestHelper { + private static mockedSelector: { key: any; value: Subject }[] = []; + + static mockSelect(selectorFn: (state: any) => T): Subject { + const store: Store = TestBed.get(Store); + if (!jest.isMockFunction(store.select)) { + jest.spyOn(store, 'select').mockImplementation((selector) => { + const match = NgxsJestHelper.mockedSelector.find((s) => s.key === selector); + if (match) { + return match.value; + } + return of(); + }); + } + + const subject = new Subject(); + NgxsJestHelper.mockedSelector = [ + ...NgxsJestHelper.mockedSelector.filter((s) => s.key !== selectorFn), + { key: selectorFn, value: subject } + ]; + return subject; + } +} + +export const mockSelect = NgxsJestHelper.mockSelect; diff --git a/src/jest/src/public_api.ts b/src/jest/src/public_api.ts new file mode 100644 index 0000000..bc93c4c --- /dev/null +++ b/src/jest/src/public_api.ts @@ -0,0 +1 @@ +export { mockSelect } from './jest-helpers'; diff --git a/src/tests/ngxs.setup.spec.ts b/src/tests/ngxs.setup.spec.ts index 6b72de7..d018f8c 100644 --- a/src/tests/ngxs.setup.spec.ts +++ b/src/tests/ngxs.setup.spec.ts @@ -1,5 +1,11 @@ -import { Action, NgxsAfterBootstrap, NgxsOnInit, State, StateContext } from '@ngxs/store'; +import { Action, NgxsAfterBootstrap, NgxsModule, NgxsOnInit, Select, Selector, State, StateContext } from '@ngxs/store'; import { NgxsTestBed } from '../lib/ngxs.setup'; +import { Component } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { CommonModule } from '@angular/common'; +import { mockSelect } from '../jest/src/public_api'; describe('Full testing NGXS States with NgxsTestBed', () => { it('should be correct testing lifecycle with NgxsTestBed', () => { @@ -136,3 +142,81 @@ describe('Full testing NGXS States with NgxsTestBed', () => { expect(getStateContextMocks[FOOD_STATE_NAME].dispatch).toHaveBeenCalledTimes(1); }); }); + +describe('Select tests', () => { + class FeedAction { + public static type = 'zoo'; + constructor(public payload: number) {} + } + + class AddAnimalsAction { + public static type = 'add animals'; + constructor(public payload: number) {} + } + + @State({ name: 'zoo', defaults: { feed: 0, animals: 0 } }) + class ZooState { + @Selector() + static feed(state: { feed: number; animals: number }) { + return state.feed; + } + + @Selector() + static animals(state: { feed: number; animals: number }) { + return state.animals; + } + + @Action(FeedAction) public feed(ctx: StateContext, { payload }: FeedAction) { + ctx.patchState({ feed: payload }); + } + + @Action(AddAnimalsAction) public animals(ctx: StateContext, { payload }: AddAnimalsAction) { + const state = ctx.getState(); + ctx.patchState({ animals: state.animals + payload }); + } + } + + @Component({ + template: ` + {{ foodSelector$ | async }} +

{{ animalsSelector$ | async }}

+ ` + }) + class HostComponent { + @Select(ZooState.feed) foodSelector$!: Observable; + @Select(ZooState.animals) animalsSelector$!: Observable; + } + + let foodSelectorSubject: Subject; + let animalsSelectorSubject: Subject; + let fixture: ComponentFixture; + let component: HostComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [HostComponent], + imports: [CommonModule, NgxsModule.forRoot([ZooState])] + }).compileComponents(); + + foodSelectorSubject = mockSelect(ZooState.feed); + animalsSelectorSubject = mockSelect(ZooState.animals); + + fixture = TestBed.createComponent(HostComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should display mocked value', () => { + foodSelectorSubject.next(1); + animalsSelectorSubject.next(2); + + fixture.detectChanges(); + expect(component).toBeTruthy(); + + const span = fixture.debugElement.query(By.css('span')); + expect(span.nativeElement.innerHTML).toMatch('1'); + const p = fixture.debugElement.query(By.css('p')); + expect(p.nativeElement.innerHTML).toMatch('2'); + }); +});