import { AnyAction, configureStore, EnhancedStore, Reducer, getDefaultMiddleware, CombinedState, PreloadedState, } from '@reduxjs/toolkit'; import { NoInfer } from '@reduxjs/toolkit/dist/tsHelpers'; import { Dispatch, Middleware, MiddlewareAPI } from 'redux'; import thunk, { ThunkMiddleware } from 'redux-thunk'; import { setStore } from '../../../app/store/store'; import { StoreState } from '../../../app/types'; export interface ReduxTesterGiven { givenRootReducer: (rootReducer: Reducer) => ReduxTesterWhen; } export interface ReduxTesterWhen { whenActionIsDispatched: ( action: any, clearPreviousActions?: boolean ) => ReduxTesterWhen & ReduxTesterThen; whenAsyncActionIsDispatched: ( action: any, clearPreviousActions?: boolean ) => Promise & ReduxTesterThen>; } export interface ReduxTesterThen { thenDispatchedActionsShouldEqual: (...dispatchedActions: AnyAction[]) => ReduxTesterWhen; thenDispatchedActionsPredicateShouldEqual: ( predicate: (dispatchedActions: AnyAction[]) => boolean ) => ReduxTesterWhen; thenNoActionsWhereDispatched: () => ReduxTesterWhen; } export interface ReduxTesterArguments { preloadedState?: PreloadedState>>; debug?: boolean; } export const reduxTester = (args?: ReduxTesterArguments): ReduxTesterGiven => { const dispatchedActions: AnyAction[] = []; const logActionsMiddleWare: Middleware<{}, Partial> = (store: MiddlewareAPI>) => (next: Dispatch) => (action: AnyAction) => { // filter out thunk actions if (action && typeof action !== 'function') { dispatchedActions.push(action); } return next(action); }; const preloadedState = args?.preloadedState ?? ({} as unknown as PreloadedState>>); const debug = args?.debug ?? false; let store: EnhancedStore | null = null; const defaultMiddleware = getDefaultMiddleware({ thunk: false, serializableCheck: false, immutableCheck: false, } as any); const givenRootReducer = (rootReducer: Reducer): ReduxTesterWhen => { store = configureStore>>({ reducer: rootReducer, middleware: [...defaultMiddleware, logActionsMiddleWare, thunk] as unknown as [ThunkMiddleware], preloadedState, }); setStore(store as any); return instance; }; const whenActionIsDispatched = ( action: any, clearPreviousActions?: boolean ): ReduxTesterWhen & ReduxTesterThen => { if (clearPreviousActions) { dispatchedActions.length = 0; } if (store === null) { throw new Error('Store was not setup properly'); } store.dispatch(action); return instance; }; const whenAsyncActionIsDispatched = async ( action: any, clearPreviousActions?: boolean ): Promise & ReduxTesterThen> => { if (clearPreviousActions) { dispatchedActions.length = 0; } if (store === null) { throw new Error('Store was not setup properly'); } await store.dispatch(action); return instance; }; const thenDispatchedActionsShouldEqual = (...actions: AnyAction[]): ReduxTesterWhen => { if (debug) { console.log('Dispatched Actions', JSON.stringify(dispatchedActions, null, 2)); } if (!actions.length) { throw new Error('thenDispatchedActionShouldEqual has to be called with at least one action'); } expect(dispatchedActions).toEqual(actions); return instance; }; const thenDispatchedActionsPredicateShouldEqual = ( predicate: (dispatchedActions: AnyAction[]) => boolean ): ReduxTesterWhen => { if (debug) { console.log('Dispatched Actions', JSON.stringify(dispatchedActions, null, 2)); } expect(predicate(dispatchedActions)).toBe(true); return instance; }; const thenNoActionsWhereDispatched = (): ReduxTesterWhen => { if (debug) { console.log('Dispatched Actions', JSON.stringify(dispatchedActions, null, 2)); } expect(dispatchedActions.length).toBe(0); return instance; }; const instance = { givenRootReducer, whenActionIsDispatched, whenAsyncActionIsDispatched, thenDispatchedActionsShouldEqual, thenDispatchedActionsPredicateShouldEqual, thenNoActionsWhereDispatched, }; return instance; };