mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Compare commits
5 commits
c5161d00c4
...
2eea0d8022
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eea0d8022 | ||
|
|
81bedf0672 | ||
|
|
112062c0b2 | ||
|
|
29de2a7671 | ||
|
|
fe67f020eb |
4 changed files with 88 additions and 153 deletions
|
|
@ -1,12 +1,10 @@
|
||||||
import React from "react";
|
import React, { act } from "react";
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
import ImageDialog from "../../../components/ImageGallery/ImageGallery";
|
import ImageGallery from "../../../components/ImageGallery/ImageGallery";
|
||||||
import ApiService from "../../../services/ApiService";
|
import ApiService from "../../../services/ApiService";
|
||||||
import { Images } from "../../../Types/Images";
|
import { Images } from "../../../Types/Images";
|
||||||
import { act } from "react";
|
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
|
|
||||||
// Mock ApiService
|
|
||||||
jest.mock("../../../services/ApiService");
|
jest.mock("../../../services/ApiService");
|
||||||
|
|
||||||
const mockImages: Images[] = [
|
const mockImages: Images[] = [
|
||||||
|
|
@ -15,136 +13,43 @@ const mockImages: Images[] = [
|
||||||
{ id: "3", file_name: "image3.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content3" },
|
{ id: "3", file_name: "image3.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content3" },
|
||||||
];
|
];
|
||||||
|
|
||||||
describe("ImageDialog Component", () => {
|
beforeAll(() => {
|
||||||
let setDialogOpenMock: jest.Mock;
|
Object.assign(navigator, {
|
||||||
let setImageLinksMock: jest.Mock;
|
clipboard: {
|
||||||
|
writeText: jest.fn(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ImageGallery", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
(ApiService.getUserImages as jest.Mock).mockResolvedValue({ images: mockImages, total: 3 });
|
||||||
setDialogOpenMock = jest.fn();
|
(ApiService.deleteImage as jest.Mock).mockResolvedValue(true);
|
||||||
setImageLinksMock = jest.fn();
|
(ApiService.uploadImage as jest.Mock).mockResolvedValue('mockImageUrl');
|
||||||
jest.spyOn(ApiService, "getImages").mockResolvedValue({ images: mockImages, total: 6 });
|
|
||||||
|
render(<ImageGallery />);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders the dialog when open", async () => {
|
it("should render images correctly", async () => {
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(
|
await screen.findByText("Gallery");
|
||||||
<ImageDialog
|
|
||||||
galleryOpen={true}
|
|
||||||
setDialogOpen={setDialogOpenMock}
|
|
||||||
setImageLinks={setImageLinksMock}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("Images disponibles")).toBeInTheDocument();
|
expect(screen.getByAltText("Image image1.jpg")).toBeInTheDocument();
|
||||||
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3));
|
expect(screen.getByAltText("Image image2.jpg")).toBeInTheDocument();
|
||||||
expect(screen.getAllByRole("img")).toHaveLength(mockImages.length);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("closes the dialog when close button is clicked", async () => {
|
it("should handle copy action", async () => {
|
||||||
|
const handleCopyMock = jest.fn();
|
||||||
|
|
||||||
|
render(<ImageGallery handleCopy={handleCopyMock} />);
|
||||||
|
|
||||||
|
const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/));
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(
|
fireEvent.click(copyButtons[0]);
|
||||||
<ImageDialog
|
|
||||||
galleryOpen={true}
|
|
||||||
setDialogOpen={setDialogOpenMock}
|
|
||||||
setImageLinks={setImageLinksMock}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByLabelText("close"));
|
expect(navigator.clipboard.writeText).toHaveBeenCalled();
|
||||||
expect(setDialogOpenMock).toHaveBeenCalledWith(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("copies the image link when copy button is clicked", async () => {
|
|
||||||
|
|
||||||
//const setImageLinksMock = jest.fn();
|
|
||||||
await act(async () => {
|
|
||||||
render(
|
|
||||||
<ImageDialog
|
|
||||||
galleryOpen={true}
|
|
||||||
setDialogOpen={setDialogOpenMock}
|
|
||||||
setImageLinks={setImageLinksMock}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await waitFor(() => expect(screen.getAllByRole("img")).toHaveLength(mockImages.length));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click the copy button
|
|
||||||
fireEvent.click(screen.getByTestId("copy-button-1"));
|
|
||||||
// Check that "Copié!" appears
|
|
||||||
expect(screen.getByText("Copié!")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("navigates to next and previous page", async () => {
|
|
||||||
await act(async () => {
|
|
||||||
render(
|
|
||||||
<ImageDialog
|
|
||||||
galleryOpen={true}
|
|
||||||
setDialogOpen={setDialogOpenMock}
|
|
||||||
setImageLinks={setImageLinksMock}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3));
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Suivant"));
|
|
||||||
|
|
||||||
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(2, 3));
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Précédent"));
|
|
||||||
|
|
||||||
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("deletes an image successfully", async () => {
|
|
||||||
jest.spyOn(ApiService, "deleteImage").mockResolvedValue(true);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
render(
|
|
||||||
<ImageDialog
|
|
||||||
galleryOpen={true}
|
|
||||||
setDialogOpen={setDialogOpenMock}
|
|
||||||
setImageLinks={setImageLinksMock}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled());
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("delete-button-1"));
|
|
||||||
|
|
||||||
await waitFor(() => expect(ApiService.deleteImage).toHaveBeenCalledWith("1"));
|
|
||||||
|
|
||||||
expect(screen.queryByTestId("delete-button-1")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("handles failed delete when image is linked", async () => {
|
|
||||||
jest.spyOn(ApiService, "deleteImage").mockResolvedValue(false);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
render(
|
|
||||||
<ImageDialog
|
|
||||||
galleryOpen={true}
|
|
||||||
setDialogOpen={setDialogOpenMock}
|
|
||||||
setImageLinks={setImageLinksMock}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalled());
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("delete-button-1"));
|
|
||||||
|
|
||||||
await waitFor(() => expect(ApiService.deleteImage).toHaveBeenCalledWith("1"));
|
|
||||||
|
|
||||||
expect(screen.getByText("Confirmer la suppression")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
@ -45,7 +45,7 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
|
||||||
|
|
||||||
const fetchImages = async () => {
|
const fetchImages = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await ApiService.getImages(imgPage, imgLimit);
|
const data = await ApiService.getUserImages(imgPage, imgLimit);
|
||||||
setImages(data.images);
|
setImages(data.images);
|
||||||
setTotalImg(data.total);
|
setTotalImg(data.total);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -156,7 +156,8 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleCopyFunction(obj.id);
|
handleCopyFunction(obj.id);
|
||||||
}}
|
}}
|
||||||
color="primary" >
|
color="primary"
|
||||||
|
data-testid={`gallery-tab-copy-${obj.id}`} >
|
||||||
|
|
||||||
<ContentCopyIcon sx={{ fontSize: 18 }} />
|
<ContentCopyIcon sx={{ fontSize: 18 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
@ -167,7 +168,8 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
|
||||||
setImageToDelete(obj);
|
setImageToDelete(obj);
|
||||||
setOpenDeleteDialog(true);
|
setOpenDeleteDialog(true);
|
||||||
}}
|
}}
|
||||||
color="error" >
|
color="error"
|
||||||
|
data-testid={`gallery-tab-delete-${obj.id}`} >
|
||||||
|
|
||||||
<DeleteIcon sx={{ fontSize: 18 }} />
|
<DeleteIcon sx={{ fontSize: 18 }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,8 @@ const JoinRoom: React.FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// init the answers array, one for each question
|
|
||||||
setAnswers(Array(questions.length).fill({} as AnswerSubmissionToBackendType));
|
|
||||||
console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`);
|
console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`);
|
||||||
|
setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []);
|
||||||
}, [questions]);
|
}, [questions]);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -64,6 +63,7 @@ const JoinRoom: React.FC = () => {
|
||||||
console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions);
|
console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions);
|
||||||
setQuizMode('teacher');
|
setQuizMode('teacher');
|
||||||
setIsWaitingForTeacher(true);
|
setIsWaitingForTeacher(true);
|
||||||
|
setQuestions([]); // clear out from last time (in case quiz is repeated)
|
||||||
setQuestions(questions);
|
setQuestions(questions);
|
||||||
// wait for next-question
|
// wait for next-question
|
||||||
});
|
});
|
||||||
|
|
@ -72,6 +72,7 @@ const JoinRoom: React.FC = () => {
|
||||||
|
|
||||||
setQuizMode('student');
|
setQuizMode('student');
|
||||||
setIsWaitingForTeacher(false);
|
setIsWaitingForTeacher(false);
|
||||||
|
setQuestions([]); // clear out from last time (in case quiz is repeated)
|
||||||
setQuestions(questions);
|
setQuestions(questions);
|
||||||
setQuestion(questions[0]);
|
setQuestion(questions[0]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,40 @@ const ManageRoom: React.FC = () => {
|
||||||
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
|
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
|
||||||
const [connectingError, setConnectingError] = useState<string>('');
|
const [connectingError, setConnectingError] = useState<string>('');
|
||||||
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
|
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
|
||||||
const [quizStarted, setQuizStarted] = useState(false);
|
const [quizStarted, setQuizStarted] = useState<boolean>(false);
|
||||||
const [formattedRoomName, setFormattedRoomName] = useState("");
|
const [formattedRoomName, setFormattedRoomName] = useState("");
|
||||||
|
const [newlyConnectedUser, setNewlyConnectedUser] = useState<StudentType | null>(null);
|
||||||
|
|
||||||
|
// Handle the newly connected user in useEffect, because it needs state info
|
||||||
|
// not available in the socket.on() callback
|
||||||
|
useEffect(() => {
|
||||||
|
if (newlyConnectedUser) {
|
||||||
|
console.log(`Handling newly connected user: ${newlyConnectedUser.name}`);
|
||||||
|
setStudents((prevStudents) => [...prevStudents, newlyConnectedUser]);
|
||||||
|
|
||||||
|
// only send nextQuestion if the quiz has started
|
||||||
|
if (!quizStarted) {
|
||||||
|
console.log(`!quizStarted: returning.... `);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quizMode === 'teacher') {
|
||||||
|
webSocketService.nextQuestion({
|
||||||
|
roomName: formattedRoomName,
|
||||||
|
questions: quizQuestions,
|
||||||
|
questionIndex: Number(currentQuestion?.question.id) - 1,
|
||||||
|
isLaunch: true // started late
|
||||||
|
});
|
||||||
|
} else if (quizMode === 'student') {
|
||||||
|
webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
|
||||||
|
} else {
|
||||||
|
console.error('Invalid quiz mode:', quizMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the newly connected user state
|
||||||
|
setNewlyConnectedUser(null);
|
||||||
|
}
|
||||||
|
}, [newlyConnectedUser]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const verifyLogin = async () => {
|
const verifyLogin = async () => {
|
||||||
|
|
@ -110,6 +142,17 @@ const ManageRoom: React.FC = () => {
|
||||||
const roomNameUpper = roomName.toUpperCase();
|
const roomNameUpper = roomName.toUpperCase();
|
||||||
setFormattedRoomName(roomNameUpper);
|
setFormattedRoomName(roomNameUpper);
|
||||||
console.log(`Creating WebSocket room named ${roomNameUpper}`);
|
console.log(`Creating WebSocket room named ${roomNameUpper}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ATTENTION: Lire les variables d'état dans
|
||||||
|
* les .on() n'est pas une bonne pratique.
|
||||||
|
* Les valeurs sont celles au moment de la création
|
||||||
|
* de la fonction et non au moment de l'exécution.
|
||||||
|
* Il faut utiliser des refs pour les valeurs qui
|
||||||
|
* changent fréquemment. Sinon, utiliser un trigger
|
||||||
|
* de useEffect pour mettre déclencher un traitement
|
||||||
|
* (voir user-joined plus bas).
|
||||||
|
*/
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
webSocketService.createRoom(roomNameUpper);
|
webSocketService.createRoom(roomNameUpper);
|
||||||
});
|
});
|
||||||
|
|
@ -124,23 +167,9 @@ const ManageRoom: React.FC = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('user-joined', (student: StudentType) => {
|
socket.on('user-joined', (student: StudentType) => {
|
||||||
console.log(`Student joined: name = ${student.name}, id = ${student.id}, quizMode = ${quizMode}, quizStarted = ${quizStarted}`);
|
setNewlyConnectedUser(student);
|
||||||
|
|
||||||
setStudents((prevStudents) => [...prevStudents, student]);
|
|
||||||
|
|
||||||
// only send nextQuestion if the quiz has started
|
|
||||||
if (!quizStarted) return;
|
|
||||||
|
|
||||||
if (quizMode === 'teacher') {
|
|
||||||
webSocketService.nextQuestion(
|
|
||||||
{roomName: formattedRoomName,
|
|
||||||
questions: quizQuestions,
|
|
||||||
questionIndex: Number(currentQuestion?.question.id) - 1,
|
|
||||||
isLaunch: false});
|
|
||||||
} else if (quizMode === 'student') {
|
|
||||||
webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('join-failure', (message) => {
|
socket.on('join-failure', (message) => {
|
||||||
setConnectingError(message);
|
setConnectingError(message);
|
||||||
setSocket(null);
|
setSocket(null);
|
||||||
|
|
@ -286,21 +315,19 @@ const ManageRoom: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const launchQuiz = () => {
|
const launchQuiz = () => {
|
||||||
|
setQuizStarted(true);
|
||||||
if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) {
|
if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) {
|
||||||
// TODO: This error happens when token expires! Need to handle it properly
|
// TODO: This error happens when token expires! Need to handle it properly
|
||||||
console.log(
|
console.log(
|
||||||
`Error launching quiz. socket: ${socket}, roomName: ${formattedRoomName}, quiz: ${quiz}`
|
`Error launching quiz. socket: ${socket}, roomName: ${formattedRoomName}, quiz: ${quiz}`
|
||||||
);
|
);
|
||||||
setQuizStarted(true);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log(`Launching quiz in ${quizMode} mode...`);
|
||||||
switch (quizMode) {
|
switch (quizMode) {
|
||||||
case 'student':
|
case 'student':
|
||||||
setQuizStarted(true);
|
|
||||||
return launchStudentMode();
|
return launchStudentMode();
|
||||||
case 'teacher':
|
case 'teacher':
|
||||||
setQuizStarted(true);
|
|
||||||
return launchTeacherMode();
|
return launchTeacherMode();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue