Skip to content
Snippets Groups Projects
Commit fe8f1719 authored by Adrian Orłów's avatar Adrian Orłów
Browse files

test: add tests for map init hook logic

parent 525ff421
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!44feat: add map API communication
Showing
with 369 additions and 101 deletions
import { TopBar } from '@/components/FunctionalArea/TopBar';
import { NavBar } from '@/components/FunctionalArea/NavBar';
import { MapNavigation } from '@/components/FunctionalArea/MapNavigation';
import { NavBar } from '@/components/FunctionalArea/NavBar';
import { TopBar } from '@/components/FunctionalArea/TopBar';
export const FunctionalArea = (): JSX.Element => (
<>
......
import { BASE_MAP_IMAGES_URL } from '@/constants';
import { getMapTileUrl } from './getMapTileUrl';
describe('getMapTileUrl - util', () => {
describe('when projectDirectory is empty', () => {
it('should return empty value', () => {
const projectDirectory = undefined;
const currentBackgroundImagePath = 'currentBackgroundImagePath';
const result = '';
expect(
getMapTileUrl({
projectDirectory,
currentBackgroundImagePath,
}),
).toBe(result);
});
});
describe('when all args are valid', () => {
it('should return correct value', () => {
const projectDirectory = 'directory';
const currentBackgroundImagePath = 'currentBackgroundImagePath';
const result = `${BASE_MAP_IMAGES_URL}/map_images/${projectDirectory}/${currentBackgroundImagePath}/{z}/{x}/{y}.PNG`;
expect(
getMapTileUrl({
projectDirectory,
currentBackgroundImagePath,
}),
).toBe(result);
});
});
});
describe.skip('useOlMapConfig - util', () => {
// TODO: tests
// everything is mocked in the file, so we need to firstly wait for module API connection
it('noop', () => {
// eslint-disable-next-line no-magic-numbers
expect(1).toEqual(1);
});
});
import { FunctionalArea } from '@/components/FunctionalArea';
import { Map } from '@/components/Map';
import { PROJECT_ID } from '@/constants';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { initMapData } from '@/redux/map/map.thunks';
import { getProjectById } from '@/redux/project/project.thunks';
import { initDataLoadingInitialized } from '@/redux/root/init.selectors';
import { AppDispatch } from '@/redux/store';
import { Manrope } from '@next/font/google';
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { twMerge } from 'tailwind-merge';
import { useInitializeStore } from './utils/useInitializeStore';
const manrope = Manrope({
variable: '--font-manrope',
......@@ -18,25 +11,8 @@ const manrope = Manrope({
subsets: ['latin'],
});
/* prettier-ignore */
const getInitStoreData =
() =>
(dispatch: AppDispatch): void => {
dispatch(getProjectById(PROJECT_ID));
dispatch(initMapData());
};
export const MinervaSPA = (): JSX.Element => {
const dispatch = useAppDispatch();
const storeInitialized = useSelector(initDataLoadingInitialized);
useEffect(() => {
if (storeInitialized) {
return;
}
dispatch(getInitStoreData());
}, [dispatch, storeInitialized]);
useInitializeStore();
return (
<div className={twMerge('relative', manrope.variable)}>
......
import { PROJECT_ID } from '@/constants';
import { backgroundsFixture } from '@/models/fixtures/backgroundsFixture';
import { modelsFixture } from '@/models/fixtures/modelsFixture';
import { overlaysFixture } from '@/models/fixtures/overlaysFixture';
import { projectFixture } from '@/models/fixtures/projectFixture';
import { apiPath } from '@/redux/apiPath';
import { backgroundsDataSelector } from '@/redux/backgrounds/background.selectors';
import { modelsDataSelector } from '@/redux/models/models.selectors';
import { overlaysDataSelector } from '@/redux/overlays/overlays.selectors';
import { projectDataSelector } from '@/redux/project/project.selectors';
import { initDataLoadingInitialized } from '@/redux/root/init.selectors';
import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { renderHook, waitFor } from '@testing-library/react';
import { HttpStatusCode } from 'axios';
import * as hook from './useInitializeStore';
const mockedAxiosClient = mockNetworkResponse();
describe('useInitializeStore - hook', () => {
describe('when fired', () => {
beforeAll(() => {
mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, modelsFixture);
mockedAxiosClient
.onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true }))
.reply(HttpStatusCode.Ok, overlaysFixture);
mockedAxiosClient
.onGet(apiPath.getProjectById(PROJECT_ID))
.reply(HttpStatusCode.Ok, projectFixture);
mockedAxiosClient
.onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID))
.reply(HttpStatusCode.Ok, backgroundsFixture);
});
it('should fetch project data in store', async () => {
const { Wrapper, store } = getReduxWrapperWithStore();
renderHook(() => hook.useInitializeStore(), { wrapper: Wrapper });
await waitFor(() => {
const data = projectDataSelector(store.getState());
expect(data).toEqual(projectFixture);
});
});
it('should fetch backgrounds data in store', async () => {
const { Wrapper, store } = getReduxWrapperWithStore();
renderHook(() => hook.useInitializeStore(), { wrapper: Wrapper });
await waitFor(() => {
const data = backgroundsDataSelector(store.getState());
expect(data).toEqual(backgroundsFixture);
});
});
it('should fetch overlays data in store', async () => {
const { Wrapper, store } = getReduxWrapperWithStore();
renderHook(() => hook.useInitializeStore(), { wrapper: Wrapper });
await waitFor(() => {
const data = overlaysDataSelector(store.getState());
expect(data).toEqual(overlaysFixture);
});
});
it('should fetch models data in store', async () => {
const { Wrapper, store } = getReduxWrapperWithStore();
renderHook(() => hook.useInitializeStore(), { wrapper: Wrapper });
await waitFor(() => {
const data = modelsDataSelector(store.getState());
expect(data).toEqual(modelsFixture);
});
});
it('should use valid initialize value', () => {
const { Wrapper, store } = getReduxWrapperWithStore();
const initializedeBefore = initDataLoadingInitialized(store.getState());
renderHook(() => hook.useInitializeStore(), { wrapper: Wrapper });
const initializedAfter = initDataLoadingInitialized(store.getState());
expect(initializedeBefore).toBe(false);
expect(initializedAfter).toBe(true);
});
});
});
import { PROJECT_ID } from '@/constants';
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { initMapData } from '@/redux/map/map.thunks';
import { getProjectById } from '@/redux/project/project.thunks';
import { initDataLoadingInitialized } from '@/redux/root/init.selectors';
import { AppDispatch } from '@/redux/store';
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
/* prettier-ignore */
export const getInitStoreData =
() =>
(dispatch: AppDispatch): void => {
dispatch(getProjectById(PROJECT_ID));
dispatch(initMapData());
};
export const useInitializeStore = (): void => {
const dispatch = useAppDispatch();
const isInitialized = useSelector(initDataLoadingInitialized);
useEffect(() => {
if (isInitialized) {
return;
}
dispatch(getInitStoreData());
}, [dispatch, isInitialized]);
};
......@@ -8,3 +8,8 @@ export const modelsFixture = createFixture(z.array(mapModelSchema), {
seed: ZOD_SEED,
array: { min: 3, max: 3 },
});
export const singleModelFixture = createFixture(mapModelSchema, {
seed: ZOD_SEED,
array: { min: 3, max: 3 },
});
......@@ -26,3 +26,5 @@ export const MAP_DATA_INITIAL_STATE: MapData = {
maxZoom: DEFAULT_MAX_ZOOM,
},
};
export const MIDDLEWARE_ALLOWED_ACTIONS: string[] = ['map/setMapData', 'map/initMapData'];
import { MapModel } from '@/types/models';
import { getUpdatedMapData } from '@/utils/getUpdatedMapData';
import { Middleware, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit';
import { modelsDataSelector } from '../models/models.selectors';
import type { AppDispatch, RootState } from '../store';
import { setMapData } from './map.slice';
import { InitMapDataActionPayload, SetMapDataActionPayload } from './map.types';
type AllowedAction = PayloadAction<SetMapDataActionPayload | InitMapDataActionPayload>;
const ALLOWED_ACTIONS = ['map/setMapData', 'map/initMapData'];
const checkIfIsActionValid = (action: AllowedAction, state: RootState): boolean => {
const isAllowedAction = ALLOWED_ACTIONS.some(allowedAction =>
action.type.includes(allowedAction),
);
const isModelIdTheSame = state.map.data.modelId === action.payload?.modelId;
return isAllowedAction && !isModelIdTheSame;
};
const getUpdatedModel = (action: AllowedAction, state: RootState): MapModel | undefined => {
const models = modelsDataSelector(state);
return models.find(model => model.idObject === action?.payload?.modelId);
};
/* prettier-ignore */
export const mapMiddleware: Middleware =
({ getState, dispatch }: MiddlewareAPI<AppDispatch, RootState>) =>
(next: AppDispatch) =>
// eslint-disable-next-line consistent-return
(action: AllowedAction) => {
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;
};
......@@ -11,7 +11,8 @@ export const getMapReducers = (builder: ActionReducerMapBuilder<MapState>): void
state.loading = 'pending';
});
builder.addCase(initMapData.fulfilled, (state, action) => {
state.data = { ...state.data, ...action.payload };
const payload = action.payload || {};
state.data = { ...state.data, ...payload };
state.loading = 'succeeded';
});
builder.addCase(initMapData.rejected, state => {
......
import { PROJECT_ID } from '@/constants';
import { backgroundsFixture } from '@/models/fixtures/backgroundsFixture';
import { modelsFixture } from '@/models/fixtures/modelsFixture';
import { overlaysFixture } from '@/models/fixtures/overlaysFixture';
import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { HttpStatusCode } from 'axios';
import { apiPath } from '../apiPath';
import { backgroundsDataSelector } from '../backgrounds/background.selectors';
import { modelsDataSelector } from '../models/models.selectors';
import { overlaysDataSelector } from '../overlays/overlays.selectors';
import { StoreType } from '../store';
import { initMapData } from './map.thunks';
import { InitMapDataActionPayload } from './map.types';
const mockedAxiosClient = mockNetworkResponse();
describe('map thunks', () => {
describe('initMapData - thunk', () => {
describe('when API is returning valid data', () => {
let store = {} as StoreType;
let payload = {} as InitMapDataActionPayload;
beforeAll(async () => {
mockedAxiosClient.resetHandlers();
mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, modelsFixture);
mockedAxiosClient
.onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true }))
.reply(HttpStatusCode.Ok, overlaysFixture);
mockedAxiosClient
.onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID))
.reply(HttpStatusCode.Ok, backgroundsFixture);
store = getReduxWrapperWithStore().store;
payload = (await store.dispatch(initMapData())).payload as InitMapDataActionPayload;
});
it('should fetch backgrounds data in store', async () => {
const data = backgroundsDataSelector(store.getState());
expect(data).toEqual(backgroundsFixture);
});
it('should fetch overlays data in store', async () => {
const data = overlaysDataSelector(store.getState());
expect(data).toEqual(overlaysFixture);
});
it('should fetch models data in store', async () => {
const data = modelsDataSelector(store.getState());
expect(data).toEqual(modelsFixture);
});
it('should return valid payload', () => {
const FIRST = 0;
expect(payload).toMatchObject({
modelId: modelsFixture[FIRST].idObject,
backgroundId: backgroundsFixture[FIRST].id,
});
});
});
describe('when API is returning empty array', () => {
let store = {} as StoreType;
let payload = {} as InitMapDataActionPayload;
beforeEach(async () => {
mockedAxiosClient.onGet(apiPath.getModelsString()).reply(HttpStatusCode.Ok, []);
mockedAxiosClient
.onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true }))
.reply(HttpStatusCode.Ok, []);
mockedAxiosClient
.onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID))
.reply(HttpStatusCode.Ok, []);
store = getReduxWrapperWithStore().store;
payload = (await store.dispatch(initMapData())).payload as InitMapDataActionPayload;
});
it('should return empty payload', () => {
expect(payload).toStrictEqual({});
});
});
});
});
......@@ -8,7 +8,22 @@ import { getAllPublicOverlaysByProjectId } from '../overlays/overlays.thunks';
import type { AppDispatch, RootState } from '../store';
import { InitMapDataActionPayload } from './map.types';
const FIRST = 0;
const getPayloadFromState = (state: RootState): InitMapDataActionPayload => {
const FIRST = 0;
const models = modelsDataSelector(state);
const backgrounds = backgroundsDataSelector(state);
const modelId = models?.[FIRST]?.idObject;
const backgroundId = backgrounds?.[FIRST]?.id;
if (!modelId || !backgroundId) {
return {};
}
return {
modelId,
backgroundId,
};
};
export const initMapData = createAsyncThunk<
InitMapDataActionPayload,
......@@ -22,10 +37,5 @@ export const initMapData = createAsyncThunk<
]);
const state = getState();
const models = modelsDataSelector(state);
const backgrounds = backgroundsDataSelector(state);
const modelId = models[FIRST].idObject;
const backgroundId = backgrounds[FIRST].id;
return { modelId, backgroundId };
return getPayloadFromState(state);
});
......@@ -30,6 +30,10 @@ export type SetMapDataActionPayload = Partial<MapData> | undefined;
export type SetMapDataAction = PayloadAction<SetMapDataActionPayload>;
export type InitMapDataActionPayload = { modelId: number; backgroundId: number };
export type InitMapDataActionPayload = { modelId: number; backgroundId: number } | object;
export type InitMapDataAction = PayloadAction<SetMapDataAction>;
export type MiddlewareAllowedAction = PayloadAction<
SetMapDataActionPayload | InitMapDataActionPayload
>;
......@@ -4,7 +4,7 @@ import { mapDataSelector } from '../map/map.selectors';
export const modelsSelector = createSelector(rootSelector, state => state.models);
export const modelsDataSelector = createSelector(modelsSelector, models => models.data || []);
export const modelsDataSelector = createSelector(modelsSelector, models => models?.data || []);
export const currentModelSelector = createSelector(
modelsDataSelector,
......
......@@ -5,5 +5,5 @@ export const overlaysSelector = createSelector(rootSelector, state => state.over
export const overlaysDataSelector = createSelector(
overlaysSelector,
overlays => overlays.data || [],
overlays => overlays?.data || [],
);
......@@ -10,7 +10,7 @@ import overlaysReducer from '@/redux/overlays/overlays.slice';
import projectReducer from '@/redux/project/project.slice';
import searchReducer from '@/redux/search/search.slice';
import { applyMiddleware, configureStore } from '@reduxjs/toolkit';
import { mapMiddleware } from './map/map.middleware';
import { mapMiddleware } from './map/middleware/map.middleware';
export const reducers = {
search: searchReducer,
......
import { DEFAULT_ZOOM } from '@/constants/map';
import { singleModelFixture } from '@/models/fixtures/modelsFixture';
import { getUpdatedMapData } from './getUpdatedMapData';
const HALF = 2;
describe('getUpdatedMapData - util', () => {
describe('when model does not have default values', () => {
const model = {
...singleModelFixture,
defaultCenterX: null,
defaultCenterY: null,
defaultZoomLevel: null,
};
it('should return correct value', () => {
const result = {
modelId: model.idObject,
size: {
width: model.width,
height: model.height,
tileSize: model.tileSize,
minZoom: model.minZoom,
maxZoom: model.maxZoom,
},
position: {
x: model.width / HALF,
y: model.height / HALF,
z: DEFAULT_ZOOM,
},
};
expect(getUpdatedMapData({ model })).toMatchObject(result);
});
});
describe('when model has default falsy values', () => {
const model = {
...singleModelFixture,
defaultCenterX: 0,
defaultCenterY: 0,
defaultZoomLevel: null,
};
it('should return correct value', () => {
const result = {
modelId: model.idObject,
size: {
width: model.width,
height: model.height,
tileSize: model.tileSize,
minZoom: model.minZoom,
maxZoom: model.maxZoom,
},
position: {
x: 0,
y: 0,
z: DEFAULT_ZOOM,
},
};
expect(getUpdatedMapData({ model })).toMatchObject(result);
});
});
describe('when model has default truthy values', () => {
const model = {
...singleModelFixture,
defaultCenterX: 10,
defaultCenterY: 10,
defaultZoomLevel: 1,
};
it('should return correct value', () => {
const result = {
modelId: model.idObject,
size: {
width: model.width,
height: model.height,
tileSize: model.tileSize,
minZoom: model.minZoom,
maxZoom: model.maxZoom,
},
position: {
x: 10,
y: 10,
z: 1,
},
};
expect(getUpdatedMapData({ model })).toMatchObject(result);
});
});
});
......@@ -6,9 +6,7 @@ interface GetUpdatedMapDataArgs {
model: MapModel;
}
type GetUpdatedMapDataResult = Pick<MapData, 'modelId' | 'size' | 'position'> & {
backgroundId?: number;
};
type GetUpdatedMapDataResult = Pick<MapData, 'modelId' | 'size' | 'position'>;
const HALF = 2;
......@@ -22,8 +20,8 @@ export const getUpdatedMapData = ({ model }: GetUpdatedMapDataArgs): GetUpdatedM
maxZoom: model.maxZoom,
},
position: {
x: model.defaultCenterX || model.width / HALF,
y: model.defaultCenterY || model.height / HALF,
z: model.defaultZoomLevel || DEFAULT_ZOOM,
x: model.defaultCenterX ?? model.width / HALF,
y: model.defaultCenterY ?? model.height / HALF,
z: model.defaultZoomLevel ?? DEFAULT_ZOOM,
},
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment