Plus besoin de local storage si on passe toutes les questions à TeacherModeQuiz aussi

Nécessite un nouveau message launch-teacher-mode
This commit is contained in:
C. Fuhrman 2025-03-08 01:09:41 -05:00
parent 22482592cd
commit ca1eb4737d
10 changed files with 157 additions and 72 deletions

View file

@ -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(
<MemoryRouter>
<StudentModeQuiz
questions={mockQuestions}
answers={Array(mockQuestions.length).fill({} as AnswerSubmissionToBackendType)}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket}
/>

View file

@ -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', () => {
<MemoryRouter>
<TeacherModeQuiz
questionInfos={{ question: mockQuestion }}
answers={Array(mockQuestions.length).fill({} as AnswerSubmissionToBackendType)}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket} />
</MemoryRouter>
@ -80,6 +82,7 @@ describe('TeacherModeQuiz', () => {
<MemoryRouter>
<TeacherModeQuiz
questionInfos={{ question: mockQuestion }}
answers={Array(mockQuestions.length).fill({} as AnswerSubmissionToBackendType)}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket}
/>
@ -94,6 +97,7 @@ describe('TeacherModeQuiz', () => {
<MemoryRouter>
<TeacherModeQuiz
questionInfos={{ question: mockQuestion }}
answers={Array(mockQuestions.length).fill({} as AnswerSubmissionToBackendType)}
submitAnswer={mockSubmitAnswer}
disconnectWebSocket={mockDisconnectWebSocket}
/>

View file

@ -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 });
});

View file

@ -7,22 +7,25 @@ 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<StudentModeQuizProps> = ({
questions,
answers,
submitAnswer,
disconnectWebSocket
}) => {
//Ajouter type AnswerQuestionType en remplacement de QuestionType
const [questionInfos, setQuestion] = useState<QuestionType>(questions[0]);
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
const [answer, setAnswer] = useState<string | number | boolean>('');
// const [answer, setAnswer] = useState<string | number | boolean>('');
const previousQuestion = () => {
@ -30,13 +33,10 @@ const StudentModeQuiz: React.FC<StudentModeQuizProps> = ({
};
useEffect(() => {
setAnswer(JSON.parse(localStorage.getItem(`Answer${questionInfos.question.id}`)||'null'));
if (answer !== null) {
setIsAnswerSubmitted(true);
} else {
setIsAnswerSubmitted(false);
}
}, [questionInfos.question , answer]);
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<StudentModeQuizProps> = ({
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question as Question}
showAnswer={isAnswerSubmitted}
answer={answer}
answer={answers[Number(questionInfos.question.id)-1]?.answer}
/>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', marginTop: '1rem' }}>
<div>

View file

@ -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<TeacherModeQuizProps> = ({
questionInfos,
answers,
submitAnswer,
disconnectWebSocket
}) => {
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false);
const [answer, setAnswer] = useState<string | number | boolean>();
const [answer, setAnswer] = useState<AnswerType>();
// 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<TeacherModeQuizProps> = ({
return (
<div className='room'>
<div className='roomHeader'>
<div className='roomHeader'>
<DisconnectButton
onReturn={disconnectWebSocket}
message={`Êtes-vous sûr de vouloir quitter?`} />
<div className='centerTitle'>
<div className='title'>Question {questionInfos.question.id}</div>
</div>
<div className='dumb'></div>
<DisconnectButton
onReturn={disconnectWebSocket}
message={`Êtes-vous sûr de vouloir quitter?`} />
<div className='centerTitle'>
<div className='title'>Question {questionInfos.question.id}</div>
</div>
{isAnswerSubmitted ? (
<div className='dumb'></div>
</div>
{isAnswerSubmitted ? (
<div>
En attente pour la prochaine question...
</div>
@ -82,20 +97,20 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
<DialogTitle>Rétroaction</DialogTitle>
<DialogContent>
<div style={{
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
maxHeight: '400px',
overflowY: 'auto',
}}>
<div style={{ textAlign: 'left', fontWeight: 'bold', marginTop: '10px'}}
>Question : </div>
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
maxHeight: '400px',
overflowY: 'auto',
}}>
<div style={{ textAlign: 'left', fontWeight: 'bold', marginTop: '10px' }}
>Question : </div>
</div>
<QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question as Question}
showAnswer={true}
answer={answer}
<QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question as Question}
showAnswer={true}
answer={answer}
/>
</DialogContent>
@ -105,7 +120,7 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
</Button>
</DialogActions>
</Dialog>
</div>
</div>
);
};

View file

@ -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<QuestionType>();
const [quizMode, setQuizMode] = useState<string>();
const [questions, setQuestions] = useState<QuestionType[]>([]);
const [answers, setAnswers] = useState<AnswerSubmissionToBackendType[]>([]);
const [connectionError, setConnectionError] = useState<string>('');
const [isConnecting, setIsConnecting] = useState<boolean>(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 (
<StudentModeQuiz
questions={questions}
answers={answers}
submitAnswer={handleOnSubmitAnswer}
disconnectWebSocket={disconnect}
/>
@ -163,6 +188,7 @@ const JoinRoom: React.FC = () => {
question && (
<TeacherModeQuiz
questionInfos={question}
answers={answers}
submitAnswer={handleOnSubmitAnswer}
disconnectWebSocket={disconnect}
/>

View file

@ -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});
}
}
};

View file

@ -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 });
}
}

View file

@ -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({

View file

@ -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);
});