diff --git a/client/src/__tests__/pages/Admin/Images.test.tsx b/client/src/__tests__/pages/Admin/Images.test.tsx new file mode 100644 index 0000000..3e44ff5 --- /dev/null +++ b/client/src/__tests__/pages/Admin/Images.test.tsx @@ -0,0 +1 @@ +//a;ready being tested by ImageGallery.test.tsx \ No newline at end of file diff --git a/client/src/__tests__/pages/Admin/Stats.test.tsx b/client/src/__tests__/pages/Admin/Stats.test.tsx new file mode 100644 index 0000000..62e7084 --- /dev/null +++ b/client/src/__tests__/pages/Admin/Stats.test.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { render, screen, waitFor, act } from "@testing-library/react"; +import Stats from "../../../pages/Admin/Stats"; +import ApiService from '../../../services/ApiService'; +import '@testing-library/jest-dom'; + +jest.mock('../../../services/ApiService', () => ({ + getStats: jest.fn(), +})); + +describe("Stats Component", () => { + beforeEach(() => { + jest.clearAllMocks(); + (ApiService.getStats as jest.Mock).mockReset(); + }); + + test("renders loading state initially", async () => { + (ApiService.getStats as jest.Mock).mockImplementationOnce(() => + new Promise((resolve) => { + setTimeout(() => { + resolve({ + quizzes: [], + total: 0, + }); + }, 100); + }) + ); + + await act(async () => { + render(); + }); + + expect(screen.getByRole("progressbar")).toBeInTheDocument(); + }); + + test("fetches and displays data", async () => { + const mockStats = { + quizzes: [{ _id: "1", title: "Mock Quiz", created_at: "2025-03-01", updated_at: "2025-03-05", email: "teacher@example.com" }], + total: 5, + }; + + (ApiService.getStats as jest.Mock).mockResolvedValueOnce(mockStats); + + + await act(async () => { + render(); + }); + + await waitFor(() => screen.queryByRole("progressbar")); + + expect(screen.getByText("Quiz du Mois")).toBeInTheDocument(); + expect(screen.getByText(mockStats.quizzes.length)).toBeInTheDocument(); + expect(screen.getByText("Quiz total")).toBeInTheDocument(); + expect(screen.getByText(mockStats.quizzes.length)).toBeInTheDocument(); + expect(screen.getByText("Enseignants")).toBeInTheDocument(); + expect(screen.getByText(mockStats.total)).toBeInTheDocument(); + }); + + test("should display the AdminTable mock component", async () => { + const mockStats = { + quizzes: [{ _id: "1", title: "Mock Quiz", created_at: "2025-03-01", updated_at: "2025-03-05", email: "teacher@example.com" }], + total: 5, + }; + + (ApiService.getStats as jest.Mock).mockResolvedValueOnce(mockStats); + + + await act(async () => { + render(); + }); + + expect(screen.getByRole('columnheader', { name: /enseignant/i })).toBeInTheDocument(); + + }); +}); diff --git a/client/src/__tests__/pages/Admin/Users.test.tsx b/client/src/__tests__/pages/Admin/Users.test.tsx new file mode 100644 index 0000000..2a907b4 --- /dev/null +++ b/client/src/__tests__/pages/Admin/Users.test.tsx @@ -0,0 +1,93 @@ +import { render, screen, waitFor, act, fireEvent, within } from '@testing-library/react'; +import Users from '../../../pages/Admin/Users'; +import ApiService from '../../../services/ApiService'; +import '@testing-library/jest-dom'; +import { AdminTableType } from '../../../Types/AdminTableType'; +import React from 'react'; + +jest.mock('../../../services/ApiService'); +jest.mock('../../../components/AdminTable/AdminTable', () => ({ + __esModule: true, + default: ({ data, onDelete }: any) => ( + + + + + + + + + + {data.map((user: any) => ( + + + + + + ))} + +
EnseignantEmailActions
{user.name}{user.email} + +
+ ), +})); + +describe('Users Component', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders users after fetching data', async () => { + const mockUsers: AdminTableType[] = [ + { _id: '1', name: 'John Doe', email: 'john.doe@example.com', created_at: new Date('2021-01-01'), roles: ['admin'] }, + { _id: '2', name: 'Jane Smith', email: 'jane.smith@example.com', created_at: new Date('2021-02-01'), roles: ['user'] }, + ]; + + (ApiService.getUsers as jest.Mock).mockResolvedValueOnce(mockUsers); + + + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('jane.smith@example.com')).toBeInTheDocument(); + }); + }); + + it('handles delete user action', async () => { + const mockUsers: AdminTableType[] = []; + + (ApiService.getUsers as jest.Mock).mockResolvedValueOnce(mockUsers); + + await act(async () => { + render(); + }); + + const columnHeader = screen.getByRole('columnheader', { name: /enseignant/i }); + expect(columnHeader).toBeInTheDocument(); + expect(screen.queryByText('John Doe')).not.toBeInTheDocument(); + }); + + it('calls handleDelete when delete button is clicked', async () => { + + const mockUsers: AdminTableType[] = [{ _id: '1', name: 'John Doe', email: 'john.doe@example.com', created_at: new Date('2021-01-01'), roles: ['Admin'] }]; + (ApiService.getUsers as jest.Mock).mockResolvedValueOnce(mockUsers); + + await act(async () => { + render(); + }); + + await waitFor(() => screen.getByText("John Doe")); + console.log(screen.debug()); + const userRow = screen.getByText("John Doe").closest("tr"); + if (userRow) { + const deleteButton = within(userRow).getByRole('button'); + fireEvent.click(deleteButton); + expect(screen.queryByText("John Doe")).not.toBeInTheDocument(); + }else { + throw new Error("User row not found"); + } + }); +}); diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx new file mode 100644 index 0000000..ab78bb3 --- /dev/null +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -0,0 +1,225 @@ +import React, { useState, useEffect } from "react"; +import { + Box, + CircularProgress, + Button, + IconButton, + Card, + CardContent, + Dialog, + DialogContent, + DialogActions, + DialogTitle, + DialogContentText, + Tabs, + Tab, + TextField +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import CloseIcon from "@mui/icons-material/Close"; +import { ImageType } from "../../Types/ImageType"; +import ApiService from "../../services/ApiService"; +import { Upload } from "@mui/icons-material"; + +interface ImagesProps { + handleCopy?: (id: string) => void; +} + +const ImageGallery: React.FC = ({ handleCopy }) => { + const [images, setImages] = useState([]); + const [totalImg, setTotalImg] = useState(0); + const [imgPage, setImgPage] = useState(1); + const [imgLimit] = useState(6); + const [loading, setLoading] = useState(false); + const [selectedImage, setSelectedImage] = useState(null); + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + const [imageToDelete, setImageToDelete] = useState(null); + const [tabValue, setTabValue] = useState(0); + const [importedImage, setImportedImage] = useState(null); + const [preview, setPreview] = useState(null); + + useEffect(() => { + const fetchImages = async () => { + setLoading(true); + const data = await ApiService.getImages(imgPage, imgLimit); + setImages(data.images); + setTotalImg(data.total); + setLoading(false); + }; + fetchImages(); + }, [imgPage]); + + const handleDelete = async () => { + if (imageToDelete) { + setLoading(true); + const isDeleted = await ApiService.deleteImage(imageToDelete.id); + setLoading(false); + if (isDeleted) { + setImages(images.filter((image) => image.id !== imageToDelete.id)); + setSelectedImage(null); + setImageToDelete(null); + setOpenDeleteDialog(false); + } + } + }; + + const defaultHandleCopy = (id: string) => { + if (navigator.clipboard) { + navigator.clipboard.writeText(id); + } + }; + + const handleCopyFunction = handleCopy || defaultHandleCopy; + + const handleImageUpload = (event: React.ChangeEvent) => { + const file = event.target.files ? event.target.files[0] : null; + setImportedImage(file); + if (file) { + const objectUrl = URL.createObjectURL(file); + setPreview(objectUrl); + } + }; + + return ( + + setTabValue(newValue)}> + + + + {tabValue === 0 && ( + <> + {loading ? ( + + + + ) : ( + <> + + {images.map((obj) => ( + setSelectedImage(obj)}> + + {`Image + + + ))} + + + + + + + )} + + )} + {tabValue === 1 && ( + + {/* Image Preview at the top */} + {preview && ( + + Preview + + + + )} + + + + + + )} + setSelectedImage(null)} maxWidth="md"> + setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}> + + + + {selectedImage && ( + Enlarged view + )} + + + {selectedImage && ( + handleCopyFunction(selectedImage.id)} color="primary"> + + + )} + { + setImageToDelete(selectedImage); + setOpenDeleteDialog(true); + }} + color="error" + > + + + + + + {/* Delete Confirmation Dialog */} + setOpenDeleteDialog(false)}> + Confirm Deletion + + Are you sure you want to delete this image? + + + + + + + + ); +}; + +export default ImageGallery; diff --git a/client/src/pages/Admin/Stats.tsx b/client/src/pages/Admin/Stats.tsx index f1ab115..29f3470 100644 --- a/client/src/pages/Admin/Stats.tsx +++ b/client/src/pages/Admin/Stats.tsx @@ -10,7 +10,7 @@ const styles = { cardHover: 'rgba(65, 105, 225, 0.7)', }; -const Users: React.FC = () => { +const Stats: React.FC = () => { const [quizzes, setQuizzes] = useState([]); const [monthlyQuizzes, setMonthlyQuizzes] = useState(0); const [totalUsers, setTotalUsers] = useState(0); @@ -30,7 +30,7 @@ const Users: React.FC = () => { return quizDate.getMonth() === currentMonth && quizDate.getFullYear() === currentYear; }); - setMonthlyQuizzes(filteredMonthlyQuizzes.length === 0 ? 10 : 0); + setMonthlyQuizzes(filteredMonthlyQuizzes.length === 0 ? 0 : filteredMonthlyQuizzes.length); } catch (error) { console.error("Error fetching quizzes:", error); } finally { @@ -58,7 +58,7 @@ const Users: React.FC = () => { { label: "Quiz du Mois", value: monthlyQuizzes }, { label: "Quiz total", value: totalQuizzes }, { label: "Enseignants", value: totalUsers }, - { label: "Enseignants du Mois", value: totalUsers }, + { label: "Enseignants du Mois", value: 0 }, ]; const labelMap = { @@ -105,4 +105,4 @@ const Users: React.FC = () => { ); }; -export default Users; +export default Stats;