diff --git a/src/hooks/test/useFetchProjectList.test.ts b/src/hooks/test/useFetchProjectList.test.ts index fbe652e..a9e17e5 100644 --- a/src/hooks/test/useFetchProjectList.test.ts +++ b/src/hooks/test/useFetchProjectList.test.ts @@ -40,7 +40,7 @@ describe('useFetchProjectList', () => { }); it('fetches projects on mount', async () => { - const { result } = renderHook(() => useFetchProjectList('', 0, 9, false)); + const { result } = renderHook(() => useFetchProjectList('mouad', 0, 9, false)); await waitFor(() => { expect(result.current.projects).toEqual([ diff --git a/src/hooks/test/useHandlePinnedProjectList.test.tsx b/src/hooks/test/useHandlePinnedProjectList.test.tsx index c6bc948..fff38f4 100644 --- a/src/hooks/test/useHandlePinnedProjectList.test.tsx +++ b/src/hooks/test/useHandlePinnedProjectList.test.tsx @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Queries, renderHook, RenderHookOptions, waitFor } from '@testing-library/react'; +import { act, Queries, renderHook, RenderHookOptions, waitFor } from '@testing-library/react'; import { useHandlePinnedProjectList } from '@/hooks/useHandlePinnedProjectList'; import { afterEach, beforeEach, describe, expectTypeOf, it, Mock, vi } from 'vitest'; import { @@ -12,6 +12,11 @@ import { PinnedProjectProviderProps, usePinnedProjectDispatch, } from '@/store/contexts/ProjectContext'; +import { fetchPinnedProjects, pinProject } from '@/shared/services/pinnedProjectService.ts'; +import React from 'react'; +import { PINNED_PROJECT_ACTION } from '@/shared/enum/project.ts'; +import { v4 as uuidv4 } from 'uuid'; +import { notifyToast } from '@/shared/notification/notification.tsx'; const mockProjectsApiResponse = [ { @@ -52,18 +57,28 @@ const mockProjectsApiResponse = [ vi.mock('@/envVariables', () => ({ getEnvVariables: vi.fn(() => 'https://mockapi.com'), })); +vi.mock('@/shared/notification/notification'); +vi.mock('uuid', () => ({ + v4: vi.fn(() => 'mocked-uuid'), +})); +vi.mock('@/shared/services/pinnedProjectService'); vi.mock('@/store/contexts/ProjectContext', async (importOriginal) => { const actual: Mock = await importOriginal(); - const mockDispatch = vi.fn(); return { ...actual, - usePinnedProjectDispatch: vi.fn(() => mockDispatch), + usePinnedProject: vi.fn(), + usePinnedProjectDispatch: vi.fn(() => { + return { + dispatch: vi.fn(), + }; + }), }; }); describe('useHandlePinnedProjectList', () => { - const mockUsePinnedProjectDispatch = usePinnedProjectDispatch as Mock; + const mockUsePinnedProjectDispatch = usePinnedProjectDispatch as Mock; + const mockDispatch = vi.fn().mockImplementation(vi.fn()); beforeEach(() => { global.fetch = vi.fn(); @@ -75,10 +90,10 @@ describe('useHandlePinnedProjectList', () => { }); it('should trigger getPinnedProject method on init and call dispatch to update pinned project list correctly', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - json: async () => mockProjectsApiResponse, - }); + const mockFetchPinnedProjects = fetchPinnedProjects as Mock; + mockFetchPinnedProjects.mockResolvedValueOnce(mockProjectsApiResponse); + + mockUsePinnedProjectDispatch.mockReturnValue(mockDispatch); const wrapper = ({ children, initialValue }: PinnedProjectProviderProps) => ( @@ -93,9 +108,91 @@ describe('useHandlePinnedProjectList', () => { expectTypeOf(result.current.getPinnedProjects).toBeFunction(); expectTypeOf(result.current.handleUnpinProject).toBeFunction(); expectTypeOf(result.current.handlePinProject).toBeFunction(); - expect(global.fetch).toHaveBeenCalledTimes(1); - expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/pinned?userId=me00247'); + expect(fetchPinnedProjects).toHaveBeenCalledTimes(1); + expect(fetchPinnedProjects).toHaveBeenCalledWith('me00247'); + expect(fetchPinnedProjects).toHaveReturned(mockProjectsApiResponse); expect(mockUsePinnedProjectDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith({ + type: PINNED_PROJECT_ACTION.INIT_LIST, + payload: mockProjectsApiResponse, + }); + }); + }); + + it('should call handlePinProject that calls pinProject and dispatch and update pinned project list correctly', async () => { + const mockPinProject = pinProject as Mock; + const mockPinProjectResponse = { + id: '3', + name: 'Bilan previsionnel 2025', + createdBy: 'zayd guillaume pegase', + creationDate: '2024-07-25T10:09:41', + studies: [7, 8], + tags: ['figma', 'config', 'modal'], + description: 'In the world of software development, achieving perfection is a journey rather than a destination.', + projectId: '3', + pinned: true, + }; + mockPinProject.mockResolvedValueOnce(mockPinProjectResponse); + + mockUsePinnedProjectDispatch.mockReturnValue(mockDispatch); + + const id = uuidv4(); + + const wrapper = ({ children, initialValue }: PinnedProjectProviderProps) => ( + + ); + + const { result } = renderHook(() => useHandlePinnedProjectList(), { + wrapper, + initialProps: { initialValue: { pinnedProject: [] } }, + } as RenderHookOptions<{ initialValue: { pinnedProject: never[] } }, Queries>); + + await act(async () => result.current.handlePinProject('me00247')); + + await waitFor(() => { + expect(pinProject).toHaveBeenCalledWith('me00247'); + expect(pinProject).toHaveReturned(mockPinProjectResponse); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith({ + type: PINNED_PROJECT_ACTION.ADD_ITEM, + payload: mockPinProjectResponse, + }); + expect(notifyToast).toHaveBeenCalledWith({ + id: id, + type: 'success', + message: 'Project pinned successfully', + }); + }); + }); + + it('should call handlePinProject and catch error when pinProject throws one', async () => { + const mockPinProject = pinProject as Mock; + mockPinProject.mockRejectedValueOnce('error'); + + mockUsePinnedProjectDispatch.mockReturnValue(mockDispatch); + + const id = uuidv4(); + + const wrapper = ({ children, initialValue }: PinnedProjectProviderProps) => ( + + ); + + const { result } = renderHook(() => useHandlePinnedProjectList(), { + wrapper, + initialProps: { initialValue: { pinnedProject: [] } }, + } as RenderHookOptions<{ initialValue: { pinnedProject: never[] } }, Queries>); + + await act(async () => result.current.handlePinProject('me00247')); + + await waitFor(() => { + expect(pinProject).toHaveBeenCalledWith('me00247'); + expect(mockDispatch).toHaveBeenCalledTimes(0); + expect(notifyToast).toHaveBeenCalledWith({ + id: id, + type: 'error', + message: 'Project already pinned', + }); }); }); }); diff --git a/src/hooks/test/useStudyTableDisplay.test.ts b/src/hooks/test/useStudyTableDisplay.test.ts index 09f05a3..28bda89 100644 --- a/src/hooks/test/useStudyTableDisplay.test.ts +++ b/src/hooks/test/useStudyTableDisplay.test.ts @@ -58,7 +58,7 @@ describe('useStudyTableDisplay', () => { expect(result.current.rows).toHaveLength(2); expect(result.current.rows).toEqual(mockResponse.content); expect(result.current.count).toEqual(2); - //expect(global.fetch).toHaveBeenCalledTimes(1); + //expect(global.fetch).toHaveBeenCalledTimes(1); TODO: ANT-2719 expect(global.fetch).toHaveBeenCalledWith( 'https://mockapi.com/v1/study/search?page=1&size=9&projectId=&search=test&sortColumn=status&sortDirection=desc', ); @@ -68,7 +68,7 @@ describe('useStudyTableDisplay', () => { renderHook(() => useStudyTableDisplay({ searchTerm: 'mouad', sortBy: { project: 'asc' }, reloadStudies: true })); }); - //expect(global.fetch).toHaveBeenCalledTimes(1); + //expect(global.fetch).toHaveBeenCalledTimes(1); TODO: ANT-2719 expect(global.fetch).toHaveBeenCalledWith( 'https://mockapi.com/v1/study/search?page=1&size=9&projectId=&search=mouad&sortColumn=project&sortDirection=asc', ); diff --git a/src/hooks/useFetchProjectList.ts b/src/hooks/useFetchProjectList.ts index 8c52259..c4914ad 100644 --- a/src/hooks/useFetchProjectList.ts +++ b/src/hooks/useFetchProjectList.ts @@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from 'react'; import { ProjectInfo } from '@/shared/types/pegase/Project.type.ts'; -import { getEnvVariables } from '@/envVariables.ts'; +import { fetchProjectFromSearchTerm } from '@/shared/services/projectService.ts'; export const useFetchProjectList = ( searchTerm: string, @@ -16,22 +16,22 @@ export const useFetchProjectList = ( ) => { const [projects, setProjects] = useState([]); const [count, setCount] = useState(0); - const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); - const fetchProjects = useCallback(async () => { - const url = `${BASE_URL}/v1/project/search?page=${current + 1}&size=${intervalSize}&search=${searchTerm || ''}`; - fetch(url) - .then((response) => response.json()) - .then((json) => { - setProjects(json.content); - setCount(json.totalElements); - }) - .catch((error) => console.error(error)); - }, [current, intervalSize, searchTerm]); + const fetchProjects = useCallback( + async (searchTerm, current, intervalSize) => { + fetchProjectFromSearchTerm(searchTerm, current, intervalSize) + .then((json) => { + setProjects(json.content); + setCount(json.totalElements); + }) + .catch((error) => console.error(error)); + }, + [current, intervalSize, searchTerm], + ); useEffect(() => { - void fetchProjects(); - }, [BASE_URL, current, searchTerm, intervalSize, shouldRefetch]); + void fetchProjects(searchTerm, current, intervalSize); + }, [current, searchTerm, intervalSize, shouldRefetch]); return { projects, count, refetch: fetchProjects }; }; diff --git a/src/hooks/useHandlePinnedProjectList.ts b/src/hooks/useHandlePinnedProjectList.ts index 70ff9a9..694b264 100644 --- a/src/hooks/useHandlePinnedProjectList.ts +++ b/src/hooks/useHandlePinnedProjectList.ts @@ -45,7 +45,6 @@ export const useHandlePinnedProjectList = () => { const toastId = uuidv4(); try { const newProject = await pinProject(projectId); - if (newProject) { dispatch?.({ type: PINNED_PROJECT_ACTION.ADD_ITEM, diff --git a/src/pages/pegase/home/components/StudyTableDisplay.tsx b/src/pages/pegase/home/components/StudyTableDisplay.tsx index acf37b8..56afd87 100644 --- a/src/pages/pegase/home/components/StudyTableDisplay.tsx +++ b/src/pages/pegase/home/components/StudyTableDisplay.tsx @@ -13,7 +13,7 @@ import { addSortColumn } from './StudyTableUtils'; import StudiesPagination from './StudiesPagination'; import { RowSelectionState } from '@tanstack/react-table'; import StudyCreationModal from '../../studies/StudyCreationModal'; -import { deleteStudy } from '@/pages/pegase/home/components/studyService'; +import { deleteStudy } from '@/shared/services/studyService'; import StdSimpleTable from '@/components/common/data/stdSimpleTable/StdSimpleTable'; import { RdsButton } from 'rte-design-system-react'; import { useStudyTableDisplay } from '@/hooks/useStudyTableDisplay'; diff --git a/src/pages/pegase/home/components/studyService.ts b/src/pages/pegase/home/components/studyService.ts deleted file mode 100644 index 706bfc6..0000000 --- a/src/pages/pegase/home/components/studyService.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { notifyToast } from '@/shared/notification/notification'; -import { getEnvVariables } from '@/envVariables'; - -interface StudyData { - name: string; - createdBy: string; - keywords: string[]; - project: string; - horizon: string; - trajectoryIds: number[]; -} -const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); - -export const saveStudy = async (studyData: StudyData, toggleModal: () => void) => { - try { - const response = await fetch(`${BASE_URL}/v1/study`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(studyData), - }); - if (!response.ok) { - const errorText = await response.text(); - const errorData = JSON.parse(errorText); - throw new Error(`${errorData.message || errorText}`); - } - notifyToast({ - type: 'success', - message: 'Study created successfully', - }); - toggleModal(); - } catch (error: any) { - notifyToast({ - type: 'error', - message: `${error.message}`, - }); - } -}; -export const fetchSuggestedKeywords = async (query: string): Promise => { - const response = await fetch(`${BASE_URL}/v1/study/keywords/search?partialName=${query}`); - if (!response.ok) { - throw new Error('Failed to fetch suggested keywords'); - } - const data = await response.json(); - return data; -}; - -export const deleteStudy = async (id: number) => { - try { - const response = await fetch(`${BASE_URL}/v1/study/${id}`, { - method: 'DELETE', - }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText); - } - notifyToast({ - type: 'success', - message: 'Study deleted successfully', - }); - } catch (error: any) { - notifyToast({ - type: 'error', - message: `${error.message}`, - }); - } -}; diff --git a/src/pages/pegase/studies/KeywordsInput.tsx b/src/pages/pegase/studies/KeywordsInput.tsx index 76becba..d8c8623 100644 --- a/src/pages/pegase/studies/KeywordsInput.tsx +++ b/src/pages/pegase/studies/KeywordsInput.tsx @@ -6,7 +6,7 @@ import { useState } from 'react'; import { RdsButton, RdsIcon, RdsIconId, RdsInputText } from 'rte-design-system-react'; -import { fetchSuggestedKeywords } from '@/pages/pegase/home/components/studyService'; +import { fetchSuggestedKeywords } from '@/shared/services/studyService'; const MAX_KEYWORDS = 6; diff --git a/src/pages/pegase/studies/ProjectInput.tsx b/src/pages/pegase/studies/ProjectInput.tsx index f5c52ff..e7daa89 100644 --- a/src/pages/pegase/studies/ProjectInput.tsx +++ b/src/pages/pegase/studies/ProjectInput.tsx @@ -5,24 +5,14 @@ */ // src/components/ProjectInput.tsx -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { RdsInputText } from 'rte-design-system-react'; -import { getEnvVariables } from '@/envVariables'; +import { fetchProjectsFromPartialName } from '@/shared/services/projectService'; interface ProjectManagerProps { value: string; onChange: (value: string) => void; } -const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); - -const fetchProjects = async (query: string): Promise => { - const response = await fetch(`${BASE_URL}/v1/project/autocomplete?partialName=${query}`); - if (!response.ok) { - throw new Error('Failed to fetch projects'); - } - const data = await response.json(); - return data.map((project: { name: string }) => project.name); // Extract and return only the 'name' property -}; const ProjectInput: React.FC = ({ value, onChange }) => { const [projects, setProjects] = useState([]); @@ -32,7 +22,7 @@ const ProjectInput: React.FC = ({ value, onChange }) => { useEffect(() => { const loadProjects = async () => { try { - const projectList = await fetchProjects(value); + const projectList = await fetchProjectsFromPartialName(value); setProjects(projectList); } catch (error) { setErrorMessage('Failed to fetch projects'); diff --git a/src/pages/pegase/studies/StudyCreationModal.tsx b/src/pages/pegase/studies/StudyCreationModal.tsx index 8eaea9e..dcb9d6e 100644 --- a/src/pages/pegase/studies/StudyCreationModal.tsx +++ b/src/pages/pegase/studies/StudyCreationModal.tsx @@ -10,8 +10,8 @@ import { useTranslation } from 'react-i18next'; import KeywordsInput from '@/pages/pegase/studies/KeywordsInput'; import HorizonInput from '@/pages/pegase/studies/HorizonInput'; import ProjectInput from '@/pages/pegase/studies/ProjectInput'; -import { saveStudy } from '@/pages/pegase/home/components/studyService'; -import { StudyDTO } from '@/shared/types/index'; +import { saveStudy } from '@/shared/services/studyService'; +import { StudyDTO } from '@/shared/types'; interface StudyCreationModalProps { isOpen?: boolean; diff --git a/src/shared/const/apiEndPoint.ts b/src/shared/const/apiEndPoint.ts index 1094c10..8d38b1c 100644 --- a/src/shared/const/apiEndPoint.ts +++ b/src/shared/const/apiEndPoint.ts @@ -9,12 +9,16 @@ import { getEnvVariables } from '@/envVariables.ts'; const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); // STUDY +export const STUDY_ENDPOINT = `${BASE_URL}/v1/study`; export const STUDY_SEARCH_ENDPOINT = `${BASE_URL}/v1/study/search`; +export const STUDY_KEYWORDS_SEARCH_ENDPOINT = `${BASE_URL}/v1/study/keywords/search`; // PINNED PROJECT export const PROJECT_PINNED_ENDPOINT = `${BASE_URL}/v1/project/pinned`; export const PROJECT_UNPIN_ENDPOINT = `${BASE_URL}/v1/project/unpin`; -export const PROJECT_PIN_PROJECT = `${BASE_URL}/v1/project/pin`; +export const PROJECT_PIN_ENDPOINT = `${BASE_URL}/v1/project/pin`; // PROJECT export const PROJECT_ENDPOINT = `${BASE_URL}/v1/project`; +export const PROJECT_AUTOCOMPLETE_ENDPOINT = `${BASE_URL}/v1/project/autocomplete`; +export const PROJECT_SEARCH_ENDPOINT = `${BASE_URL}/v1/project/search`; diff --git a/src/shared/services/pinnedProjectService.ts b/src/shared/services/pinnedProjectService.ts index 9366f7b..3355b55 100644 --- a/src/shared/services/pinnedProjectService.ts +++ b/src/shared/services/pinnedProjectService.ts @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { PROJECT_PIN_PROJECT, PROJECT_PINNED_ENDPOINT, PROJECT_UNPIN_ENDPOINT } from '@/shared/const/apiEndPoint'; +import { PROJECT_PIN_ENDPOINT, PROJECT_PINNED_ENDPOINT, PROJECT_UNPIN_ENDPOINT } from '@/shared/const/apiEndPoint'; import { ProjectInfo } from '@/shared/types/pegase/Project.type'; /** @@ -41,7 +41,7 @@ export const fetchPinnedProjects = async (userId: string) => { export const pinProject = async (projectId: string) => { const userId = 'me00247'; - const apiUrl = `${PROJECT_PIN_PROJECT}?userId=${userId}&projectId=${projectId}`; + const apiUrl = `${PROJECT_PIN_ENDPOINT}?userId=${userId}&projectId=${projectId}`; const response = await fetch(apiUrl, { method: 'POST', diff --git a/src/shared/services/projectService.ts b/src/shared/services/projectService.ts index 486a930..865bb38 100644 --- a/src/shared/services/projectService.ts +++ b/src/shared/services/projectService.ts @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { PROJECT_ENDPOINT } from '@/shared/const/apiEndPoint'; +import { PROJECT_AUTOCOMPLETE_ENDPOINT, PROJECT_ENDPOINT, PROJECT_SEARCH_ENDPOINT } from '@/shared/const/apiEndPoint'; export const deleteProjectById = async (projectId: string) => { const response = await fetch(`${PROJECT_ENDPOINT}/${projectId}`, { @@ -36,3 +36,33 @@ export const fetchProjectDetails = async (projectId: string) => { return await response.json(); }; + +/** + * Retrieve a project from a partial name of project + * + * @param {string} query - Partial name of a project + * @return {Promise} - List of project name + */ +export const fetchProjectsFromPartialName = async (query: string): Promise => { + const response = await fetch(`${PROJECT_AUTOCOMPLETE_ENDPOINT}?partialName=${query}`); + if (!response.ok) { + throw new Error('Failed to fetch projects'); + } + const data = await response.json(); + return data.map((project: { name: string }) => project.name); +}; + +/** + * Retrieve a list of project from a user name + * + * @param searchTerm + * @param current + * @param intervalSize + */ +export const fetchProjectFromSearchTerm = async (searchTerm, current, intervalSize) => { + const response = await fetch( + `${PROJECT_SEARCH_ENDPOINT}?page=${current + 1}&size=${intervalSize}&search=${searchTerm || ''}`, + ); + + return await response.json(); +}; diff --git a/src/shared/services/studyService.ts b/src/shared/services/studyService.ts index 636721c..3cfc0f7 100644 --- a/src/shared/services/studyService.ts +++ b/src/shared/services/studyService.ts @@ -6,6 +6,8 @@ import { PaginatedResponse, StudyDTO } from '@/shared/types'; import { STUDY_SEARCH_ENDPOINT } from '@/shared/const/apiEndPoint'; +import { STUDY_ENDPOINT, STUDY_KEYWORDS_SEARCH_ENDPOINT } from '@/shared/const/apiEndPoint.ts'; +import { notifyToast } from '@/shared/notification/notification.tsx'; /** * Retrieve a list of studies from a term @@ -40,3 +42,84 @@ export const fetchSearchStudies = async ( return { content: json.content, totalElements: json.totalElements }; }; + +/** + * Retrieve a list of suggested keywords from a partial name of a study + * + * @param {string} query - Partial name of a study + * + * @returns {Promise} - Promise object that represents a list of keywords + */ +export const fetchSuggestedKeywords = async (query: string): Promise => { + const response = await fetch(`${STUDY_KEYWORDS_SEARCH_ENDPOINT}?partialName=${query}`); + if (!response.ok) { + throw new Error('Failed to fetch suggested keywords'); + } + return await response.json(); +}; + +/** + * Create a study + * Display toast if creation succeeds or fails + * + * @param {Omit} studyData - Partial study data + * @param {function} toggleModal - Handle toggle of opening modal boolean + */ +export const saveStudy = async ( + studyData: Omit, + toggleModal: () => void, +) => { + try { + const response = await fetch(`${STUDY_ENDPOINT}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(studyData), + }); + if (!response.ok) { + const errorText = await response.text(); + const errorData = JSON.parse(errorText); + throw new Error(`${errorData.message || errorText}`); + } + notifyToast({ + type: 'success', + message: 'Study created successfully', + }); + toggleModal(); + } catch (error: unknown) { + if (error instanceof Error) { + notifyToast({ + type: 'error', + message: `${error.message}`, + }); + } + } +}; + +/** + * Delete a study + * Display toast if deletion succeeds or fails + * + * @param {number} id - Study id + */ +export const deleteStudy = async (id: number) => { + try { + const response = await fetch(`${STUDY_ENDPOINT}/${id}`, { + method: 'DELETE', + }); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText); + } + notifyToast({ + type: 'success', + message: 'Study deleted successfully', + }); + } catch (error: any) { + notifyToast({ + type: 'error', + message: `${error.message}`, + }); + } +}; diff --git a/src/shared/services/test/pinnedProjectService.test.tsx b/src/shared/services/test/pinnedProjectService.test.tsx index d9010bb..9a3026c 100644 --- a/src/shared/services/test/pinnedProjectService.test.tsx +++ b/src/shared/services/test/pinnedProjectService.test.tsx @@ -8,8 +8,6 @@ import { fetchPinnedProjects, pinProject, unpinProject } from '../pinnedProjectS import { vi } from 'vitest'; import { waitFor } from '@testing-library/react'; -const mockFetch = vi.fn(); -global.fetch = mockFetch; vi.mock('@/shared/notification/notification'); vi.mock('@/envVariables', () => ({ getEnvVariables: vi.fn(() => 'https://mockapi.com'), diff --git a/src/shared/services/test/projectService.test.tsx b/src/shared/services/test/projectService.test.tsx index 580681c..7b1e498 100644 --- a/src/shared/services/test/projectService.test.tsx +++ b/src/shared/services/test/projectService.test.tsx @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { deleteProjectById, fetchProjectDetails } from '../projectService.ts'; +import { deleteProjectById, fetchProjectDetails, fetchProjectsFromPartialName } from '../projectService.ts'; import { vi } from 'vitest'; import { waitFor } from '@testing-library/react'; @@ -108,3 +108,58 @@ describe('fetchProjectDetails', () => { await expect(async () => fetchProjectDetails(projectId)).rejects.toThrowError('Network error'); }); }); + +describe('fetchProjectsFromPartialName', () => { + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should delete a pinned project from pinned project list', async () => { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => { + return [ + { + id: '123', + name: 'Bilan prévisionnel 2023', + description: 'Project Description', + createdBy: 'User A', + creationDate: '2024-01-01', + tags: ['tag1', 'tag2'], + }, + { + id: '123', + name: 'Bilan prévisionnel 2019', + description: 'Project Description', + createdBy: 'User B', + creationDate: '2013-08-01', + tags: ['tag3', 'tag4'], + }, + ]; + }, + }); + + const result = await fetchProjectsFromPartialName('name'); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/autocomplete?partialName=name'); + expect(result).toEqual(['Bilan prévisionnel 2023', 'Bilan prévisionnel 2019']); + }); + }); + + it('should handle delete failure gracefully', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + text: async () => 'Error', + }); + + await expect(async () => fetchProjectsFromPartialName('name')).rejects.toThrowError('Failed to fetch projects'); + }); +}); diff --git a/src/shared/services/test/studyService.test.tsx b/src/shared/services/test/studyService.test.tsx index 581ef51..a4af1c6 100644 --- a/src/shared/services/test/studyService.test.tsx +++ b/src/shared/services/test/studyService.test.tsx @@ -6,8 +6,10 @@ import { vi } from 'vitest'; import { waitFor } from '@testing-library/react'; -import { fetchSearchStudies } from '@/shared/services/studyService.ts'; +import { deleteStudy, fetchSearchStudies, fetchSuggestedKeywords, saveStudy } from '@/shared/services/studyService.ts'; +import { notifyToast } from '@/shared/notification/notification.tsx'; +vi.mock('@/shared/notification/notification'); vi.mock('@/envVariables', () => ({ getEnvVariables: vi.fn(() => 'https://mockapi.com'), })); @@ -24,21 +26,25 @@ describe('fetchSearchStudies', () => { it('should fetch study list', async () => { //Successful fetch response mock + const mockResponse = { + content: [ + { + id: 1, + name: 'Project 1', + createdBy: 'User A', + creationDate: '2023-10-01', + keywords: ['Keyword1', 'Keyword2'], + project: '1', + status: 'IN_PROGRESS', + horizon: '2030-2031', + trajectoryIds: [1, 7], + }, + ], + totalElements: 1, + }; global.fetch = vi.fn().mockResolvedValueOnce({ ok: true, - json: () => - Promise.resolve({ - content: [ - { - projectId: '1', - name: 'Project 1', - tags: ['Tag1', 'Tag2'], - creationDate: '2023-10-01', - createdBy: 'User A', - }, - ], - totalElements: 1, - }), + json: () => Promise.resolve(mockResponse), }); const result = await fetchSearchStudies('test', '124', 3, 10, { column: 'asc' }); @@ -48,18 +54,7 @@ describe('fetchSearchStudies', () => { expect(global.fetch).toHaveBeenCalledWith( `https://mockapi.com/v1/study/search?page=4&size=10&projectId=124&search=test&sortColumn=column&sortDirection=asc`, ); - expect(result).toEqual({ - content: [ - { - projectId: '1', - name: 'Project 1', - tags: ['Tag1', 'Tag2'], - creationDate: '2023-10-01', - createdBy: 'User A', - }, - ], - totalElements: 1, - }); + expect(result).toEqual(mockResponse); }); }); @@ -74,3 +69,157 @@ describe('fetchSearchStudies', () => { ); }); }); + +describe('fetchSuggestedKeywords', () => { + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return suggested keywords', async () => { + const mockResponse = ['keyword1', 'keyword2', 'keyword3']; + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockResponse), + }); + + const result = await fetchSuggestedKeywords('test'); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/study/keywords/search?partialName=test'); + expect(result).toEqual(mockResponse); + }); + + it('should handle fetch failure gracefully', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + }); + + await expect(async () => fetchSuggestedKeywords('test')).rejects.toThrowError('Failed to fetch suggested keywords'); + }); +}); + +describe('saveStudy', () => { + const mockStudy = { + id: 1, + name: 'Project 1', + createdBy: 'User A', + keywords: ['Keyword1', 'Keyword2'], + project: '1', + horizon: '2030-2031', + trajectoryIds: [1, 7], + }; + + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should create a study', async () => { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(), + }); + const toggleModal = vi.fn(); + + await saveStudy(mockStudy, toggleModal); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/study', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(mockStudy), + }); + expect(notifyToast).toHaveBeenCalledWith({ + type: 'success', + message: 'Study created successfully', + }); + expect(toggleModal).toHaveBeenCalledTimes(1); + }); + + it('should throw an error message', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + text: () => 'error', + }); + + const result = await saveStudy(mockStudy, vi.fn()); + expect(result).toEqual(undefined); + }); + + it('should handle fetch failure and display a notification', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockRejectedValueOnce(new Error('Failed to create study')); + + await saveStudy(mockStudy, vi.fn()); + + expect(notifyToast).toHaveBeenCalledWith({ + type: 'error', + message: 'Failed to create study', + }); + }); +}); + +describe('deleteStudy', () => { + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should delete a study', async () => { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(), + }); + + await deleteStudy(5); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/study/5', { + method: 'DELETE', + }); + expect(notifyToast).toHaveBeenCalledWith({ + type: 'success', + message: 'Study deleted successfully', + }); + }); + + it('should throw an error message', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + text: () => 'error', + }); + + const result = await deleteStudy(2); + expect(result).toEqual(undefined); + }); + + it('should handle delete failure and display a notification', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockRejectedValueOnce({ message: 'Failed to delete study' }); + + await deleteStudy(2); + + expect(notifyToast).toHaveBeenCalledWith({ + type: 'error', + message: 'Failed to delete study', + }); + }); +});