UNPKG

@sigi/core

Version:
156 lines (132 loc) 4.54 kB
import { Action, Epic } from '@sigi/types' import { Reducer } from 'react' import { interval } from 'rxjs' import { delay, filter, map, mergeMap, tap } from 'rxjs/operators' import { Store } from '../store' interface State { foo: string bar: number | null } const delayTime = 300 describe('store specs', () => { const UPDATE_FOO = 'update-foo' const UPDATE_BAR = 'update-bar' const ASYNC_UPDATE_FOO = 'async-update-foo' let timer = jest.useFakeTimers() const mockReducer: Reducer<State, Action<any>> = (prevState, action) => { if (action.type === UPDATE_FOO) { return { ...prevState, foo: action.payload } } else if (action.type === UPDATE_BAR) { return { ...prevState, bar: action.payload } } return prevState } let store: Store<State> const mockEpic: Epic = (action$) => action$.pipe( filter(({ type }) => type === ASYNC_UPDATE_FOO), delay(delayTime), map(({ payload }) => ({ type: UPDATE_FOO, payload, store })), ) const defaultState = { foo: '1', bar: null, } beforeEach(() => { store = new Store('testStore', mockReducer, mockEpic) store.setup(defaultState) timer = jest.useFakeTimers() }) afterEach(() => { store.dispose() jest.useRealTimers() }) describe('state', () => { it('should be able to create state', () => { expect(store.state).toBe(defaultState) }) }) describe('dispatch', () => { it('should be able to change state via dispatch action', () => { store.dispatch({ type: UPDATE_FOO, payload: '2', store }) expect(store.state.foo).toBe('2') }) it('should do nothing when action type is not matched any reducer', () => { store.dispatch({ type: '__NOOP__', payload: '2', store }) expect(store.state.foo).toBe(defaultState.foo) }) it('should run epic', () => { store.dispatch({ type: ASYNC_UPDATE_FOO, payload: '2', store }) expect(store.state.foo).toBe(defaultState.foo) timer.advanceTimersByTime(delayTime) expect(store.state.foo).toBe('2') }) it("should be able to dispatch other store's action", () => { const otherStore = new Store<State>('otherStore', mockReducer) otherStore.setup(defaultState) expect(store.state.foo).toBe(defaultState.foo) otherStore.dispatch({ type: UPDATE_FOO, payload: '2', store, }) expect(store.state.foo).toBe('2') }) it('should be able to add epic after setup', () => { const spy = jest.fn() store.addEpic((prev) => (action$) => action$.pipe(tap(spy), prev)) const action = { type: '__NOOP__', payload: null, store } store.dispatch(action) expect(spy).toHaveBeenCalledTimes(1) const [[arg]] = spy.mock.calls expect(arg).toStrictEqual(action) }) it('should respect epics ordering', () => { const spy = jest.fn() store.addEpic((prev) => (action$) => action$.pipe(prev, tap(spy))) store.dispatch({ type: UPDATE_FOO, payload: '2', store: store }) expect(spy).toHaveBeenCalledTimes(0) }) }) describe('Dispose', () => { it('should complete all pending epic after disposed', () => { const nextSpy = jest.fn() const store = new Store('testStore', mockReducer, (action$) => action$.pipe( mergeMap((action) => interval(1000).pipe(map(() => action))), tap({ next: nextSpy, }), ), ) store.setup(defaultState) store.dispatch({ type: UPDATE_FOO, payload: '2', store }) timer.advanceTimersByTime(1000) expect(nextSpy).toHaveBeenCalledTimes(1) store.dispose() timer.advanceTimersByTime(1000) expect(nextSpy).toHaveBeenCalledTimes(1) }) it('should complete all pending epic after disposed on added epic', () => { const nextSpy = jest.fn() const store = new Store('testStore', mockReducer) store.setup(defaultState) store.addEpic( // eslint-disable-next-line sonarjs/no-identical-functions () => (action$) => action$.pipe( mergeMap((action) => interval(1000).pipe(map(() => action))), tap({ next: nextSpy, }), ), ) store.dispatch({ type: UPDATE_FOO, payload: '2', store }) timer.advanceTimersByTime(1000) expect(nextSpy).toHaveBeenCalledTimes(1) store.dispose() timer.advanceTimersByTime(1000) expect(nextSpy).toHaveBeenCalledTimes(1) }) }) })