diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx index 3e62bb01794f181da42cfbce620a9161f9463ff7..fc02472aa6d90d9dc2453dd2457bf230a3f56b34 100644 --- a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx +++ b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx @@ -1,12 +1,12 @@ +import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; +import { MODELS_MOCK } from '@/models/mocks/modelsMock'; import { StoreType } from '@/redux/store'; +import { Accordion } from '@/shared/Accordion'; import { InitialStoreState, getReduxWrapperWithStore, } from '@/utils/testing/getReduxWrapperWithStore'; import { render, screen } from '@testing-library/react'; -import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture'; -import { Accordion } from '@/shared/Accordion'; -import { MODELS_MOCK } from '@/models/mocks/modelsMock'; import { BioEntitiesAccordion } from './BioEntitiesAccordion.component'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -35,7 +35,7 @@ describe('BioEntitiesAccordion - component', () => { error: { name: '', message: '' }, }, models: { - data: undefined, + data: [], loading: 'pending', error: { name: '', message: '' }, }, diff --git a/src/models/fixtures/backgroundsFixture.ts b/src/models/fixtures/backgroundsFixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..e06e5c15e67fea0ee563e14f80792846f5ed998c --- /dev/null +++ b/src/models/fixtures/backgroundsFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +import { z } from 'zod'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { mapBackground } from '../mapBackground'; + +export const backgroundsFixture = createFixture(z.array(mapBackground), { + seed: ZOD_SEED, + array: { min: 2, max: 2 }, +}); diff --git a/src/models/fixtures/overlaysFixture.ts b/src/models/fixtures/overlaysFixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0a26efd4daccf2dbd062e7b37a67ef6e2d1033a --- /dev/null +++ b/src/models/fixtures/overlaysFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +import { z } from 'zod'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { mapOverlay } from '../mapOverlay'; + +export const overlaysFixture = createFixture(z.array(mapOverlay), { + seed: ZOD_SEED, + array: { min: 2, max: 2 }, +}); diff --git a/src/models/fixtures/projectFixture.ts b/src/models/fixtures/projectFixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..99e01bb36cb06a4dac15e20fbf116238a398a1f5 --- /dev/null +++ b/src/models/fixtures/projectFixture.ts @@ -0,0 +1,9 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { projectSchema } from '../project'; + +export const projectFixture = createFixture(projectSchema, { + seed: ZOD_SEED, + array: { min: 1, max: 1 }, +}); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 30395bcb0f57848d8cfa12682e57b40d2faebc5c..776993edc272561e3cdead6f3d8b6fc2f3e58706 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -16,4 +16,5 @@ export const apiPath = { ): string => `projects/${projectId}/overlays/?publicOverlay=${String(publicOverlay)}`, getAllBackgroundsByProjectIdQuery: (projectId: string): string => `projects/${projectId}/backgrounds/`, + getProjectById: (projectId: string): string => `projects/${projectId}`, }; diff --git a/src/redux/backgrounds/backgrounds.reducers.test.ts b/src/redux/backgrounds/backgrounds.reducers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b99161ae9290ee475248566ffe020cc56dc0561 --- /dev/null +++ b/src/redux/backgrounds/backgrounds.reducers.test.ts @@ -0,0 +1,80 @@ +import { PROJECT_ID } from '@/constants'; +import { backgroundsFixture } from '@/models/fixtures/backgroundsFixture'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import { apiPath } from '../apiPath'; +import backgroundsReducer from './backgrounds.slice'; +import { getAllBackgroundsByProjectId } from './backgrounds.thunks'; +import { BackgroundsState } from './backgrounds.types'; + +const mockedAxiosClient = mockNetworkResponse(); + +const INITIAL_STATE: BackgroundsState = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, +}; + +describe('backgrounds reducer', () => { + let store = {} as ToolkitStoreWithSingleSlice<BackgroundsState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('backgrounds', backgroundsReducer); + }); + + it('should match initial state', () => { + const action = { type: 'unknown' }; + + expect(backgroundsReducer(undefined, action)).toEqual(INITIAL_STATE); + }); + it('should update store after succesfull getAllBackgroundsByProjectId query', async () => { + mockedAxiosClient + .onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID)) + .reply(HttpStatusCode.Ok, backgroundsFixture); + + const { type } = await store.dispatch(getAllBackgroundsByProjectId(PROJECT_ID)); + const { data, loading, error } = store.getState().backgrounds; + + expect(type).toBe('backgrounds/getAllBackgroundsByProjectId/fulfilled'); + expect(loading).toEqual('succeeded'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual(backgroundsFixture); + }); + + it('should update store after failed getAllBackgroundsByProjectId query', async () => { + mockedAxiosClient + .onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID)) + .reply(HttpStatusCode.NotFound, []); + + const { type } = await store.dispatch(getAllBackgroundsByProjectId(PROJECT_ID)); + const { data, loading, error } = store.getState().backgrounds; + + expect(type).toBe('backgrounds/getAllBackgroundsByProjectId/rejected'); + expect(loading).toEqual('failed'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual([]); + }); + + it('should update store on loading getAllBackgroundsByProjectId query', async () => { + mockedAxiosClient + .onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID)) + .reply(HttpStatusCode.Ok, backgroundsFixture); + + const actionPromise = store.dispatch(getAllBackgroundsByProjectId(PROJECT_ID)); + + const { data, loading } = store.getState().backgrounds; + expect(data).toEqual([]); + expect(loading).toEqual('pending'); + + actionPromise.then(() => { + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = + store.getState().backgrounds; + + expect(dataPromiseFulfilled).toEqual(backgroundsFixture); + expect(promiseFulfilled).toEqual('succeeded'); + }); + }); +}); diff --git a/src/redux/backgrounds/backgrounds.reducers.ts b/src/redux/backgrounds/backgrounds.reducers.ts index 6c099f17070cd19fff023efdbfaa8e57ca521c0e..2f0868b4a10d853b4c4d74e5ec913e42b05df9b9 100644 --- a/src/redux/backgrounds/backgrounds.reducers.ts +++ b/src/redux/backgrounds/backgrounds.reducers.ts @@ -5,8 +5,15 @@ import { BackgroundsState } from './backgrounds.types'; export const getAllBackgroundsByProjectIdReducer = ( builder: ActionReducerMapBuilder<BackgroundsState>, ): void => { + builder.addCase(getAllBackgroundsByProjectId.pending, state => { + state.loading = 'pending'; + }); builder.addCase(getAllBackgroundsByProjectId.fulfilled, (state, action) => { state.data = action.payload || []; state.loading = 'succeeded'; }); + builder.addCase(getAllBackgroundsByProjectId.rejected, state => { + state.loading = 'failed'; + // TODO to discuss manage state of failure + }); }; diff --git a/src/redux/backgrounds/backgrounds.thunks.ts b/src/redux/backgrounds/backgrounds.thunks.ts index ee0b860f5584dc5865b34fc549f8d7a6fb885024..3741a1c8ef88c80078adf1462bdeb39807f92e65 100644 --- a/src/redux/backgrounds/backgrounds.thunks.ts +++ b/src/redux/backgrounds/backgrounds.thunks.ts @@ -7,7 +7,7 @@ import { z } from 'zod'; import { apiPath } from '../apiPath'; export const getAllBackgroundsByProjectId = createAsyncThunk( - 'models/getAllBackgroundsByProjectId', + 'backgrounds/getAllBackgroundsByProjectId', async (projectId: string): Promise<MapBackground[]> => { const response = await axiosInstance.get<MapBackground[]>( apiPath.getAllBackgroundsByProjectIdQuery(projectId), diff --git a/src/redux/models/models.reducers.test.ts b/src/redux/models/models.reducers.test.ts index c130ae0ee5a782380554a87b39e9a77f31ec3864..1677afdfd86b83f6a1ea7834cbc15ceb0d71018e 100644 --- a/src/redux/models/models.reducers.test.ts +++ b/src/redux/models/models.reducers.test.ts @@ -1,13 +1,13 @@ -import { HttpStatusCode } from 'axios'; import { modelsFixture } from '@/models/fixtures/modelsFixture'; -import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { apiPath } from '@/redux/apiPath'; import { ToolkitStoreWithSingleSlice, createStoreInstanceUsingSliceReducer, } from '@/utils/createStoreInstanceUsingSliceReducer'; -import { apiPath } from '@/redux/apiPath'; -import { getModels } from './models.thunks'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; import modelsReducer from './models.slice'; +import { getModels } from './models.thunks'; import { ModelsState } from './models.types'; const mockedAxiosClient = mockNetworkResponse(); diff --git a/src/redux/models/models.types.ts b/src/redux/models/models.types.ts index 06bd18926c75f5fc88c8f55cde1e92cd5c102609..6d27b9dc198ef7ac5a36b5cb355d141ecf3604a2 100644 --- a/src/redux/models/models.types.ts +++ b/src/redux/models/models.types.ts @@ -1,4 +1,4 @@ import { FetchDataState } from '@/types/fetchDataState'; import { MapModel } from '@/types/models'; -export type ModelsState = FetchDataState<MapModel[] | []>; +export type ModelsState = FetchDataState<MapModel[], []>; diff --git a/src/redux/overlays/overlays.reducers.test.ts b/src/redux/overlays/overlays.reducers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0116134dfce094433f2fd9a9948ee98541cd1eb --- /dev/null +++ b/src/redux/overlays/overlays.reducers.test.ts @@ -0,0 +1,79 @@ +import { PROJECT_ID } from '@/constants'; +import { overlaysFixture } from '@/models/fixtures/overlaysFixture'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import { apiPath } from '../apiPath'; +import overlaysReducer from './overlays.slice'; +import { getAllPublicOverlaysByProjectId } from './overlays.thunks'; +import { OverlaysState } from './overlays.types'; + +const mockedAxiosClient = mockNetworkResponse(); + +const INITIAL_STATE: OverlaysState = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, +}; + +describe('overlays reducer', () => { + let store = {} as ToolkitStoreWithSingleSlice<OverlaysState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('overlays', overlaysReducer); + }); + + it('should match initial state', () => { + const action = { type: 'unknown' }; + + expect(overlaysReducer(undefined, action)).toEqual(INITIAL_STATE); + }); + it('should update store after succesfull getAllPublicOverlaysByProjectId query', async () => { + mockedAxiosClient + .onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true })) + .reply(HttpStatusCode.Ok, overlaysFixture); + + const { type } = await store.dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID)); + const { data, loading, error } = store.getState().overlays; + + expect(type).toBe('overlays/getAllPublicOverlaysByProjectId/fulfilled'); + expect(loading).toEqual('succeeded'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual(overlaysFixture); + }); + + it('should update store after failed getAllPublicOverlaysByProjectId query', async () => { + mockedAxiosClient + .onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true })) + .reply(HttpStatusCode.NotFound, []); + + const { type } = await store.dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID)); + const { data, loading, error } = store.getState().overlays; + + expect(type).toBe('overlays/getAllPublicOverlaysByProjectId/rejected'); + expect(loading).toEqual('failed'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual([]); + }); + + it('should update store on loading getAllPublicOverlaysByProjectId query', async () => { + mockedAxiosClient + .onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true })) + .reply(HttpStatusCode.Ok, overlaysFixture); + + const actionPromise = store.dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID)); + + const { data, loading } = store.getState().overlays; + expect(data).toEqual([]); + expect(loading).toEqual('pending'); + + actionPromise.then(() => { + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().overlays; + + expect(dataPromiseFulfilled).toEqual(overlaysFixture); + expect(promiseFulfilled).toEqual('succeeded'); + }); + }); +}); diff --git a/src/redux/overlays/overlays.reducers.ts b/src/redux/overlays/overlays.reducers.ts index 09863b89a1a6f81fa67e66216a2a5afb21c69449..99e493ea4f4f7b6d9288b0c28cfb9234057e9bc9 100644 --- a/src/redux/overlays/overlays.reducers.ts +++ b/src/redux/overlays/overlays.reducers.ts @@ -5,8 +5,15 @@ import { OverlaysState } from './overlays.types'; export const getAllPublicOverlaysByProjectIdReducer = ( builder: ActionReducerMapBuilder<OverlaysState>, ): void => { + builder.addCase(getAllPublicOverlaysByProjectId.pending, state => { + state.loading = 'pending'; + }); builder.addCase(getAllPublicOverlaysByProjectId.fulfilled, (state, action) => { state.data = action.payload || []; state.loading = 'succeeded'; }); + builder.addCase(getAllPublicOverlaysByProjectId.rejected, state => { + state.loading = 'failed'; + // TODO to discuss manage state of failure + }); }; diff --git a/src/redux/overlays/overlays.thunks.ts b/src/redux/overlays/overlays.thunks.ts index 3dc3c70e8be55ef5502dece731bb090a7758d07d..6a01933378559ff68685808752a45c4cc11ce9aa 100644 --- a/src/redux/overlays/overlays.thunks.ts +++ b/src/redux/overlays/overlays.thunks.ts @@ -7,7 +7,7 @@ import { z } from 'zod'; import { apiPath } from '../apiPath'; export const getAllPublicOverlaysByProjectId = createAsyncThunk( - 'models/getAllPublicOverlaysByProjectId', + 'overlays/getAllPublicOverlaysByProjectId', async (projectId: string): Promise<MapOverlay[]> => { const response = await axiosInstance.get<MapOverlay[]>( apiPath.getAllOverlaysByProjectIdQuery(projectId, { publicOverlay: true }), diff --git a/src/redux/project/project.reducers.test.ts b/src/redux/project/project.reducers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..28b9ef70063d829473534c60f51abe85143da8f9 --- /dev/null +++ b/src/redux/project/project.reducers.test.ts @@ -0,0 +1,77 @@ +import { PROJECT_ID } from '@/constants'; +import { projectFixture } from '@/models/fixtures/projectFixture'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import { apiPath } from '../apiPath'; +import projectReducer from './project.slice'; +import { getProjectById } from './project.thunks'; +import { ProjectState } from './project.types'; + +const mockedAxiosClient = mockNetworkResponse(); + +const INITIAL_STATE: ProjectState = { + data: undefined, + loading: 'idle', + error: { name: '', message: '' }, +}; + +describe('project reducer', () => { + let store = {} as ToolkitStoreWithSingleSlice<ProjectState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('project', projectReducer); + }); + + it('should match initial state', () => { + const action = { type: 'unknown' }; + + expect(projectReducer(undefined, action)).toEqual(INITIAL_STATE); + }); + it('should update store after succesfull getProjectById query', async () => { + mockedAxiosClient + .onGet(apiPath.getProjectById(PROJECT_ID)) + .reply(HttpStatusCode.Ok, projectFixture); + + const { type } = await store.dispatch(getProjectById(PROJECT_ID)); + const { data, loading, error } = store.getState().project; + + expect(type).toBe('project/getProjectById/fulfilled'); + expect(loading).toEqual('succeeded'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual(projectFixture); + }); + + it('should update store after failed getProjectById query', async () => { + mockedAxiosClient.onGet(apiPath.getProjectById(PROJECT_ID)).reply(HttpStatusCode.NotFound, []); + + const { type } = await store.dispatch(getProjectById(PROJECT_ID)); + const { data, loading, error } = store.getState().project; + + expect(type).toBe('project/getProjectById/rejected'); + expect(loading).toEqual('failed'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual(undefined); + }); + + it('should update store on loading getProjectById query', async () => { + mockedAxiosClient + .onGet(apiPath.getProjectById(PROJECT_ID)) + .reply(HttpStatusCode.Ok, projectFixture); + + const actionPromise = store.dispatch(getProjectById(PROJECT_ID)); + + const { data, loading } = store.getState().project; + expect(data).toEqual(undefined); + expect(loading).toEqual('pending'); + + actionPromise.then(() => { + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().project; + + expect(dataPromiseFulfilled).toEqual(projectFixture); + expect(promiseFulfilled).toEqual('succeeded'); + }); + }); +}); diff --git a/src/redux/project/project.reducers.ts b/src/redux/project/project.reducers.ts index aee885ae235dace1b0345e3ee4f6a7d42ad8b3e8..435d55e8abbb5a895a21a0a4bba01e793333e36c 100644 --- a/src/redux/project/project.reducers.ts +++ b/src/redux/project/project.reducers.ts @@ -1,10 +1,17 @@ -import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; -import { ProjectState } from '@/redux/project/project.types'; import { getProjectById } from '@/redux/project/project.thunks'; +import { ProjectState } from '@/redux/project/project.types'; +import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; export const getProjectByIdReducer = (builder: ActionReducerMapBuilder<ProjectState>): void => { + builder.addCase(getProjectById.pending, state => { + state.loading = 'pending'; + }); builder.addCase(getProjectById.fulfilled, (state, action) => { - state.data = action.payload; + state.data = action.payload || undefined; state.loading = 'succeeded'; }); + builder.addCase(getProjectById.rejected, state => { + state.loading = 'failed'; + // TODO to discuss manage state of failure + }); }; diff --git a/src/redux/project/project.thunks.ts b/src/redux/project/project.thunks.ts index d99f5d55ca335e5549354ac7d5e649489e1683f5..f3d9fbe26e2fc0d226a86be7b8762b39a938b777 100644 --- a/src/redux/project/project.thunks.ts +++ b/src/redux/project/project.thunks.ts @@ -3,11 +3,12 @@ import { axiosInstance } from '@/services/api/utils/axiosInstance'; import { Project } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { createAsyncThunk } from '@reduxjs/toolkit'; +import { apiPath } from '../apiPath'; export const getProjectById = createAsyncThunk( 'project/getProjectById', async (id: string): Promise<Project | undefined> => { - const response = await axiosInstance.get<Project>(`projects/${id}`); + const response = await axiosInstance.get<Project>(apiPath.getProjectById(id)); const isDataValid = validateDataUsingZodSchema(response.data, projectSchema);