From 8eea9f3d2b854603a0d371297e1de3d13f9526b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com> Date: Tue, 24 Oct 2023 14:02:57 +0200 Subject: [PATCH] test: add tests for backgrounds, overlays, project --- .../BioEntitiesAccordion.component.test.tsx | 8 +- src/models/fixtures/backgroundsFixture.ts | 10 +++ src/models/fixtures/overlaysFixture.ts | 10 +++ src/models/fixtures/projectFixture.ts | 9 +++ src/redux/apiPath.ts | 1 + .../backgrounds/backgrounds.reducers.test.ts | 80 +++++++++++++++++++ src/redux/backgrounds/backgrounds.reducers.ts | 7 ++ src/redux/backgrounds/backgrounds.thunks.ts | 2 +- src/redux/models/models.reducers.test.ts | 8 +- src/redux/models/models.types.ts | 2 +- src/redux/overlays/overlays.reducers.test.ts | 79 ++++++++++++++++++ src/redux/overlays/overlays.reducers.ts | 7 ++ src/redux/overlays/overlays.thunks.ts | 2 +- src/redux/project/project.reducers.test.ts | 77 ++++++++++++++++++ src/redux/project/project.reducers.ts | 13 ++- src/redux/project/project.thunks.ts | 3 +- 16 files changed, 303 insertions(+), 15 deletions(-) create mode 100644 src/models/fixtures/backgroundsFixture.ts create mode 100644 src/models/fixtures/overlaysFixture.ts create mode 100644 src/models/fixtures/projectFixture.ts create mode 100644 src/redux/backgrounds/backgrounds.reducers.test.ts create mode 100644 src/redux/overlays/overlays.reducers.test.ts create mode 100644 src/redux/project/project.reducers.test.ts 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 3e62bb01..fc02472a 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 00000000..e06e5c15 --- /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 00000000..c0a26efd --- /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 00000000..99e01bb3 --- /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 30395bcb..776993ed 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 00000000..9b99161a --- /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 6c099f17..2f0868b4 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 ee0b860f..3741a1c8 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 c130ae0e..1677afdf 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 06bd1892..6d27b9dc 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 00000000..d0116134 --- /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 09863b89..99e493ea 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 3dc3c70e..6a019333 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 00000000..28b9ef70 --- /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 aee885ae..435d55e8 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 d99f5d55..f3d9fbe2 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); -- GitLab