From 24042df94f1f46a30f3d1b1db8c90294393d2310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com> Date: Thu, 26 Oct 2023 00:28:06 +0200 Subject: [PATCH] feat: add map middleware and tests --- .../map/middleware/checkIfActionValid.test.ts | 121 ++++++++++++++++++ .../map/middleware/checkIfIsActionValid.ts | 19 +++ .../map/middleware/getUpdatedModel.test.ts | 53 ++++++++ src/redux/map/middleware/getUpdatedModel.ts | 15 +++ .../map/middleware/map.middleware.test.ts | 106 +++++++++++++++ src/redux/map/middleware/map.middleware.ts | 28 ++++ 6 files changed, 342 insertions(+) create mode 100644 src/redux/map/middleware/checkIfActionValid.test.ts create mode 100644 src/redux/map/middleware/checkIfIsActionValid.ts create mode 100644 src/redux/map/middleware/getUpdatedModel.test.ts create mode 100644 src/redux/map/middleware/getUpdatedModel.ts create mode 100644 src/redux/map/middleware/map.middleware.test.ts create mode 100644 src/redux/map/middleware/map.middleware.ts diff --git a/src/redux/map/middleware/checkIfActionValid.test.ts b/src/redux/map/middleware/checkIfActionValid.test.ts new file mode 100644 index 00000000..ed62dfc9 --- /dev/null +++ b/src/redux/map/middleware/checkIfActionValid.test.ts @@ -0,0 +1,121 @@ +import { RootState } from '@/redux/store'; +import { Loading } from '@/types/loadingState'; +import { MAP_DATA_INITIAL_STATE, MIDDLEWARE_ALLOWED_ACTIONS } from '../map.constants'; +import { checkIfIsActionValid } from './checkIfIsActionValid'; + +const state: Pick<RootState, 'map'> = { + map: { + data: { + ...MAP_DATA_INITIAL_STATE, + modelId: 2137, + }, + loading: 'idle' as Loading, + error: { name: '', message: '' }, + }, +}; + +describe('checkIfIsActionValid - util', () => { + describe('when action type is allowed', () => { + describe('when action payload has model id equal to current', () => { + const modelId = 2137; + + it.each(MIDDLEWARE_ALLOWED_ACTIONS)('should return false', actionType => { + expect( + checkIfIsActionValid( + { + type: actionType, + payload: { + modelId, + }, + }, + state as RootState, + ), + ).toBe(false); + }); + }); + + describe('when action payload has model id different than current', () => { + const modelId = 997; + + it.each(MIDDLEWARE_ALLOWED_ACTIONS)('should return true', actionType => { + expect( + checkIfIsActionValid( + { + type: actionType, + payload: { + modelId, + }, + }, + state as RootState, + ), + ).toBe(true); + }); + }); + + describe('when action payload has NOT model id', () => { + it.each(MIDDLEWARE_ALLOWED_ACTIONS)('should return true', actionType => { + expect( + checkIfIsActionValid( + { + type: actionType, + payload: {}, + }, + state as RootState, + ), + ).toBe(true); + }); + }); + }); + + describe('when action type is NOT allowed', () => { + describe('when action payload has model id equal to current', () => { + const modelId = 2137; + + it('should return false', () => { + expect( + checkIfIsActionValid( + { + type: 'some/other/action', + payload: { + modelId, + }, + }, + state as RootState, + ), + ).toBe(false); + }); + }); + + describe('when action payload has model id different than current', () => { + const modelId = 997; + + it('should return false', () => { + expect( + checkIfIsActionValid( + { + type: 'some/other/action', + payload: { + modelId, + }, + }, + state as RootState, + ), + ).toBe(false); + }); + }); + + describe('when action payload has NOT model id', () => { + it('should return false', () => { + expect( + checkIfIsActionValid( + { + type: 'some/other/action', + payload: {}, + }, + state as RootState, + ), + ).toBe(false); + }); + }); + }); +}); diff --git a/src/redux/map/middleware/checkIfIsActionValid.ts b/src/redux/map/middleware/checkIfIsActionValid.ts new file mode 100644 index 00000000..38596143 --- /dev/null +++ b/src/redux/map/middleware/checkIfIsActionValid.ts @@ -0,0 +1,19 @@ +import type { RootState } from '@/redux/store'; +import { MIDDLEWARE_ALLOWED_ACTIONS } from '../map.constants'; +import { MiddlewareAllowedAction } from '../map.types'; + +export const checkIfIsActionValid = ( + action: MiddlewareAllowedAction, + state: RootState, +): boolean => { + const isAllowedAction = MIDDLEWARE_ALLOWED_ACTIONS.some(allowedAction => + action.type.includes(allowedAction), + ); + + const { modelId: currentModelId } = state.map.data; + const payload = action?.payload || {}; + const payloadModelId = 'modelId' in payload ? payload.modelId : null; + const isModelIdTheSame = currentModelId === payloadModelId; + + return isAllowedAction && !isModelIdTheSame; +}; diff --git a/src/redux/map/middleware/getUpdatedModel.test.ts b/src/redux/map/middleware/getUpdatedModel.test.ts new file mode 100644 index 00000000..65cd7f25 --- /dev/null +++ b/src/redux/map/middleware/getUpdatedModel.test.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-magic-numbers */ +import { modelsFixture } from '@/models/fixtures/modelsFixture'; +import { RootState } from '@/redux/store'; +import { MIDDLEWARE_ALLOWED_ACTIONS } from '../map.constants'; +import { getUpdatedModel } from './getUpdatedModel'; + +const state: Pick<RootState, 'models'> = { + models: { + data: modelsFixture, + loading: 'idle', + error: { name: '', message: '' }, + }, +}; + +describe('getUpdatedModel - util', () => { + describe('when payload has valid modelId', () => { + const model = modelsFixture[0]; + const action = { + type: MIDDLEWARE_ALLOWED_ACTIONS[0], + payload: { + modelId: model.idObject, + }, + }; + + it('returns model object', () => { + expect(getUpdatedModel(action, state as RootState)).toStrictEqual(model); + }); + }); + + describe('when payload has invalid modelId', () => { + const action = { + type: MIDDLEWARE_ALLOWED_ACTIONS[0], + payload: { + modelId: null, + }, + }; + + it('returns undefined', () => { + expect(getUpdatedModel(action, state as RootState)).toStrictEqual(undefined); + }); + }); + + describe('when payload does not have modelId', () => { + const action = { + type: MIDDLEWARE_ALLOWED_ACTIONS[0], + payload: {}, + }; + + it('returns undefined', () => { + expect(getUpdatedModel(action, state as RootState)).toStrictEqual(undefined); + }); + }); +}); diff --git a/src/redux/map/middleware/getUpdatedModel.ts b/src/redux/map/middleware/getUpdatedModel.ts new file mode 100644 index 00000000..60c26a24 --- /dev/null +++ b/src/redux/map/middleware/getUpdatedModel.ts @@ -0,0 +1,15 @@ +import { modelsDataSelector } from '@/redux/models/models.selectors'; +import type { RootState } from '@/redux/store'; +import { MapModel } from '@/types/models'; +import { MiddlewareAllowedAction } from '../map.types'; + +export const getUpdatedModel = ( + action: MiddlewareAllowedAction, + state: RootState, +): MapModel | undefined => { + const models = modelsDataSelector(state); + const payload = action?.payload || {}; + const payloadModelId = 'modelId' in payload ? payload.modelId : null; + + return models.find(model => model.idObject === payloadModelId); +}; diff --git a/src/redux/map/middleware/map.middleware.test.ts b/src/redux/map/middleware/map.middleware.test.ts new file mode 100644 index 00000000..7698a04f --- /dev/null +++ b/src/redux/map/middleware/map.middleware.test.ts @@ -0,0 +1,106 @@ +/* eslint-disable no-magic-numbers */ +import { modelsFixture } from '@/models/fixtures/modelsFixture'; +import type { RootState } from '@/redux/store'; +import { Loading } from '@/types/loadingState'; +import { MAP_DATA_INITIAL_STATE, MIDDLEWARE_ALLOWED_ACTIONS } from '../map.constants'; +import * as checkIfIsActionValid from './checkIfIsActionValid'; +import * as getUpdatedModel from './getUpdatedModel'; +import { mapMiddleware } from './map.middleware'; + +jest.mock('./checkIfIsActionValid', () => { + return { + __esModule: true, + ...jest.requireActual('./checkIfIsActionValid'), + }; +}); + +jest.mock('./getUpdatedModel', () => { + return { + __esModule: true, + ...jest.requireActual('./getUpdatedModel'), + }; +}); + +const defaultSliceState = { + data: [], + loading: 'idle' as Loading, + error: { name: '', message: '' }, +}; + +const checkIfIsActionValidSpy = jest.spyOn(checkIfIsActionValid, 'checkIfIsActionValid'); +const getUpdatedModelSpy = jest.spyOn(getUpdatedModel, 'getUpdatedModel'); + +describe('map middleware', () => { + describe('on action', () => { + const doDispatch = jest.fn(); + const doGetState = jest.fn().mockImplementation( + (): Pick<RootState, 'models' | 'map'> => ({ + map: { + ...defaultSliceState, + data: MAP_DATA_INITIAL_STATE, + }, + models: { + ...defaultSliceState, + data: modelsFixture, + }, + }), + ); + + const handler = mapMiddleware({ + dispatch: doDispatch, + getState: doGetState, + }); + + describe('when action is valid', () => { + const next = jest.fn(); + + describe('and when model is valid', () => { + it.each(MIDDLEWARE_ALLOWED_ACTIONS)('should run next action', actionType => { + const model = modelsFixture[0]; + const action = { + payload: { + modelId: model.idObject, + }, + type: actionType, + }; + + handler(next)(action); + expect(checkIfIsActionValidSpy).toHaveLastReturnedWith(true); + expect(getUpdatedModelSpy).toHaveLastReturnedWith(model); + expect(next).toBeCalled(); + }); + }); + + describe('and when model is NOT valid', () => { + it.each(MIDDLEWARE_ALLOWED_ACTIONS)('should run next action', actionType => { + const action = { + payload: { + modelId: null, + }, + type: actionType, + }; + + handler(next)(action); + expect(checkIfIsActionValidSpy).toHaveLastReturnedWith(true); + expect(getUpdatedModelSpy).toHaveLastReturnedWith(undefined); + expect(next).toBeCalled(); + }); + }); + }); + + describe('when action is NOT valid', () => { + it('should run next action', () => { + const next = jest.fn(); + const action = { + payload: {}, + type: 'some/other/action', + }; + + handler(next)(action); + expect(checkIfIsActionValidSpy).toHaveLastReturnedWith(false); + expect(getUpdatedModelSpy).toHaveLastReturnedWith(undefined); + expect(next).toBeCalled(); + }); + }); + }); +}); diff --git a/src/redux/map/middleware/map.middleware.ts b/src/redux/map/middleware/map.middleware.ts new file mode 100644 index 00000000..93b9e3ff --- /dev/null +++ b/src/redux/map/middleware/map.middleware.ts @@ -0,0 +1,28 @@ +import { getUpdatedMapData } from '@/utils/map/getUpdatedMapData'; +import { Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; +import type { AppDispatch, RootState } from '../../store'; +import { setMapData } from '../map.slice'; +import { MiddlewareAllowedAction } from '../map.types'; +import { checkIfIsActionValid } from './checkIfIsActionValid'; +import { getUpdatedModel } from './getUpdatedModel'; + +/* prettier-ignore */ +export const mapMiddleware: Middleware = + ({ getState, dispatch }: MiddlewareAPI<AppDispatch, RootState>) => + (next: AppDispatch) => + // eslint-disable-next-line consistent-return + (action: MiddlewareAllowedAction) => { + const state = getState(); + const isActionValid = checkIfIsActionValid(action, state); + const updatedModel = getUpdatedModel(action, state); + const returnValue = next(action); + + if (!isActionValid || !updatedModel) { + return returnValue; + } + + const updatedMapData = getUpdatedMapData({ model: updatedModel }); + dispatch(setMapData(updatedMapData)); + + return returnValue; + }; -- GitLab