From fe8f17196fe53e5771e6fcc93d48638a2fd10158 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:26:26 +0200
Subject: [PATCH] test: add tests for map init hook logic

---
 .../FunctionalArea.component.tsx              |  4 +-
 .../utils/config/getMapTileUrl.test.ts        | 34 +++++++
 .../Map/MapViewer/utils/useOlMapInit.test.ts  |  9 --
 src/components/SPA/MinervaSPA.component.tsx   | 28 +-----
 .../SPA/utils/useInitializeStore.test.ts      | 85 +++++++++++++++++
 .../SPA/utils/useInitializeStore.ts           | 29 ++++++
 src/models/fixtures/modelsFixture.ts          |  5 +
 src/redux/map/map.constants.ts                |  2 +
 src/redux/map/map.middleware.ts               | 46 ---------
 src/redux/map/map.reducers.ts                 |  3 +-
 src/redux/map/map.thunks.test.ts              | 85 +++++++++++++++++
 src/redux/map/map.thunks.ts                   | 24 +++--
 src/redux/map/map.types.ts                    |  6 +-
 src/redux/models/models.selectors.ts          |  2 +-
 src/redux/overlays/overlays.selectors.ts      |  2 +-
 src/redux/store.ts                            |  2 +-
 src/utils/map/getUpdatedMapData.test.ts       | 94 +++++++++++++++++++
 src/utils/{ => map}/getUpdatedMapData.ts      | 10 +-
 18 files changed, 369 insertions(+), 101 deletions(-)
 create mode 100644 src/components/Map/MapViewer/utils/config/getMapTileUrl.test.ts
 delete mode 100644 src/components/Map/MapViewer/utils/useOlMapInit.test.ts
 create mode 100644 src/components/SPA/utils/useInitializeStore.test.ts
 create mode 100644 src/components/SPA/utils/useInitializeStore.ts
 delete mode 100644 src/redux/map/map.middleware.ts
 create mode 100644 src/redux/map/map.thunks.test.ts
 create mode 100644 src/utils/map/getUpdatedMapData.test.ts
 rename src/utils/{ => map}/getUpdatedMapData.ts (74%)

diff --git a/src/components/FunctionalArea/FunctionalArea.component.tsx b/src/components/FunctionalArea/FunctionalArea.component.tsx
index 784d7fde..d38bcd19 100644
--- a/src/components/FunctionalArea/FunctionalArea.component.tsx
+++ b/src/components/FunctionalArea/FunctionalArea.component.tsx
@@ -1,6 +1,6 @@
-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 => (
   <>
diff --git a/src/components/Map/MapViewer/utils/config/getMapTileUrl.test.ts b/src/components/Map/MapViewer/utils/config/getMapTileUrl.test.ts
new file mode 100644
index 00000000..91d8919a
--- /dev/null
+++ b/src/components/Map/MapViewer/utils/config/getMapTileUrl.test.ts
@@ -0,0 +1,34 @@
+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);
+    });
+  });
+});
diff --git a/src/components/Map/MapViewer/utils/useOlMapInit.test.ts b/src/components/Map/MapViewer/utils/useOlMapInit.test.ts
deleted file mode 100644
index 100eaaee..00000000
--- a/src/components/Map/MapViewer/utils/useOlMapInit.test.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-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);
-  });
-});
diff --git a/src/components/SPA/MinervaSPA.component.tsx b/src/components/SPA/MinervaSPA.component.tsx
index 6703ea8c..856e3487 100644
--- a/src/components/SPA/MinervaSPA.component.tsx
+++ b/src/components/SPA/MinervaSPA.component.tsx
@@ -1,15 +1,8 @@
 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)}>
diff --git a/src/components/SPA/utils/useInitializeStore.test.ts b/src/components/SPA/utils/useInitializeStore.test.ts
new file mode 100644
index 00000000..d1b7da08
--- /dev/null
+++ b/src/components/SPA/utils/useInitializeStore.test.ts
@@ -0,0 +1,85 @@
+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);
+    });
+  });
+});
diff --git a/src/components/SPA/utils/useInitializeStore.ts b/src/components/SPA/utils/useInitializeStore.ts
new file mode 100644
index 00000000..7e2fb0af
--- /dev/null
+++ b/src/components/SPA/utils/useInitializeStore.ts
@@ -0,0 +1,29 @@
+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]);
+};
diff --git a/src/models/fixtures/modelsFixture.ts b/src/models/fixtures/modelsFixture.ts
index 2eb9109d..91bc15d7 100644
--- a/src/models/fixtures/modelsFixture.ts
+++ b/src/models/fixtures/modelsFixture.ts
@@ -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 },
+});
diff --git a/src/redux/map/map.constants.ts b/src/redux/map/map.constants.ts
index a6307772..ba88a038 100644
--- a/src/redux/map/map.constants.ts
+++ b/src/redux/map/map.constants.ts
@@ -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'];
diff --git a/src/redux/map/map.middleware.ts b/src/redux/map/map.middleware.ts
deleted file mode 100644
index dc1e99e7..00000000
--- a/src/redux/map/map.middleware.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-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;
-      };
diff --git a/src/redux/map/map.reducers.ts b/src/redux/map/map.reducers.ts
index 51f7faab..962704b3 100644
--- a/src/redux/map/map.reducers.ts
+++ b/src/redux/map/map.reducers.ts
@@ -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 => {
diff --git a/src/redux/map/map.thunks.test.ts b/src/redux/map/map.thunks.test.ts
new file mode 100644
index 00000000..e6eb1523
--- /dev/null
+++ b/src/redux/map/map.thunks.test.ts
@@ -0,0 +1,85 @@
+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({});
+      });
+    });
+  });
+});
diff --git a/src/redux/map/map.thunks.ts b/src/redux/map/map.thunks.ts
index 481d5cfb..e6d8fef4 100644
--- a/src/redux/map/map.thunks.ts
+++ b/src/redux/map/map.thunks.ts
@@ -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);
 });
diff --git a/src/redux/map/map.types.ts b/src/redux/map/map.types.ts
index 108648dc..7051691e 100644
--- a/src/redux/map/map.types.ts
+++ b/src/redux/map/map.types.ts
@@ -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
+>;
diff --git a/src/redux/models/models.selectors.ts b/src/redux/models/models.selectors.ts
index 1ae57c7e..8a9478b1 100644
--- a/src/redux/models/models.selectors.ts
+++ b/src/redux/models/models.selectors.ts
@@ -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,
diff --git a/src/redux/overlays/overlays.selectors.ts b/src/redux/overlays/overlays.selectors.ts
index 80b0439b..f9d36cad 100644
--- a/src/redux/overlays/overlays.selectors.ts
+++ b/src/redux/overlays/overlays.selectors.ts
@@ -5,5 +5,5 @@ export const overlaysSelector = createSelector(rootSelector, state => state.over
 
 export const overlaysDataSelector = createSelector(
   overlaysSelector,
-  overlays => overlays.data || [],
+  overlays => overlays?.data || [],
 );
diff --git a/src/redux/store.ts b/src/redux/store.ts
index e1ecab61..aaecc167 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -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,
diff --git a/src/utils/map/getUpdatedMapData.test.ts b/src/utils/map/getUpdatedMapData.test.ts
new file mode 100644
index 00000000..5afdbc3c
--- /dev/null
+++ b/src/utils/map/getUpdatedMapData.test.ts
@@ -0,0 +1,94 @@
+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);
+    });
+  });
+});
diff --git a/src/utils/getUpdatedMapData.ts b/src/utils/map/getUpdatedMapData.ts
similarity index 74%
rename from src/utils/getUpdatedMapData.ts
rename to src/utils/map/getUpdatedMapData.ts
index 7bbc1329..552e29a6 100644
--- a/src/utils/getUpdatedMapData.ts
+++ b/src/utils/map/getUpdatedMapData.ts
@@ -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,
   },
 });
-- 
GitLab