This commit is contained in:
Juba.M 2025-03-21 14:44:06 -04:00 committed by GitHub
commit 498d7d2c1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 220 additions and 60 deletions

View file

@ -152,7 +152,7 @@ describe('LiveResults', () => {
expect(classAverageElement).toBeInTheDocument(); expect(classAverageElement).toBeInTheDocument();
}); });
test('displays the correct answers per question', () => { test('displays the correct answers per question in %', () => {
render( render(
<LiveResults <LiveResults
socket={mockSocket} socket={mockSocket}
@ -175,4 +175,67 @@ describe('LiveResults', () => {
}); });
}); });
test('displays the submitted answer(s) in a question cell', () => {
render(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
// Show answers should be enabled
const showAnswersSwitch = screen.getByLabelText('Afficher les réponses');
// Toggle the display of answers is it's not already enabled
if (!(showAnswersSwitch as HTMLInputElement).checked) {
fireEvent.click(showAnswersSwitch);
}
mockStudents.forEach((student) => {
student.answers.forEach((answer) => {
const chosenAnswerElements = screen.getAllByText(answer.answer.join(', '));
const chosenAnswerElement = chosenAnswerElements.find((element) => {
return element.closest('td')?.classList.contains('MuiTableCell-root');
});
expect(chosenAnswerElement).toBeInTheDocument();
});
});
});
test('highlights the cell of the selected question', () => {
render(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
// Select the first question
const questionCell = screen.getByText(`Q${1}`);
fireEvent.click(questionCell);
// Check if the selected question is highlighted
expect(questionCell.closest('th')?.classList.contains('selected-question')).toBe(true);
});
test('Show answers should be enabled by default', () => {
render(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
const showAnswersSwitch = screen.getByLabelText('Afficher les réponses');
expect((showAnswersSwitch as HTMLInputElement).checked).toBe(true);
});
}); });

View file

@ -75,36 +75,36 @@ describe('ManageRoom', () => {
</MemoryRouter> </MemoryRouter>
); );
}); });
await act(async () => { await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1]; const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('Test Room'); createSuccessCallback('Test Room');
}); });
await waitFor(() => { await waitFor(() => {
expect(ApiService.getQuiz).toHaveBeenCalledWith('test-quiz-id'); expect(ApiService.getQuiz).toHaveBeenCalledWith('test-quiz-id');
}); });
const launchButton = screen.getByText('Lancer'); const launchButton = screen.getByText('Lancer');
fireEvent.click(launchButton); fireEvent.click(launchButton);
const rythmeButton = screen.getByText('Rythme du professeur'); const rythmeButton = screen.getByText('Rythme du professeur');
fireEvent.click(rythmeButton); fireEvent.click(rythmeButton);
const secondLaunchButton = screen.getAllByText('Lancer'); const secondLaunchButton = screen.getAllByText('Lancer');
fireEvent.click(secondLaunchButton[1]); fireEvent.click(secondLaunchButton[1]);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Test Quiz')).toBeInTheDocument(); expect(screen.getByText('Test Quiz')).toBeInTheDocument();
const roomHeader = document.querySelector('h1'); const roomHeader = document.querySelector('h1');
expect(roomHeader).toHaveTextContent('Salle : TEST ROOM'); expect(roomHeader).toHaveTextContent('Salle : TEST ROOM');
expect(screen.getByText('0/60')).toBeInTheDocument(); expect(screen.getByText('0/60')).toBeInTheDocument();
expect(screen.getByText('Question 1/2')).toBeInTheDocument(); expect(screen.getByText('Question 1/2')).toBeInTheDocument();
}); });
}); });
test('handles create-success event', async () => { test('handles create-success event', async () => {
await act(async () => { await act(async () => {
render( render(
@ -171,30 +171,30 @@ describe('ManageRoom', () => {
</MemoryRouter> </MemoryRouter>
); );
}); });
await act(async () => { await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1]; const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('Test Room'); createSuccessCallback('Test Room');
}); });
fireEvent.click(screen.getByText('Lancer')); fireEvent.click(screen.getByText('Lancer'));
fireEvent.click(screen.getByText('Rythme du professeur')); fireEvent.click(screen.getByText('Rythme du professeur'));
fireEvent.click(screen.getAllByText('Lancer')[1]); fireEvent.click(screen.getAllByText('Lancer')[1]);
await waitFor(() => { await waitFor(() => {
screen.debug(); screen.debug();
}); });
const nextQuestionButton = await screen.findByRole('button', { name: /Prochaine question/i }); const nextQuestionButton = await screen.findByRole('button', { name: /Prochaine question/i });
expect(nextQuestionButton).toBeInTheDocument(); expect(nextQuestionButton).toBeInTheDocument();
fireEvent.click(nextQuestionButton); fireEvent.click(nextQuestionButton);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Question 2/2')).toBeInTheDocument(); expect(screen.getByText('Question 2/2')).toBeInTheDocument();
}); });
}); });
test('handles disconnect', async () => { test('handles disconnect', async () => {
await act(async () => { await act(async () => {
render( render(
@ -230,38 +230,38 @@ describe('ManageRoom', () => {
</MemoryRouter> </MemoryRouter>
); );
}); });
await act(async () => { await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1]; const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('Test Room'); createSuccessCallback('Test Room');
}); });
const launchButton = screen.getByText('Lancer'); const launchButton = screen.getByText('Lancer');
fireEvent.click(launchButton); fireEvent.click(launchButton);
const rythmeButton = screen.getByText('Rythme du professeur'); const rythmeButton = screen.getByText('Rythme du professeur');
fireEvent.click(rythmeButton); fireEvent.click(rythmeButton);
const secondLaunchButton = screen.getAllByText('Lancer'); const secondLaunchButton = screen.getAllByText('Lancer');
fireEvent.click(secondLaunchButton[1]); fireEvent.click(secondLaunchButton[1]);
await act(async () => { await act(async () => {
const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1]; const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
userJoinedCallback(mockStudents[0]); userJoinedCallback(mockStudents[0]);
}); });
await act(async () => { await act(async () => {
const submitAnswerCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'submit-answer-room')[1]; const submitAnswerCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'submit-answer-room')[1];
submitAnswerCallback(mockAnswerData); submitAnswerCallback(mockAnswerData);
}); });
await waitFor(() => { await waitFor(() => {
// console.info(consoleSpy.mock.calls); // console.info(consoleSpy.mock.calls);
expect(consoleSpy).toHaveBeenCalledWith( expect(consoleSpy).toHaveBeenCalledWith(
'Received answer from Student 1 for question 1: Answer1' 'Received answer from Student 1 for question 1: Answer1'
); );
}); });
consoleSpy.mockRestore(); consoleSpy.mockRestore();
}); });
@ -294,5 +294,66 @@ describe('ManageRoom', () => {
expect(screen.queryByText('Student 1')).not.toBeInTheDocument(); expect(screen.queryByText('Student 1')).not.toBeInTheDocument();
}); });
}); });
test('initializes isQuestionShown based on quizMode', async() => {
render(<MemoryRouter>
<ManageRoom />
</MemoryRouter>);
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('Test Room');
});
await waitFor(() => {
expect(ApiService.getQuiz).toHaveBeenCalledWith('test-quiz-id');
});
const launchButton = screen.getByText('Lancer');
fireEvent.click(launchButton);
const rythmeButton = screen.getByText(`Rythme de l'étudiant`);
fireEvent.click(rythmeButton);
const secondLaunchButton = screen.getAllByText('Lancer');
fireEvent.click(secondLaunchButton[1]);
expect(screen.queryByText('Question')).not.toBeInTheDocument();
});
test('renders the close button when quizMode is student and isQuestionShown is true', async() => {
render(<MemoryRouter>
<ManageRoom />
</MemoryRouter>);
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('Test Room');
});
await waitFor(() => {
expect(ApiService.getQuiz).toHaveBeenCalledWith('test-quiz-id');
});
const launchButton = screen.getByText('Lancer');
fireEvent.click(launchButton);
const rythmeButton = screen.getByText(`Rythme de l'étudiant`);
fireEvent.click(rythmeButton);
const secondLaunchButton = screen.getAllByText('Lancer');
fireEvent.click(secondLaunchButton[1]);
const tableHeader = screen.getByText('Q1');
fireEvent.click(tableHeader);
const questionVisibilitySwitch = screen.getByTestId('question-visibility-switch'); // Get the specific switch
expect(screen.getByText(/Question 1\//i)).toBeInTheDocument();
fireEvent.click(questionVisibilitySwitch);
expect(screen.queryByRole('button', { name: /✖/i })).not.toBeInTheDocument();
expect(screen.queryByText(/Question 1\//i)).not.toBeInTheDocument();
});
}); });

View file

@ -42,7 +42,11 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
const answer = student.answers.find( const answer = student.answers.find(
(answer) => parseInt(answer.idQuestion.toString()) === index + 1 (answer) => parseInt(answer.idQuestion.toString()) === index + 1
); );
const answerText = answer ? answer.answer.toString() : ''; const answerText = answer
? Array.isArray(answer.answer)
? answer.answer.join(', ') // Join array elements with a space or another delimiter
: "" // never reached
: '';
const isCorrect = answer ? answer.isCorrect : false; const isCorrect = answer ? answer.isCorrect : false;
return ( return (
@ -63,6 +67,7 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
} }
> >
{showCorrectAnswers ? ( {showCorrectAnswers ? (
// strips out formatting of answer text here (it will break images, katex, etc.)
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: '', text: answerText }) }}></div> <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: '', text: answerText }) }}></div>
) : isCorrect ? ( ) : isCorrect ? (
<FontAwesomeIcon icon={faCheck} aria-label="correct" /> <FontAwesomeIcon icon={faCheck} aria-label="correct" />
@ -91,4 +96,4 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
</TableBody> </TableBody>
); );
}; };
export default LiveResultsTableFooter; export default LiveResultsTableFooter;

View file

@ -18,14 +18,14 @@ import DisconnectButton from 'src/components/DisconnectButton/DisconnectButton';
import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay'; import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
import { QuestionType } from 'src/Types/QuestionType'; import { QuestionType } from 'src/Types/QuestionType';
import { Button } from '@mui/material'; import { Button, FormControlLabel, Switch } from '@mui/material';
import { checkIfIsCorrect } from './useRooms'; import { checkIfIsCorrect } from './useRooms';
const ManageRoom: React.FC = () => { const ManageRoom: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [socket, setSocket] = useState<Socket | null>(null); const [socket, setSocket] = useState<Socket | null>(null);
const [students, setStudents] = useState<StudentType[]>([]); const [students, setStudents] = useState<StudentType[]>([]);
const { quizId = '', roomName = '' } = useParams<{ quizId: string, roomName: string }>(); const { quizId = '', roomName = '' } = useParams<{ quizId: string, roomName: string }>();
const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>(); const [quizQuestions, setQuizQuestions] = useState<QuestionType[] | undefined>();
const [quiz, setQuiz] = useState<QuizType | null>(null); const [quiz, setQuiz] = useState<QuizType | null>(null);
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher'); const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
@ -34,6 +34,7 @@ const ManageRoom: React.FC = () => {
const [quizStarted, setQuizStarted] = useState<boolean>(false); const [quizStarted, setQuizStarted] = useState<boolean>(false);
const [formattedRoomName, setFormattedRoomName] = useState(""); const [formattedRoomName, setFormattedRoomName] = useState("");
const [newlyConnectedUser, setNewlyConnectedUser] = useState<StudentType | null>(null); const [newlyConnectedUser, setNewlyConnectedUser] = useState<StudentType | null>(null);
const [isQuestionShown, setIsQuestionShown] = useState<boolean>(quizMode === 'student' ? false : true);
// Handle the newly connected user in useEffect, because it needs state info // Handle the newly connected user in useEffect, because it needs state info
// not available in the socket.on() callback // not available in the socket.on() callback
@ -41,13 +42,13 @@ const ManageRoom: React.FC = () => {
if (newlyConnectedUser) { if (newlyConnectedUser) {
console.log(`Handling newly connected user: ${newlyConnectedUser.name}`); console.log(`Handling newly connected user: ${newlyConnectedUser.name}`);
setStudents((prevStudents) => [...prevStudents, newlyConnectedUser]); setStudents((prevStudents) => [...prevStudents, newlyConnectedUser]);
// only send nextQuestion if the quiz has started // only send nextQuestion if the quiz has started
if (!quizStarted) { if (!quizStarted) {
console.log(`!quizStarted: returning.... `); console.log(`!quizStarted: returning.... `);
return; return;
} }
if (quizMode === 'teacher') { if (quizMode === 'teacher') {
webSocketService.nextQuestion({ webSocketService.nextQuestion({
roomName: formattedRoomName, roomName: formattedRoomName,
@ -60,12 +61,16 @@ const ManageRoom: React.FC = () => {
} else { } else {
console.error('Invalid quiz mode:', quizMode); console.error('Invalid quiz mode:', quizMode);
} }
// Reset the newly connected user state // Reset the newly connected user state
setNewlyConnectedUser(null); setNewlyConnectedUser(null);
} }
}, [newlyConnectedUser]); }, [newlyConnectedUser]);
useEffect(() => {
setIsQuestionShown(quizMode === 'student' ? false : true);
}, [quizMode]);
useEffect(() => { useEffect(() => {
const verifyLogin = async () => { const verifyLogin = async () => {
if (!ApiService.isLoggedIn()) { if (!ApiService.isLoggedIn()) {
@ -91,7 +96,7 @@ const ManageRoom: React.FC = () => {
return () => { return () => {
disconnectWebSocket(); disconnectWebSocket();
}; };
}, [roomName, navigate]); }, [roomName, navigate]);
useEffect(() => { useEffect(() => {
if (quizId) { if (quizId) {
@ -213,14 +218,14 @@ const ManageRoom: React.FC = () => {
console.log(`Comparing ${ans.idQuestion} to ${idQuestion}`); console.log(`Comparing ${ans.idQuestion} to ${idQuestion}`);
return ans.idQuestion === idQuestion return ans.idQuestion === idQuestion
? { ? {
...ans, ...ans,
answer, answer,
isCorrect: checkIfIsCorrect( isCorrect: checkIfIsCorrect(
answer, answer,
idQuestion, idQuestion,
quizQuestions! quizQuestions!
) )
} }
: ans; : ans;
}); });
} else { } else {
@ -253,10 +258,12 @@ const ManageRoom: React.FC = () => {
if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return; if (nextQuestionIndex === undefined || nextQuestionIndex > quizQuestions.length - 1) return;
setCurrentQuestion(quizQuestions[nextQuestionIndex]); setCurrentQuestion(quizQuestions[nextQuestionIndex]);
webSocketService.nextQuestion({roomName: formattedRoomName, webSocketService.nextQuestion({
questions: quizQuestions, roomName: formattedRoomName,
questionIndex: nextQuestionIndex, questions: quizQuestions,
isLaunch: false}); questionIndex: nextQuestionIndex,
isLaunch: false
});
}; };
const previousQuestion = () => { const previousQuestion = () => {
@ -266,7 +273,7 @@ const ManageRoom: React.FC = () => {
if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return; if (prevQuestionIndex === undefined || prevQuestionIndex < 0) return;
setCurrentQuestion(quizQuestions[prevQuestionIndex]); setCurrentQuestion(quizQuestions[prevQuestionIndex]);
webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: prevQuestionIndex, isLaunch: false}); webSocketService.nextQuestion({ roomName: formattedRoomName, questions: quizQuestions, questionIndex: prevQuestionIndex, isLaunch: false });
}; };
const initializeQuizQuestion = () => { const initializeQuizQuestion = () => {
@ -294,7 +301,7 @@ const ManageRoom: React.FC = () => {
} }
setCurrentQuestion(quizQuestions[0]); setCurrentQuestion(quizQuestions[0]);
webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex: 0, isLaunch: true}); webSocketService.nextQuestion({ roomName: formattedRoomName, questions: quizQuestions, questionIndex: 0, isLaunch: true });
}; };
const launchStudentMode = () => { const launchStudentMode = () => {
@ -306,6 +313,7 @@ const ManageRoom: React.FC = () => {
return; return;
} }
setQuizQuestions(quizQuestions); setQuizQuestions(quizQuestions);
setCurrentQuestion(quizQuestions[0]);
webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions); webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
}; };
@ -331,9 +339,11 @@ const ManageRoom: React.FC = () => {
if (quiz?.content && quizQuestions) { if (quiz?.content && quizQuestions) {
setCurrentQuestion(quizQuestions[questionIndex]); setCurrentQuestion(quizQuestions[questionIndex]);
if (quizMode === 'teacher') { if (quizMode === 'teacher') {
webSocketService.nextQuestion({roomName: formattedRoomName, questions: quizQuestions, questionIndex, isLaunch: false}); webSocketService.nextQuestion({ roomName: formattedRoomName, questions: quizQuestions, questionIndex, isLaunch: false });
} }
setIsQuestionShown(true);
} }
}; };
const handleReturn = () => { const handleReturn = () => {
@ -398,15 +408,35 @@ const ManageRoom: React.FC = () => {
{/* the following breaks the css (if 'room' classes are nested) */} {/* the following breaks the css (if 'room' classes are nested) */}
<div className=""> <div className="">
{quizQuestions ? ( {quizQuestions ? (
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<div className="title center-h-align mb-2">{quiz?.title}</div> <div className="title center-h-align mb-2">{quiz?.title}</div>
{!isNaN(Number(currentQuestion?.question.id)) && (
<strong className="number of questions"> <div className='close-button-wrapper'>
Question {Number(currentQuestion?.question.id)}/ <FormControlLabel
{quizQuestions?.length} label={<div className="text-sm">Afficher les questions</div>}
</strong> control={
)} <Switch
data-testid="question-visibility-switch" // Add a unique test ID
checked={isQuestionShown}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setIsQuestionShown(e.target.checked)
}
/>
}
/>
</div>
{!isNaN(Number(currentQuestion?.question.id))
&& isQuestionShown && (
<strong className="number of questions">
Question {Number(currentQuestion?.question.id)}/
{quizQuestions?.length}
</strong>
)
}
{quizMode === 'teacher' && ( {quizMode === 'teacher' && (
<div className="mb-1"> <div className="mb-1">
@ -421,11 +451,11 @@ const ManageRoom: React.FC = () => {
<div className="mb-2 flex-column-wrapper"> <div className="mb-2 flex-column-wrapper">
<div className="preview-and-result-container"> <div className="preview-and-result-container">
{currentQuestion && ( {currentQuestion && isQuestionShown && (
<QuestionDisplay <QuestionDisplay
showAnswer={false} showAnswer={false}
question={currentQuestion?.question as Question} question={currentQuestion?.question as Question}
/> />
)} )}

View file

@ -37,10 +37,11 @@
/* align-items: center; */ /* align-items: center; */
} }
.close-button-wrapper{
display: flex;
justify-content: flex-start;
margin-right: 1rem;
}
/* .create-room-container { /* .create-room-container {
display: flex; display: flex;