added tests

This commit is contained in:
Eddi3_As 2025-03-28 22:39:44 -04:00
parent 8daef780d3
commit 09f7af313c
5 changed files with 398 additions and 4 deletions

View file

@ -0,0 +1 @@
//a;ready being tested by ImageGallery.test.tsx

View file

@ -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(<Stats />);
});
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(<Stats />);
});
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(<Stats />);
});
expect(screen.getByRole('columnheader', { name: /enseignant/i })).toBeInTheDocument();
});
});

View file

@ -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) => (
<table>
<thead>
<tr>
<th>Enseignant</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{data.map((user: any) => (
<tr key={user.email}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<button onClick={() => onDelete(user)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
),
}));
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(<Users />);
});
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(<Users />);
});
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(<Users />);
});
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");
}
});
});

View file

@ -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<ImagesProps> = ({ handleCopy }) => {
const [images, setImages] = useState<ImageType[]>([]);
const [totalImg, setTotalImg] = useState(0);
const [imgPage, setImgPage] = useState(1);
const [imgLimit] = useState(6);
const [loading, setLoading] = useState(false);
const [selectedImage, setSelectedImage] = useState<ImageType | null>(null);
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const [imageToDelete, setImageToDelete] = useState<ImageType | null>(null);
const [tabValue, setTabValue] = useState(0);
const [importedImage, setImportedImage] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(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<HTMLInputElement>) => {
const file = event.target.files ? event.target.files[0] : null;
setImportedImage(file);
if (file) {
const objectUrl = URL.createObjectURL(file);
setPreview(objectUrl);
}
};
return (
<Box p={3}>
<Tabs value={tabValue} onChange={(_, newValue) => setTabValue(newValue)}>
<Tab label="Gallery" />
<Tab label="Import" />
</Tabs>
{tabValue === 0 && (
<>
{loading ? (
<Box display="flex" justifyContent="center" alignItems="center" height={200}>
<CircularProgress />
</Box>
) : (
<>
<Box display="grid" gridTemplateColumns="repeat(3, 1fr)" gridTemplateRows="repeat(2, 1fr)" gap={2} maxWidth="900px" margin="auto">
{images.map((obj) => (
<Card key={obj.id} sx={{ cursor: "pointer" }} onClick={() => setSelectedImage(obj)}>
<CardContent sx={{ p: 0 }}>
<img
src={`data:${obj.mime_type};base64,${obj.file_content}`}
alt={`Image ${obj.file_name}`}
style={{ width: "100%", height: 250, objectFit: "cover", borderRadius: 8 }}
/>
</CardContent>
</Card>
))}
</Box>
<Box display="flex" justifyContent="center" mt={2}>
<Button onClick={() => setImgPage((prev) => Math.max(prev - 1, 1))} disabled={imgPage === 1} color="primary">
Précédent
</Button>
<Button onClick={() => setImgPage((prev) => (prev * imgLimit < totalImg ? prev + 1 : prev))} disabled={imgPage * imgLimit >= totalImg} color="primary">
Suivant
</Button>
</Box>
</>
)}
</>
)}
{tabValue === 1 && (
<Box display="flex" flexDirection="column" alignItems="center" width="100%" mt={3}>
{/* Image Preview at the top */}
{preview && (
<Box
mt={2}
mb={2}
sx={{
width: "100%",
maxWidth: 600,
textAlign: "center",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<img
src={preview}
alt="Preview"
style={{
width: "100%",
height: "auto",
borderRadius: 8,
maxHeight: "600px",
}}
/>
</Box>
)}
<Box display="flex" flexDirection="row" alignItems="center" width="100%" maxWidth={400}>
<TextField
type="file"
onChange={handleImageUpload}
slotProps={{
htmlInput: {
accept: "image/*",
},
}}
sx={{ flexGrow: 1 }}
/>
<Button
variant="outlined"
aria-label="Téléverser"
onClick={() => { console.log("save..."); }}
sx={{ ml: 2, height: "100%" }}
>
Téléverser <Upload sx={{ ml: 1 }} />
</Button>
</Box>
</Box>
)}
<Dialog open={!!selectedImage} onClose={() => setSelectedImage(null)} maxWidth="md">
<IconButton color="primary" onClick={() => setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}>
<CloseIcon />
</IconButton>
<DialogContent>
{selectedImage && (
<img
src={`data:${selectedImage.mime_type};base64,${selectedImage.file_content}`}
alt="Enlarged view"
style={{ width: "100%", height: "auto", borderRadius: 8, maxHeight: "500px" }}
/>
)}
</DialogContent>
<DialogActions sx={{ justifyContent: "center" }}>
{selectedImage && (
<IconButton onClick={() => handleCopyFunction(selectedImage.id)} color="primary">
<ContentCopyIcon />
</IconButton>
)}
<IconButton
onClick={() => {
setImageToDelete(selectedImage);
setOpenDeleteDialog(true);
}}
color="error"
>
<DeleteIcon />
</IconButton>
</DialogActions>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={openDeleteDialog} onClose={() => setOpenDeleteDialog(false)}>
<DialogTitle>Confirm Deletion</DialogTitle>
<DialogContent>
<DialogContentText>Are you sure you want to delete this image?</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDeleteDialog(false)} color="primary">
Cancel
</Button>
<Button onClick={handleDelete} color="error">
Delete
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default ImageGallery;

View file

@ -10,7 +10,7 @@ const styles = {
cardHover: 'rgba(65, 105, 225, 0.7)', cardHover: 'rgba(65, 105, 225, 0.7)',
}; };
const Users: React.FC = () => { const Stats: React.FC = () => {
const [quizzes, setQuizzes] = useState<AdminTableType[]>([]); const [quizzes, setQuizzes] = useState<AdminTableType[]>([]);
const [monthlyQuizzes, setMonthlyQuizzes] = useState(0); const [monthlyQuizzes, setMonthlyQuizzes] = useState(0);
const [totalUsers, setTotalUsers] = useState(0); const [totalUsers, setTotalUsers] = useState(0);
@ -30,7 +30,7 @@ const Users: React.FC = () => {
return quizDate.getMonth() === currentMonth && quizDate.getFullYear() === currentYear; return quizDate.getMonth() === currentMonth && quizDate.getFullYear() === currentYear;
}); });
setMonthlyQuizzes(filteredMonthlyQuizzes.length === 0 ? 10 : 0); setMonthlyQuizzes(filteredMonthlyQuizzes.length === 0 ? 0 : filteredMonthlyQuizzes.length);
} catch (error) { } catch (error) {
console.error("Error fetching quizzes:", error); console.error("Error fetching quizzes:", error);
} finally { } finally {
@ -58,7 +58,7 @@ const Users: React.FC = () => {
{ label: "Quiz du Mois", value: monthlyQuizzes }, { label: "Quiz du Mois", value: monthlyQuizzes },
{ label: "Quiz total", value: totalQuizzes }, { label: "Quiz total", value: totalQuizzes },
{ label: "Enseignants", value: totalUsers }, { label: "Enseignants", value: totalUsers },
{ label: "Enseignants du Mois", value: totalUsers }, { label: "Enseignants du Mois", value: 0 },
]; ];
const labelMap = { const labelMap = {
@ -105,4 +105,4 @@ const Users: React.FC = () => {
); );
}; };
export default Users; export default Stats;