Compare commits

...

5 commits

Author SHA1 Message Date
Edwin S Lopez
2eea0d8022
Merge 81bedf0672 into 112062c0b2 2025-03-21 03:20:01 +00:00
Eddi3_As
81bedf0672 Fix - tests 2025-03-20 23:19:54 -04:00
Christopher (Cris) Fuhrman
112062c0b2
Merge pull request #284 from ets-cfuhrman-pfe/fuhrmanator/issue283
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 0s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 0s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 0s
Tests / lint-and-tests (client) (push) Failing after 0s
Tests / lint-and-tests (server) (push) Failing after 0s
[BUG] étudiant qui se joint à une salle après le démarrage du quiz es…
2025-03-11 02:52:58 -04:00
C. Fuhrman
29de2a7671 Correction de bogue trouvé par test! 2025-03-09 01:19:31 -05:00
C. Fuhrman
fe67f020eb [BUG] étudiant qui se joint à une salle après le démarrage du quiz est bloqué
Fixes #283
Valeurs de l'état de la page (quizStarted) n'ont pas leur valeur actuelle dans un on(). Alors, on déplace la logique du traitement du nouvel étudiant dans un useEffect et on provoque le useEffect dans le on()
2025-03-09 00:54:21 -05:00
4 changed files with 88 additions and 153 deletions

View file

@ -1,12 +1,10 @@
import React from "react";
import React, { act } from "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 { Images } from "../../../Types/Images";
import { act } from "react";
import "@testing-library/jest-dom";
// Mock ApiService
jest.mock("../../../services/ApiService");
const mockImages: Images[] = [
@ -15,136 +13,43 @@ const mockImages: Images[] = [
{ id: "3", file_name: "image3.jpg", mime_type: "image/jpeg", file_content: "mockBase64Content3" },
];
describe("ImageDialog Component", () => {
let setDialogOpenMock: jest.Mock;
let setImageLinksMock: jest.Mock;
beforeAll(() => {
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(),
},
});
});
describe("ImageGallery", () => {
beforeEach(() => {
jest.clearAllMocks();
setDialogOpenMock = jest.fn();
setImageLinksMock = jest.fn();
jest.spyOn(ApiService, "getImages").mockResolvedValue({ images: mockImages, total: 6 });
(ApiService.getUserImages as jest.Mock).mockResolvedValue({ images: mockImages, total: 3 });
(ApiService.deleteImage as jest.Mock).mockResolvedValue(true);
(ApiService.uploadImage as jest.Mock).mockResolvedValue('mockImageUrl');
render(<ImageGallery />);
});
test("renders the dialog when open", async () => {
it("should render images correctly", async () => {
await act(async () => {
render(
<ImageDialog
galleryOpen={true}
setDialogOpen={setDialogOpenMock}
setImageLinks={setImageLinksMock}
/>
);
});
expect(screen.getByText("Images disponibles")).toBeInTheDocument();
await waitFor(() => expect(ApiService.getImages).toHaveBeenCalledWith(1, 3));
expect(screen.getAllByRole("img")).toHaveLength(mockImages.length);
});
test("closes the dialog when close button is clicked", async () => {
await act(async () => {
render(
<ImageDialog
galleryOpen={true}
setDialogOpen={setDialogOpenMock}
setImageLinks={setImageLinksMock}
/>
);
});
fireEvent.click(screen.getByLabelText("close"));
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));
await screen.findByText("Gallery");
});
expect(screen.getByAltText("Image image1.jpg")).toBeInTheDocument();
expect(screen.getByAltText("Image image2.jpg")).toBeInTheDocument();
});
it("should handle copy action", async () => {
const handleCopyMock = jest.fn();
// Click the copy button
fireEvent.click(screen.getByTestId("copy-button-1"));
// Check that "Copié!" appears
expect(screen.getByText("Copié!")).toBeInTheDocument();
render(<ImageGallery handleCopy={handleCopyMock} />);
const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/));
await act(async () => {
fireEvent.click(copyButtons[0]);
});
expect(navigator.clipboard.writeText).toHaveBeenCalled();
});
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();
});
});

View file

@ -45,7 +45,7 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
const fetchImages = async () => {
setLoading(true);
const data = await ApiService.getImages(imgPage, imgLimit);
const data = await ApiService.getUserImages(imgPage, imgLimit);
setImages(data.images);
setTotalImg(data.total);
setLoading(false);
@ -156,7 +156,8 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
e.stopPropagation();
handleCopyFunction(obj.id);
}}
color="primary" >
color="primary"
data-testid={`gallery-tab-copy-${obj.id}`} >
<ContentCopyIcon sx={{ fontSize: 18 }} />
</IconButton>
@ -167,7 +168,8 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy }) => {
setImageToDelete(obj);
setOpenDeleteDialog(true);
}}
color="error" >
color="error"
data-testid={`gallery-tab-delete-${obj.id}`} >
<DeleteIcon sx={{ fontSize: 18 }} />
</IconButton>

View file

@ -39,9 +39,8 @@ const JoinRoom: React.FC = () => {
}, []);
useEffect(() => {
// init the answers array, one for each question
setAnswers(Array(questions.length).fill({} as AnswerSubmissionToBackendType));
console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`);
setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []);
}, [questions]);
@ -64,6 +63,7 @@ const JoinRoom: React.FC = () => {
console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions);
setQuizMode('teacher');
setIsWaitingForTeacher(true);
setQuestions([]); // clear out from last time (in case quiz is repeated)
setQuestions(questions);
// wait for next-question
});
@ -72,6 +72,7 @@ const JoinRoom: React.FC = () => {
setQuizMode('student');
setIsWaitingForTeacher(false);
setQuestions([]); // clear out from last time (in case quiz is repeated)
setQuestions(questions);
setQuestion(questions[0]);
});

View file

@ -36,8 +36,40 @@ const ManageRoom: React.FC = () => {
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
const [connectingError, setConnectingError] = useState<string>('');
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
const [quizStarted, setQuizStarted] = useState(false);
const [quizStarted, setQuizStarted] = useState<boolean>(false);
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(() => {
const verifyLogin = async () => {
@ -110,6 +142,17 @@ const ManageRoom: React.FC = () => {
const roomNameUpper = roomName.toUpperCase();
setFormattedRoomName(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', () => {
webSocketService.createRoom(roomNameUpper);
});
@ -124,23 +167,9 @@ const ManageRoom: React.FC = () => {
});
socket.on('user-joined', (student: StudentType) => {
console.log(`Student joined: name = ${student.name}, id = ${student.id}, quizMode = ${quizMode}, quizStarted = ${quizStarted}`);
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);
}
setNewlyConnectedUser(student);
});
socket.on('join-failure', (message) => {
setConnectingError(message);
setSocket(null);
@ -286,21 +315,19 @@ const ManageRoom: React.FC = () => {
};
const launchQuiz = () => {
setQuizStarted(true);
if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) {
// TODO: This error happens when token expires! Need to handle it properly
console.log(
`Error launching quiz. socket: ${socket}, roomName: ${formattedRoomName}, quiz: ${quiz}`
);
setQuizStarted(true);
return;
}
console.log(`Launching quiz in ${quizMode} mode...`);
switch (quizMode) {
case 'student':
setQuizStarted(true);
return launchStudentMode();
case 'teacher':
setQuizStarted(true);
return launchTeacherMode();
}
};