diff --git a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx index 74f5867..4a670a3 100644 --- a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx @@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router-dom'; import StudentModeQuiz from 'src/components/StudentModeQuiz/StudentModeQuiz'; import { BaseQuestion, parse } from 'gift-pegjs'; import { QuestionType } from 'src/Types/QuestionType'; +import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; const mockGiftQuestions = parse( `::Sample Question 1:: Sample Question 1 {=Option A ~Option B} @@ -23,12 +24,13 @@ const mockDisconnectWebSocket = jest.fn(); beforeEach(() => { // Clear local storage before each test - localStorage.clear(); + // localStorage.clear(); render( @@ -118,4 +120,4 @@ describe('StudentModeQuiz', () => { expect(screen.getByText('Sample Question 2')).toBeInTheDocument(); expect(screen.getByText('Répondre')).toBeInTheDocument(); }); -}); \ No newline at end of file +}); diff --git a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx index 788dbeb..6a4ec59 100644 --- a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx @@ -7,6 +7,7 @@ import { BaseQuestion, MultipleChoiceQuestion, parse } from 'gift-pegjs'; import TeacherModeQuiz from 'src/components/TeacherModeQuiz/TeacherModeQuiz'; import { MemoryRouter } from 'react-router-dom'; import { QuestionType } from 'src/Types/QuestionType'; +import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; const mockGiftQuestions = parse( `::Sample Question 1:: Sample Question 1 {=Option A ~Option B} @@ -36,6 +37,7 @@ describe('TeacherModeQuiz', () => { @@ -80,6 +82,7 @@ describe('TeacherModeQuiz', () => { @@ -94,6 +97,7 @@ describe('TeacherModeQuiz', () => { diff --git a/client/src/__tests__/services/WebsocketService.test.tsx b/client/src/__tests__/services/WebsocketService.test.tsx index d21423e..343a8ce 100644 --- a/client/src/__tests__/services/WebsocketService.test.tsx +++ b/client/src/__tests__/services/WebsocketService.test.tsx @@ -1,7 +1,9 @@ //WebsocketService.test.tsx +import { BaseQuestion, parse } from 'gift-pegjs'; import WebsocketService from '../../services/WebsocketService'; import { io, Socket } from 'socket.io-client'; import { ENV_VARIABLES } from 'src/constants'; +import { QuestionType } from 'src/Types/QuestionType'; jest.mock('socket.io-client'); @@ -45,10 +47,16 @@ describe('WebSocketService', () => { test('nextQuestion should emit next-question event with correct parameters', () => { const roomName = 'testRoom'; - const question = { id: 1, text: 'Sample Question' }; - + const mockGiftQuestions = parse('A {T}'); + const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => { + if (question.type !== "Category") + question.id = (index + 1).toString(); + const newMockQuestion = question; + return {question : newMockQuestion as BaseQuestion}; + }); mockSocket = WebsocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); - WebsocketService.nextQuestion(roomName, question); + WebsocketService.nextQuestion({roomName, questions: mockQuestions, questionIndex: 0, isLaunch: false}); + const question = mockQuestions[0]; expect(mockSocket.emit).toHaveBeenCalledWith('next-question', { roomName, question }); }); diff --git a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx index db7b3e8..1feb4a1 100644 --- a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx +++ b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx @@ -7,36 +7,36 @@ import { Button } from '@mui/material'; //import QuestionNavigation from '../QuestionNavigation/QuestionNavigation'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import { Question } from 'gift-pegjs'; +import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; interface StudentModeQuizProps { questions: QuestionType[]; + answers: AnswerSubmissionToBackendType[]; submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void; disconnectWebSocket: () => void; } const StudentModeQuiz: React.FC = ({ questions, + answers, submitAnswer, disconnectWebSocket }) => { //Ajouter type AnswerQuestionType en remplacement de QuestionType const [questionInfos, setQuestion] = useState(questions[0]); const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); - const [answer, setAnswer] = useState(''); + // const [answer, setAnswer] = useState(''); const previousQuestion = () => { setQuestion(questions[Number(questionInfos.question?.id) - 2]); }; - useEffect(() => { - setAnswer(JSON.parse(localStorage.getItem(`Answer${questionInfos.question.id}`)||'null')); - if (answer !== null) { - setIsAnswerSubmitted(true); - } else { - setIsAnswerSubmitted(false); - } - }, [questionInfos.question , answer]); + useEffect(() => { + const savedAnswer = answers[Number(questionInfos.question.id)-1]?.answer; + console.log(`StudentModeQuiz: useEffect: savedAnswer: ${savedAnswer}`); + setIsAnswerSubmitted(savedAnswer !== undefined); + }, [questionInfos.question, answers]); const nextQuestion = () => { setQuestion(questions[Number(questionInfos.question?.id)]); @@ -73,7 +73,7 @@ const StudentModeQuiz: React.FC = ({ handleOnSubmitAnswer={handleOnSubmitAnswer} question={questionInfos.question as Question} showAnswer={isAnswerSubmitted} - answer={answer} + answer={answers[Number(questionInfos.question.id)-1]?.answer} />
diff --git a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx index 54b37f4..c11e8a5 100644 --- a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx +++ b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx @@ -6,39 +6,54 @@ import { QuestionType } from '../../Types/QuestionType'; import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'; import { Question } from 'gift-pegjs'; +import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; +// import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface TeacherModeQuizProps { questionInfos: QuestionType; + answers: AnswerSubmissionToBackendType[]; submitAnswer: (_answer: string | number | boolean, _idQuestion: number) => void; disconnectWebSocket: () => void; } const TeacherModeQuiz: React.FC = ({ questionInfos, + answers, submitAnswer, disconnectWebSocket }) => { const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); - const [answer, setAnswer] = useState(); + const [answer, setAnswer] = useState(); + // arrive here the first time after waiting for next question + useEffect(() => { + console.log(`TeacherModeQuiz: useEffect: answers: ${JSON.stringify(answers)}`); + console.log(`TeacherModeQuiz: useEffect: questionInfos.question.id: ${questionInfos.question.id} answer: ${answer}`); + const oldAnswer = answers[Number(questionInfos.question.id) -1 ]?.answer; + console.log(`TeacherModeQuiz: useEffect: oldAnswer: ${oldAnswer}`); + setAnswer(oldAnswer); + setIsFeedbackDialogOpen(false); + }, [questionInfos.question, answers]); + + // handle showing the feedback dialog + useEffect(() => { + console.log(`TeacherModeQuiz: useEffect: answer: ${answer}`); + setIsAnswerSubmitted(answer !== undefined); + setIsFeedbackDialogOpen(answer !== undefined); + }, [answer]); + useEffect(() => { - // Close the feedback dialog when the question changes - handleFeedbackDialogClose(); - setIsAnswerSubmitted(false); - setAnswer(JSON.parse(localStorage.getItem(`Answer${questionInfos.question.id}`)||'null')); - if (typeof answer !== "object" && typeof answer !== "undefined") { - setIsAnswerSubmitted(true); - setIsFeedbackDialogOpen(true); - } - - }, [questionInfos.question , answer]); + console.log(`TeacherModeQuiz: useEffect: isAnswerSubmitted: ${isAnswerSubmitted}`); + setIsFeedbackDialogOpen(isAnswerSubmitted); + }, [isAnswerSubmitted]); const handleOnSubmitAnswer = (answer: string | number | boolean) => { const idQuestion = Number(questionInfos.question.id) || -1; submitAnswer(answer, idQuestion); - setAnswer(answer); + // setAnswer(answer); setIsFeedbackDialogOpen(true); }; @@ -49,21 +64,21 @@ const TeacherModeQuiz: React.FC = ({ return (
-
+
- - -
-
Question {questionInfos.question.id}
-
- -
+ +
+
Question {questionInfos.question.id}
- {isAnswerSubmitted ? ( +
+ +
+ + {isAnswerSubmitted ? (
En attente pour la prochaine question...
@@ -71,7 +86,7 @@ const TeacherModeQuiz: React.FC = ({ )} @@ -82,20 +97,20 @@ const TeacherModeQuiz: React.FC = ({ Rétroaction
-
Question :
+ wordWrap: 'break-word', + whiteSpace: 'pre-wrap', + maxHeight: '400px', + overflowY: 'auto', + }}> +
Question :
- -
@@ -105,7 +120,7 @@ const TeacherModeQuiz: React.FC = ({ -
+
); }; diff --git a/client/src/pages/Student/JoinRoom/JoinRoom.tsx b/client/src/pages/Student/JoinRoom/JoinRoom.tsx index a196ef8..dc7e80c 100644 --- a/client/src/pages/Student/JoinRoom/JoinRoom.tsx +++ b/client/src/pages/Student/JoinRoom/JoinRoom.tsx @@ -17,6 +17,8 @@ import LoginContainer from 'src/components/LoginContainer/LoginContainer' import ApiService from '../../../services/ApiService' +export type AnswerType = string | number | boolean; + const JoinRoom: React.FC = () => { const [roomName, setRoomName] = useState(''); const [username, setUsername] = useState(ApiService.getUsername()); @@ -25,6 +27,7 @@ const JoinRoom: React.FC = () => { const [question, setQuestion] = useState(); const [quizMode, setQuizMode] = useState(); const [questions, setQuestions] = useState([]); + const [answers, setAnswers] = useState([]); const [connectionError, setConnectionError] = useState(''); const [isConnecting, setIsConnecting] = useState(false); @@ -35,6 +38,13 @@ 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)}`); + }, [questions]); + + const handleCreateSocket = () => { console.log(`JoinRoom: handleCreateSocket: ${ENV_VARIABLES.VITE_BACKEND_URL}`); const socket = webSocketService.connect(ENV_VARIABLES.VITE_BACKEND_URL); @@ -45,12 +55,18 @@ const JoinRoom: React.FC = () => { console.log(`on(join-success): Successfully joined the room ${roomJoinedName}`); }); socket.on('next-question', (question: QuestionType) => { - console.log('on(next-question): Received next-question:', question); + console.log('JoinRoom: on(next-question): Received next-question:', question); setQuizMode('teacher'); setIsWaitingForTeacher(false); - setQuestion(question); }); + socket.on('launch-teacher-mode', (questions: QuestionType[]) => { + console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions); + setQuizMode('teacher'); + setIsWaitingForTeacher(true); + setQuestions(questions); + // wait for next-question + }); socket.on('launch-student-mode', (questions: QuestionType[]) => { console.log('on(launch-student-mode): Received launch-student-mode:', questions); @@ -84,7 +100,7 @@ const JoinRoom: React.FC = () => { }; const disconnect = () => { - localStorage.clear(); +// localStorage.clear(); webSocketService.disconnect(); setSocket(null); setQuestion(undefined); @@ -109,14 +125,22 @@ const JoinRoom: React.FC = () => { } }; - const handleOnSubmitAnswer = (answer: string | number | boolean, idQuestion: number) => { + const handleOnSubmitAnswer = (answer: AnswerType, idQuestion: number) => { + console.info(`JoinRoom: handleOnSubmitAnswer: answer: ${answer}, idQuestion: ${idQuestion}`); const answerData: AnswerSubmissionToBackendType = { roomName: roomName, answer: answer, username: username, idQuestion: idQuestion }; - localStorage.setItem(`Answer${idQuestion}`, JSON.stringify(answer)); + // localStorage.setItem(`Answer${idQuestion}`, JSON.stringify(answer)); + setAnswers((prevAnswers) => { + console.log(`JoinRoom: handleOnSubmitAnswer: prevAnswers: ${JSON.stringify(prevAnswers)}`); + const newAnswers = [...prevAnswers]; // Create a copy of the previous answers array + newAnswers[idQuestion - 1] = answerData; // Update the specific answer + return newAnswers; // Return the new array + }); + console.log(`JoinRoom: handleOnSubmitAnswer: answers: ${JSON.stringify(answers)}`); webSocketService.submitAnswer(answerData); }; @@ -154,6 +178,7 @@ const JoinRoom: React.FC = () => { return ( @@ -163,6 +188,7 @@ const JoinRoom: React.FC = () => { question && ( diff --git a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx index 05cc340..411567c 100644 --- a/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx +++ b/client/src/pages/Teacher/ManageRoom/ManageRoom.tsx @@ -131,7 +131,11 @@ const ManageRoom: React.FC = () => { if (!quizStarted) return; if (quizMode === 'teacher') { - webSocketService.nextQuestion(formattedRoomName, currentQuestion); + webSocketService.nextQuestion( + {roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: Number(currentQuestion?.question.id) - 1, + isLaunch: false}); } else if (quizMode === 'student') { webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions); } @@ -224,7 +228,10 @@ const ManageRoom: React.FC = () => { if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return; setCurrentQuestion(quizQuestions[nextQuestionIndex]); - webSocketService.nextQuestion(formattedRoomName, quizQuestions[nextQuestionIndex]); + webSocketService.nextQuestion({roomName: formattedRoomName, + questions: quizQuestions, + questionIndex: nextQuestionIndex, + isLaunch: false}); }; const previousQuestion = () => { @@ -234,7 +241,7 @@ const ManageRoom: React.FC = () => { if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return; setCurrentQuestion(quizQuestions[prevQuestionIndex]); - webSocketService.nextQuestion(formattedRoomName, quizQuestions[prevQuestionIndex]); + webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: prevQuestionIndex, isLaunch: false}); }; const initializeQuizQuestion = () => { @@ -262,7 +269,7 @@ const ManageRoom: React.FC = () => { } setCurrentQuestion(quizQuestions[0]); - webSocketService.nextQuestion(formattedRoomName, quizQuestions[0]); + webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: 0, isLaunch: true}); }; const launchStudentMode = () => { @@ -300,9 +307,8 @@ const ManageRoom: React.FC = () => { const showSelectedQuestion = (questionIndex: number) => { if (quiz?.content && quizQuestions) { setCurrentQuestion(quizQuestions[questionIndex]); - if (quizMode === 'teacher') { - webSocketService.nextQuestion(formattedRoomName, quizQuestions[questionIndex]); + webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex, isLaunch: false}); } } }; diff --git a/client/src/services/WebsocketService.tsx b/client/src/services/WebsocketService.tsx index 3cacf36..212aa21 100644 --- a/client/src/services/WebsocketService.tsx +++ b/client/src/services/WebsocketService.tsx @@ -1,4 +1,5 @@ import { io, Socket } from 'socket.io-client'; +import { QuestionType } from 'src/Types/QuestionType'; // Must (manually) sync these types to server/socket/socket.js @@ -59,12 +60,19 @@ class WebSocketService { // } // } - nextQuestion(roomName: string, question: unknown) { - console.log('WebsocketService: nextQuestion', roomName, question); - if (!question) { + nextQuestion(args: {roomName: string, questions: QuestionType[] | undefined, questionIndex: number, isLaunch: boolean}) { + // deconstruct args + const { roomName, questions, questionIndex, isLaunch } = args; + console.log('WebsocketService: nextQuestion', roomName, questions, questionIndex, isLaunch); + if (!questions || !questions[questionIndex]) { throw new Error('WebsocketService: nextQuestion: question is null'); } + if (this.socket) { + if (isLaunch) { + this.socket.emit('launch-teacher-mode', { roomName, questions }); + } + const question = questions[questionIndex]; this.socket.emit('next-question', { roomName, question }); } } diff --git a/server/__tests__/socket.test.js b/server/__tests__/socket.test.js index 739d79d..2d84da4 100644 --- a/server/__tests__/socket.test.js +++ b/server/__tests__/socket.test.js @@ -109,17 +109,29 @@ describe("websocket server", () => { }); }); - test("should send next question", (done) => { - studentSocket.on("next-question", (question) => { - expect(question).toEqual({ question: "question2" }); + test("should launch teacher mode", (done) => { + studentSocket.on("launch-teacher-mode", (questions) => { + expect(questions).toEqual([ + { question: "question1" }, + { question: "question2" }, + ]); done(); }); - teacherSocket.emit("next-question", { + teacherSocket.emit("launch-teacher-mode", { roomName: "ROOM1", - question: { question: "question2" }, + questions: [{ question: "question1" }, { question: "question2" }], }); }); + test("should send next question", (done) => { + studentSocket.on("next-question", ( question ) => { + expect(question).toBe("question2"); + done(); + }); + teacherSocket.emit("next-question", { roomName: "ROOM1", question: 'question2'}, + ); + }); + test("should send answer", (done) => { teacherSocket.on("submit-answer-room", (answer) => { expect(answer).toEqual({ diff --git a/server/socket/socket.js b/server/socket/socket.js index 393135c..0adaf92 100644 --- a/server/socket/socket.js +++ b/server/socket/socket.js @@ -81,6 +81,10 @@ const setupWebsocket = (io) => { socket.to(roomName).emit("next-question", question); }); + socket.on("launch-teacher-mode", ({ roomName, questions }) => { + socket.to(roomName).emit("launch-teacher-mode", questions); + }); + socket.on("launch-student-mode", ({ roomName, questions }) => { socket.to(roomName).emit("launch-student-mode", questions); });