diff --git a/package-lock.json b/package-lock.json index 8cfd5fa..5a23802 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "@hookform/resolvers": "3.3.2", "@reduxjs/toolkit": "1.9.7", "axios": "1.6.2", - "axios-mock-adapter": "^1.22.0", "classnames": "2.5.1", "i18next": "23.6.0", "i18next-browser-languagedetector": "7.1.0", @@ -27,8 +26,8 @@ }, "devDependencies": { "@tailwindcss/typography": "0.5.10", - "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "14.2.2", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^14.2.2", "@types/js-cookie": "3.0.6", "@types/react": "18.2.15", "@types/react-dom": "18.2.7", @@ -124,9 +123,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", "dev": true, "engines": { "node": ">=6.9.0" @@ -210,9 +209,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", - "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -233,13 +232,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", "to-fast-properties": "^2.0.0" }, "engines": { diff --git a/package.json b/package.json index ac0d66b..88e089d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "@hookform/resolvers": "3.3.2", "@reduxjs/toolkit": "1.9.7", "axios": "1.6.2", - "axios-mock-adapter": "^1.22.0", "classnames": "2.5.1", "i18next": "23.6.0", "i18next-browser-languagedetector": "7.1.0", @@ -30,8 +29,8 @@ }, "devDependencies": { "@tailwindcss/typography": "0.5.10", - "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "14.2.2", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^14.2.2", "@types/js-cookie": "3.0.6", "@types/react": "18.2.15", "@types/react-dom": "18.2.7", diff --git a/src/__tests__/components/studentFilters/ApiGetUserDetail.test.tsx b/src/__tests__/components/studentFilters/ApiGetUserDetail.test.tsx new file mode 100644 index 0000000..225ac52 --- /dev/null +++ b/src/__tests__/components/studentFilters/ApiGetUserDetail.test.tsx @@ -0,0 +1,95 @@ +import React, { useContext } from 'react' +import axios from 'axios' +import { fireEvent, render } from '@testing-library/react' +// eslint-disable-next-line import/no-extraneous-dependencies +import MockAdapter from 'axios-mock-adapter' +import { FetchStudentsListHome } from '../../../api/FetchStudentsList' +import { + StudentFiltersProvider, + StudentFiltersContext, +} from '../../../context/StudentFiltersContext' +import StudentFiltersContent from '../../../components/studentFilters/StudentFiltersContent' + +const mockAxios = new MockAdapter(axios) + +describe('FetchStudentsListHome function', () => { + afterEach(() => { + mockAxios.reset() + }) + + it('should fetch student list for home', async () => { + const selectedRoles = ['role1', 'role2'] + + const expectedUrl = + 'https://itaperfils.eurecatacademy.org/api/v1/student/list/for-home?specialization=role1,role2' + + const mockData = [ + { id: 1, name: 'Student 1' }, + { id: 2, name: 'Student 2' }, + ] + + mockAxios.onGet(expectedUrl).reply(200, mockData) + + const result = await FetchStudentsListHome(selectedRoles) + + expect(result).toEqual(mockData) + }) + + it('should handle errors', async () => { + const selectedRoles: string[] = [] + + const expectedUrl = + 'https://itaperfils.eurecatacademy.org/api/v1/student/list/for-home' + + mockAxios.onGet(expectedUrl).reply(500) + + await expect(FetchStudentsListHome(selectedRoles)).rejects.toThrow() + }) +}) + +describe('StudentFiltersContent component', () => { + it('renders StudentFiltersContent and handles user events', () => { + const { getByTestId } = render( + + + , + ) + + // Verifies that the component renders correctly + expect(getByTestId('student-filters-content')).toBeDefined() + }) + + it('should add and remove role', () => { + const TestComponent = () => { + const { selectedRoles, addRole, removeRole } = + useContext(StudentFiltersContext) || {} + + if (!selectedRoles || !addRole || !removeRole) { + throw new Error('Context is undefined') + } + return ( +
+ + +
{selectedRoles.join(',')}
+
+ ) + } + + const { getByText, queryByText } = render( + + + , + ) + + fireEvent.click(getByText('Add role')) + expect(getByText('test')).toBeInTheDocument() + + fireEvent.click(getByText('Remove role')) + expect(queryByText('test')).not.toBeInTheDocument() + }) +}) diff --git a/src/__tests__/components/studentFilters/Landing.test.tsx b/src/__tests__/components/studentFilters/Landing.test.tsx new file mode 100644 index 0000000..3325295 --- /dev/null +++ b/src/__tests__/components/studentFilters/Landing.test.tsx @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/react' +import { Provider } from 'react-redux' // Import Provider from react-redux +import Landing from '../../../components/landing/Landing' + +import { store } from '../../../store/store' + +describe('StudentDetailsLayout', () => { + it('should render the studentDetailsLayout component correctly', () => { + const { container } = render( + + + , + ) + expect(container).toBeInTheDocument() + }) + + test('renders all the different cards', () => { + expect(screen.queryByTestId('MenuNavbar')).toBeDefined() + expect(screen.queryByTestId('UserNavbar')).toBeDefined() + expect(screen.queryByTestId('StudentFiltersLayout')).toBeDefined() + expect(screen.queryByTestId('StudentLayout')).toBeDefined() + expect(screen.queryByTestId('StudentDetailsLayout')).toBeDefined() + }) +}) diff --git a/src/__tests__/components/studentFilters/StudenFtiltersContext.test.tsx b/src/__tests__/components/studentFilters/StudenFtiltersContext.test.tsx new file mode 100644 index 0000000..f91e89c --- /dev/null +++ b/src/__tests__/components/studentFilters/StudenFtiltersContext.test.tsx @@ -0,0 +1,43 @@ +import { renderHook, act } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { ReactNode, useContext } from 'react' +import { + StudentFiltersProvider, + StudentFiltersContext, +} from '../../../context/StudentFiltersContext' + +describe('StudentFiltersProvider', () => { + it('should add role to selectedRoles', () => { + const wrapper = ( + { children }: { children: ReactNode }, // Explicitly type children prop + ) => {children} + + const { result } = renderHook(() => useContext(StudentFiltersContext), { + wrapper, + }) + + act(() => { + result.current?.addRole('Role1') // Guard against result.current being undefined + }) + + expect(result.current?.selectedRoles).toEqual(['Role1']) // Guard against result.current being undefined + }) + + it('should remove role from selectedRoles', () => { + const wrapper = ( + { children }: { children: ReactNode }, // Explicitly type children prop + ) => {children} + + const { result } = renderHook(() => useContext(StudentFiltersContext), { + wrapper, + }) + + act(() => { + result.current?.addRole('Role1') // Guard against result.current being undefined + result.current?.addRole('Role2') // Guard against result.current being undefined + result.current?.removeRole('Role1') // Guard against result.current being undefined + }) + + expect(result.current?.selectedRoles).toEqual(['Role2']) // Guard against result.current being undefined + }) +}) diff --git a/src/__tests__/components/studentFilters/StudentFiltersContent.test.tsx b/src/__tests__/components/studentFilters/StudentFiltersContent.test.tsx new file mode 100644 index 0000000..90f30e2 --- /dev/null +++ b/src/__tests__/components/studentFilters/StudentFiltersContent.test.tsx @@ -0,0 +1,69 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { render, RenderResult, waitFor } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; +import StudentFiltersProvider from '../../../components/studentFilters/StudentFiltersContent'; +import { StudentFiltersContext } from '../../../context/StudentFiltersContext'; + +describe('StudentFiltersProvider', () => { + let mock: MockAdapter; + + beforeAll(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.reset(); + }); + + afterAll(() => { + mock.restore(); + }); + + const selectedRoles: string[] = []; + const addRole = () => { }; + const removeRole = () => { }; + + const value = { + selectedRoles, + addRole, + removeRole, + }; + + const rolesData: string[] | undefined = []; + const developmentData: string[] | undefined = []; + + test('renders student filters correctly', async () => { + // Mock API responses + mock + .onGet('https://itaperfils.eurecatacademy.org/api/v1/specialization/list') + .reply(200, rolesData); + + mock + .onGet('https://itaperfils.eurecatacademy.org/api/v1/development/list') + .reply(200, developmentData); + + let getByText: RenderResult['getByText']; + + // Render the component + await act(async () => { + const renderResult = render( + + + + ); + getByText = renderResult.getByText; + + // Wait for data to be fetched and rendered + await waitFor(() => { + rolesData.forEach((role) => { + expect(getByText(role)).toBeInTheDocument(); + }); + + developmentData.forEach((development) => { + expect(getByText(development)).toBeInTheDocument(); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/api/FetchStudentsList.ts b/src/api/FetchStudentsList.ts index d2e8b6b..e23954f 100644 --- a/src/api/FetchStudentsList.ts +++ b/src/api/FetchStudentsList.ts @@ -1,15 +1,26 @@ -import axios from 'axios' -import { IStudentList } from '../interfaces/interfaces' +// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars +import axios, { AxiosError } from 'axios'; +import { IStudentList } from '../interfaces/interfaces'; // eslint-disable-next-line consistent-return -export const FetchStudentsListHome = async () => { +export const FetchStudentsListHome = async (selectedRoles:Array= []) => { + try { - const response = await axios.get( - 'https://itaperfils.eurecatacademy.org/api/v1/student/list/for-home', - ) - return response.data - } catch (e) { + let queryParams = ''; + + // Construir la cadena de consulta para roles seleccionados + if (selectedRoles.length > 0) { + queryParams += `specialization=${selectedRoles.join(',')}`; + } + + // Construir la URL completa con la cadena de consulta + const url = `https://itaperfils.eurecatacademy.org/api/v1/student/list/for-home${queryParams ? `?${queryParams}` : ''}`; + + const response = await axios.get(url); + return response.data; + // @ts-expect-error throws AxiosError exception + } catch (e: AxiosError) { // eslint-disable-next-line no-console - console.error(e) + throw new DOMException(e.message, 'ConnectionFailed'); } -} + }; \ No newline at end of file diff --git a/src/components/landing/Landing.tsx b/src/components/landing/Landing.tsx index a0da1e4..44c4861 100644 --- a/src/components/landing/Landing.tsx +++ b/src/components/landing/Landing.tsx @@ -3,19 +3,25 @@ import UserNavbar from '../userNavBar/UserNavbar' import StudentDetailsLayout from '../studentDetail/StudentDetailsLayout' import StudentsLayout from '../students/StudentsLayout' import StudentFiltersLayout from '../studentFilters/StudentFiltersLayout' +import { StudentFiltersProvider } from '../../context/StudentFiltersContext' const Landing = () => (
- + {/* Added data-testid */}
- -
-
- - + {/* Added data-testid */} + +
+
+ {' '} + {/* Added data-testid */} + {' '} + {/* Added data-testid */} +
+ {' '} + {/* Added data-testid */}
- -
+
) diff --git a/src/components/studentFilters/StudentFiltersContent.tsx b/src/components/studentFilters/StudentFiltersContent.tsx index a9f42a8..d89fe7b 100644 --- a/src/components/studentFilters/StudentFiltersContent.tsx +++ b/src/components/studentFilters/StudentFiltersContent.tsx @@ -1,83 +1,115 @@ import axios from 'axios' -import { useEffect, useState } from 'react' +import React, { useContext, useEffect, useMemo, useState } from 'react' +import { StudentFiltersContext } from '../../context/StudentFiltersContext' -const StudentFiltersContent: React.FC = () => { +const StudentFiltersProvider: React.FC = () => { const [roles, setRoles] = useState([]) const [development, setDevelopment] = useState([]) + const context = useContext(StudentFiltersContext) + + if (!context) { + throw new Error('StudentFiltersContext must be provided') + } + + const { selectedRoles, addRole, removeRole } = context + + const value = useMemo( + () => ({ + selectedRoles, + addRole, + removeRole, + }), + [selectedRoles, addRole, removeRole], + ) + const urlRoles = 'https://itaperfils.eurecatacademy.org/api/v1/specialization/list' const urlDevelopment = 'https://itaperfils.eurecatacademy.org/api/v1/development/list' - const fetchData = ( + const fetchData = async ( url: string, + setData: React.Dispatch>, ) => { - axios - .get(url) - .then((response) => { - setData(response.data) - }) - .catch((error) => { - // eslint-disable-next-line no-console - console.error(error) - }) + try { + const response = await axios.get(url) + setData(response.data) + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching data:', error) + // Handle error gracefully, e.g., show a message to the user + } } useEffect(() => { fetchData(urlRoles, setRoles) - }, [urlRoles]) - - useEffect(() => { fetchData(urlDevelopment, setDevelopment) - }, [urlDevelopment]) + }, [urlRoles, urlDevelopment]) + + const toggleRole = (role: string) => { + if (selectedRoles.includes(role)) { + removeRole(role) + } else { + addRole(role) + } + } return ( -
-

Filtros

-
-
-

Roles

-
- {roles.map((role) => ( - - ))} + +
+

Filtros

+
+
+

Roles

+
+ {roles.map((role) => ( + + ))} +
-
-
-

Desarrollo

-
- {development.map((role) => ( - - ))} +
+

Desarrollo

+
+ {development.map((tag) => ( + + ))} +
-
+
) } -export default StudentFiltersContent +export default StudentFiltersProvider diff --git a/src/components/studentFilters/StudentFiltersLayout.tsx b/src/components/studentFilters/StudentFiltersLayout.tsx index 34dada3..e4126f1 100644 --- a/src/components/studentFilters/StudentFiltersLayout.tsx +++ b/src/components/studentFilters/StudentFiltersLayout.tsx @@ -2,8 +2,8 @@ import StudentFiltersContent from './StudentFiltersContent' const StudentFiltersLayout: React.FC = () => (
- +
) -export default StudentFiltersLayout +export default StudentFiltersLayout \ No newline at end of file diff --git a/src/components/students/StudentsList.tsx b/src/components/students/StudentsList.tsx index 1743f50..63c32a6 100644 --- a/src/components/students/StudentsList.tsx +++ b/src/components/students/StudentsList.tsx @@ -1,28 +1,31 @@ -import { useEffect, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import StudentCard from './StudentCard' import { useAppSelector } from '../../hooks/ReduxHooks' import { IStudentList } from '../../interfaces/interfaces' import { FetchStudentsListHome } from '../../api/FetchStudentsList' +import { StudentFiltersContext } from '../../context/StudentFiltersContext'; const StudentsList: React.FC = () => { const isPanelOpen = useAppSelector( (state) => state.ShowUserReducer.isUserPanelOpen, - ) + ); + + const studentFilterContext = useContext(StudentFiltersContext) - const [students, setStudents] = useState() + const [students, setStudents] = useState() useEffect(() => { const fetchStudents = async () => { try { - const studentsList = await FetchStudentsListHome() - setStudents(studentsList) + const studentsList = await FetchStudentsListHome( studentFilterContext?.selectedRoles || []); + setStudents(studentsList); } catch (error) { // eslint-disable-next-line no-console - console.error('Error fetching students:', error) + console.error('Error fetching students:', error); } - } - fetchStudents() - }, []) + }; + fetchStudents(); + }, [studentFilterContext?.selectedRoles]); return (
void; + removeRole: (role: string) => void; +} + +export const StudentFiltersContext = createContext(undefined); + +interface StudentFiltersProviderProps { + children: ReactNode; +} + +export const StudentFiltersProvider: React.FC = ({ children }) => { + const [selectedRoles, setSelectedRoles] = useState([]); + + const addRole = useCallback((role: string) => { + setSelectedRoles(prevRoles => [...prevRoles, role]); + }, []); + + const removeRole = useCallback((role: string) => { + setSelectedRoles(prevRoles => prevRoles.filter(r => r !== role)); + }, []); + + const value = useMemo(() => ({ selectedRoles, addRole, removeRole }), [selectedRoles, addRole, removeRole]); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/store/reducers/getUserDetail/apiGetUserDetail.ts b/src/store/reducers/getUserDetail/apiGetUserDetail.ts index 55c33ae..5d2fac0 100644 --- a/src/store/reducers/getUserDetail/apiGetUserDetail.ts +++ b/src/store/reducers/getUserDetail/apiGetUserDetail.ts @@ -1,21 +1,29 @@ -/* eslint-disable no-param-reassign */ -import { createSlice } from '@reduxjs/toolkit' +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; const initialState = { - isUserPanelOpen: false, + isUserPanelOpen: false, + filteredStudents: '' } -const studentDetailPanel = createSlice({ - name: 'userDetail', - initialState, - reducers: { - openUserPanel: (state) => { - state.isUserPanelOpen = true +const showUserInfo = createSlice({ + name: "showUserReducer", + initialState, + reducers: { + openUserPanel: (state) => { + // eslint-disable-next-line no-param-reassign + state.isUserPanelOpen = true; + }, + closeUserPanel: (state) => { + // eslint-disable-next-line no-param-reassign + state.isUserPanelOpen = false; + }, + setFilteredStudents: (state, action: PayloadAction) => { + // eslint-disable-next-line no-param-reassign + state.filteredStudents = action.payload; + }, }, - closeUserPanel: (state) => { - state.isUserPanelOpen = false - }, - }, -}) -export const { openUserPanel, closeUserPanel } = studentDetailPanel.actions -export default studentDetailPanel.reducer +}); + +export const { openUserPanel, closeUserPanel, setFilteredStudents } = showUserInfo.actions; + +export default showUserInfo.reducer; \ No newline at end of file