import { AnyAction, configureStore, EnhancedStore, Reducer, Tuple } from '@reduxjs/toolkit'; import { Middleware, Store, StoreEnhancer, UnknownAction } from 'redux'; import { thunk, ThunkDispatch, ThunkMiddleware } from 'redux-thunk'; import { setStore } from '../../../app/store/store'; import { StoreState } from '../../../app/types/store'; 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?: Partial>; debug?: boolean; } export const reduxTester = (args?: ReduxTesterArguments): ReduxTesterGiven => { const dispatchedActions: AnyAction[] = []; const logActionsMiddleWare: Middleware<{}, Partial> = (store) => (next) => (action) => { // filter out thunk actions if (action && typeof action !== 'function') { dispatchedActions.push(action as AnyAction); } return next(action); }; const preloadedState = args?.preloadedState ?? ({} as unknown as Partial>); const debug = args?.debug ?? false; let store: EnhancedStore | null = null; const givenRootReducer = ( rootReducer: Reducer>> ): ReduxTesterWhen => { store = configureStore< State, UnknownAction, Tuple<[ThunkMiddleware]>, Tuple< [ StoreEnhancer<{ dispatch: ThunkDispatch; }>, StoreEnhancer, ] >, Partial> >({ reducer: rootReducer, middleware: (getDefaultMiddleware) => [ ...getDefaultMiddleware({ thunk: false, serializableCheck: false, immutableCheck: false, }), logActionsMiddleWare, thunk, ] as unknown as Tuple<[ThunkMiddleware]>, preloadedState, }); setStore(store as Store); 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; };