From 42e3041830351ab584f169ac5facfe3a03cb584f Mon Sep 17 00:00:00 2001 From: "C. Fuhrman" Date: Fri, 21 Mar 2025 00:25:25 -0400 Subject: [PATCH] first cut, with tests --- .../LiveResults/LiveResults.test.tsx | 6 +- .../LiveResultsTable.test.tsx | 4 +- .../LiveResultsTableBody.test.tsx | 4 +- .../LiveResultsTableFooter.test.tsx | 6 +- .../LiveResults/LiveResults.test.tsx | 161 ++++---- .../MultipleChoiceQuestionDisplay.test.tsx | 24 ++ .../pages/ManageRoom/IsCorrect.test.tsx | 353 ++++++++++++++++++ .../pages/ManageRoom/ManageRoom.test.tsx | 6 +- .../StudentModeQuiz/StudentModeQuiz.test.tsx | 39 +- .../MultipleChoiceQuestionDisplay.tsx | 71 ++-- .../NumericalQuestionDisplay.tsx | 6 +- .../ShortAnswerQuestionDisplay.tsx | 6 +- .../TrueFalseQuestionDisplay.tsx | 53 ++- .../src/pages/Student/JoinRoom/JoinRoom.tsx | 2 +- .../pages/Teacher/ManageRoom/ManageRoom.tsx | 66 +--- .../src/pages/Teacher/ManageRoom/useRooms.ts | 144 ++++++- 16 files changed, 725 insertions(+), 226 deletions(-) create mode 100644 client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx index 3431b73..b9b6b9f 100644 --- a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx @@ -19,8 +19,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => }); const mockStudents: StudentType[] = [ - { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, - { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, + { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] }, + { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] }, ]; const mockShowSelectedQuestion = jest.fn(); @@ -92,4 +92,4 @@ describe('LiveResults', () => { expect(mockShowSelectedQuestion).toHaveBeenCalled(); }); -}); \ No newline at end of file +}); diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx index d6f41d6..fe26173 100644 --- a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx @@ -20,8 +20,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => const mockStudents: StudentType[] = [ - { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, - { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, + { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] }, + { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] }, ]; const mockShowSelectedQuestion = jest.fn(); diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.test.tsx index 9d4fb5c..ce10e1b 100644 --- a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.test.tsx +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.test.tsx @@ -20,8 +20,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => }); const mockStudents: StudentType[] = [ - { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, - { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, + { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] }, + { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] }, ]; const mockGetStudentGrade = jest.fn((student: StudentType) => { diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableFooter.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableFooter.test.tsx index 99a6dc3..dd7b54a 100644 --- a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableFooter.test.tsx +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableFooter.test.tsx @@ -6,8 +6,8 @@ import LiveResultsTableFooter from 'src/components/LiveResults/LiveResultsTable/ const mockStudents: StudentType[] = [ - { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, - { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, + { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] }, + { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] }, ]; const mockGetStudentGrade = jest.fn((student: StudentType) => { @@ -52,4 +52,4 @@ describe('LiveResultsTableFooter', () => { expect(screen.getByText('50 %')).toBeInTheDocument(); }); -}); \ No newline at end of file +}); diff --git a/client/src/__tests__/components/LiveResults/LiveResults.test.tsx b/client/src/__tests__/components/LiveResults/LiveResults.test.tsx index ce6244e..26cd714 100644 --- a/client/src/__tests__/components/LiveResults/LiveResults.test.tsx +++ b/client/src/__tests__/components/LiveResults/LiveResults.test.tsx @@ -5,7 +5,7 @@ import LiveResults from 'src/components/LiveResults/LiveResults'; import { QuestionType } from 'src/Types/QuestionType'; import { StudentType } from 'src/Types/StudentType'; import { Socket } from 'socket.io-client'; -import { BaseQuestion,parse } from 'gift-pegjs'; +import { BaseQuestion, parse } from 'gift-pegjs'; const mockSocket: Socket = { on: jest.fn(), @@ -19,19 +19,21 @@ const mockGiftQuestions = parse( `::Sample Question 1:: Question stem { =Choice 1 - ~Choice 2 + =Choice 2 + ~Choice 3 }`); const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => { if (question.type !== "Category") question.id = (index + 1).toString(); const newMockQuestion = question; - return {question : newMockQuestion as BaseQuestion}; + return { question: newMockQuestion as BaseQuestion }; }); const mockStudents: StudentType[] = [ - { id: '1', name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Choice 1', isCorrect: true }] }, - { id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: 'Choice 2', isCorrect: false }] }, + { id: '1', name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Choice 1'], isCorrect: false }] }, + { id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: ['Choice 3'], isCorrect: false }] }, + { id: '3', name: 'Student 3', answers: [{ idQuestion: 1, answer: ['Choice 1', 'Choice 2'], isCorrect: true }] }, ]; describe('LiveResults', () => { @@ -52,7 +54,7 @@ describe('LiveResults', () => { // Toggle the display of usernames back fireEvent.click(toggleUsernamesSwitch); - + // Check if the component renders the students mockStudents.forEach((student) => { expect(screen.getByText(student.name)).toBeInTheDocument(); @@ -82,82 +84,83 @@ describe('LiveResults', () => { }); }); -}); -test('calculates and displays the correct student grades', () => { - render( - - ); + test('calculates and displays the correct student grades', () => { + render( + + ); - // Toggle the display of usernames - const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); + // Toggle the display of usernames + const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); - // Toggle the display of usernames back - fireEvent.click(toggleUsernamesSwitch); - - // Check if the student grades are calculated and displayed correctly - mockStudents.forEach((student) => { - const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100; - expect(screen.getByText(`${grade.toFixed()} %`)).toBeInTheDocument(); - }); -}); + // Toggle the display of usernames back + fireEvent.click(toggleUsernamesSwitch); -test('calculates and displays the class average', () => { - render( - - ); - - // Toggle the display of usernames - const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); - - // Toggle the display of usernames back - fireEvent.click(toggleUsernamesSwitch); - - // Calculate the class average - const totalGrades = mockStudents.reduce((total, student) => { - return total + (student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100); - }, 0); - const classAverage = totalGrades / mockStudents.length; - - // Check if the class average is displayed correctly - const classAverageElements = screen.getAllByText(`${classAverage.toFixed()} %`); - const classAverageElement = classAverageElements.find((element) => { - return element.closest('td')?.classList.contains('MuiTableCell-footer'); - }); - expect(classAverageElement).toBeInTheDocument(); -}); - -test('displays the correct answers per question', () => { - render( - - ); - - // Check if the correct answers per question are displayed correctly - mockQuestions.forEach((_, index) => { - const correctAnswers = mockStudents.filter(student => student.answers.some(answer => answer.idQuestion === index + 1 && answer.isCorrect)).length; - const correctAnswersPercentage = (correctAnswers / mockStudents.length) * 100; - const correctAnswersElements = screen.getAllByText(`${correctAnswersPercentage.toFixed()} %`); - const correctAnswersElement = correctAnswersElements.find((element) => { - return element.closest('td')?.classList.contains('MuiTableCell-root'); + // Check if the student grades are calculated and displayed correctly + mockStudents.forEach((student) => { + const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100; + expect(screen.getByText(`${grade.toFixed()} %`)).toBeInTheDocument(); }); - expect(correctAnswersElement).toBeInTheDocument(); }); -}); \ No newline at end of file + + test('calculates and displays the class average', () => { + render( + + ); + + // Toggle the display of usernames + const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); + + // Toggle the display of usernames back + fireEvent.click(toggleUsernamesSwitch); + + // Calculate the class average + const totalGrades = mockStudents.reduce((total, student) => { + return total + (student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100); + }, 0); + const classAverage = totalGrades / mockStudents.length; + + // Check if the class average is displayed correctly + const classAverageElements = screen.getAllByText(`${classAverage.toFixed()} %`); + const classAverageElement = classAverageElements.find((element) => { + return element.closest('td')?.classList.contains('MuiTableCell-footer'); + }); + expect(classAverageElement).toBeInTheDocument(); + }); + + test('displays the correct answers per question', () => { + render( + + ); + + // Check if the correct answers per question are displayed correctly + mockQuestions.forEach((_, index) => { + const correctAnswers = mockStudents.filter(student => student.answers.some(answer => answer.idQuestion === index + 1 && answer.isCorrect)).length; + const correctAnswersPercentage = (correctAnswers / mockStudents.length) * 100; + const correctAnswersElements = screen.getAllByText(`${correctAnswersPercentage.toFixed()} %`); + const correctAnswersElement = correctAnswersElements.find((element) => { + return element.closest('td')?.classList.contains('MuiTableCell-root'); + }); + expect(correctAnswersElement).toBeInTheDocument(); + }); + }); + +}); diff --git a/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx index 8751e8b..adfa980 100644 --- a/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.test.tsx @@ -73,6 +73,30 @@ describe('MultipleChoiceQuestionDisplay', () => { expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith('Choice 1'); }); + test('submits multiple selected answers', () => { + const choiceButton1 = screen.getByText('Choice 1').closest('button'); + const choiceButton2 = screen.getByText('Choice 2').closest('button'); + + if (!choiceButton1 || !choiceButton2) throw new Error('Choice buttons not found'); + + // Simulate selecting multiple answers + act(() => { + fireEvent.click(choiceButton1); + }); + act(() => { + fireEvent.click(choiceButton2); + }); + + // Simulate submitting the answers + const submitButton = screen.getByText('Répondre'); + act(() => { + fireEvent.click(submitButton); + }); + + // Verify that the mockHandleOnSubmitAnswer function is called with both answers + expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(['Choice 1', 'Choice 2']); + }); + it('should show ✅ next to the correct answer and ❌ next to the wrong answers when showAnswer is true', async () => { const choiceButton = screen.getByText('Choice 1').closest('button'); if (!choiceButton) throw new Error('Choice button not found'); diff --git a/client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx b/client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx new file mode 100644 index 0000000..a150c55 --- /dev/null +++ b/client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx @@ -0,0 +1,353 @@ +import { checkIfIsCorrect } from 'src/pages/Teacher/ManageRoom/useRooms'; +import { HighLowNumericalAnswer, MultipleChoiceQuestion, MultipleNumericalAnswer, NumericalQuestion, RangeNumericalAnswer, ShortAnswerQuestion, SimpleNumericalAnswer, TrueFalseQuestion } from 'gift-pegjs'; +import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; +import { QuestionType } from 'src/Types/QuestionType'; + +describe('checkIfIsCorrect', () => { + const mockQuestions: QuestionType[] = [ + { + question: { + id: '1', + type: 'MC', + choices: [ + { isCorrect: true, formattedText: { text: 'Answer1' } }, + { isCorrect: true, formattedText: { text: 'Answer2' } }, + { isCorrect: false, formattedText: { text: 'Answer3' } }, + ], + } as MultipleChoiceQuestion, + }, + ]; + + test('returns true when all selected answers are correct', () => { + const answer: AnswerType = ['Answer1', 'Answer2']; + const result = checkIfIsCorrect(answer, 1, mockQuestions); + expect(result).toBe(true); + }); + + test('returns false when some selected answers are incorrect', () => { + const answer: AnswerType = ['Answer1', 'Answer3']; + const result = checkIfIsCorrect(answer, 1, mockQuestions); + expect(result).toBe(false); + }); + + test('returns false when no answers are selected', () => { + const answer: AnswerType = []; + const result = checkIfIsCorrect(answer, 1, mockQuestions); + expect(result).toBe(false); + }); + + test('returns false when no correct answers are provided in the question', () => { + const mockQuestionsWithNoCorrectAnswers: QuestionType[] = [ + { + question: { + id: '1', + type: 'MC', + choices: [ + { isCorrect: false, formattedText: { text: 'Answer1' } }, + { isCorrect: false, formattedText: { text: 'Answer2' } }, + ], + } as MultipleChoiceQuestion, + }, + ]; + const answer: AnswerType = ['Answer1']; + const result = checkIfIsCorrect(answer, 1, mockQuestionsWithNoCorrectAnswers); + expect(result).toBe(false); + }); + + test('returns true for a correct true/false answer', () => { + const mockQuestionsTF: QuestionType[] = [ + { + question: { + id: '2', + type: 'TF', + isTrue: true, + } as TrueFalseQuestion, + }, + ]; + const answer: AnswerType = [true]; + const result = checkIfIsCorrect(answer, 2, mockQuestionsTF); + expect(result).toBe(true); + }); + + test('returns false for an incorrect true/false answer', () => { + const mockQuestionsTF: QuestionType[] = [ + { + question: { + id: '2', + type: 'TF', + isTrue: true, + } as TrueFalseQuestion, + }, + ]; + const answer: AnswerType = [false]; + const result = checkIfIsCorrect(answer, 2, mockQuestionsTF); + expect(result).toBe(false); + }); + + test('returns false for a true/false question with no answer', () => { + const mockQuestionsTF: QuestionType[] = [ + { + question: { + id: '2', + type: 'TF', + isTrue: true, + } as TrueFalseQuestion, + }, + ]; + const answer: AnswerType = []; + const result = checkIfIsCorrect(answer, 2, mockQuestionsTF); + expect(result).toBe(false); + }); + + test('returns true for a correct true/false answer when isTrue is false', () => { + const mockQuestionsTF: QuestionType[] = [ + { + question: { + id: '3', + type: 'TF', + isTrue: false, // Correct answer is false + } as TrueFalseQuestion, + }, + ]; + const answer: AnswerType = [false]; + const result = checkIfIsCorrect(answer, 3, mockQuestionsTF); + expect(result).toBe(true); + }); + + test('returns false for an incorrect true/false answer when isTrue is false', () => { + const mockQuestionsTF: QuestionType[] = [ + { + question: { + id: '3', + type: 'TF', + isTrue: false, // Correct answer is false + } as TrueFalseQuestion, + }, + ]; + const answer: AnswerType = [true]; + const result = checkIfIsCorrect(answer, 3, mockQuestionsTF); + expect(result).toBe(false); + }); + + test('returns true for a correct short answer', () => { + const mockQuestionsShort: QuestionType[] = [ + { + question: { + id: '4', + type: 'Short', + choices: [ + { text: 'CorrectAnswer1' }, + { text: 'CorrectAnswer2' }, + ], + } as ShortAnswerQuestion, + }, + ]; + const answer: AnswerType = ['CorrectAnswer1']; + const result = checkIfIsCorrect(answer, 4, mockQuestionsShort); + expect(result).toBe(true); + }); + + test('returns false for an incorrect short answer', () => { + const mockQuestionsShort: QuestionType[] = [ + { + question: { + id: '4', + type: 'Short', + choices: [ + { text: 'CorrectAnswer1' }, + { text: 'CorrectAnswer2' }, + ], + } as ShortAnswerQuestion, + }, + ]; + const answer: AnswerType = ['WrongAnswer']; + const result = checkIfIsCorrect(answer, 4, mockQuestionsShort); + expect(result).toBe(false); + }); + + test('returns true for a correct short answer with case insensitivity', () => { + const mockQuestionsShort: QuestionType[] = [ + { + question: { + id: '4', + type: 'Short', + choices: [ + { text: 'CorrectAnswer1' }, + { text: 'CorrectAnswer2' }, + ], + } as ShortAnswerQuestion, + }, + ]; + const answer: AnswerType = ['correctanswer1']; // Lowercase version of the correct answer + const result = checkIfIsCorrect(answer, 4, mockQuestionsShort); + expect(result).toBe(true); + }); + + test('returns false for a short answer question with no answer', () => { + const mockQuestionsShort: QuestionType[] = [ + { + question: { + id: '4', + type: 'Short', + choices: [ + { text: 'CorrectAnswer1' }, + { text: 'CorrectAnswer2' }, + ], + } as ShortAnswerQuestion, + }, + ]; + const answer: AnswerType = []; + const result = checkIfIsCorrect(answer, 4, mockQuestionsShort); + expect(result).toBe(false); + }); + + + test('returns true for a correct simple numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '5', + type: 'Numerical', + choices: [ + { type: 'simple', number: 42 } as SimpleNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [42]; // User's answer + const result = checkIfIsCorrect(answer, 5, mockQuestionsNumerical); + expect(result).toBe(true); + }); + + test('returns false for an incorrect simple numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '5', + type: 'Numerical', + choices: [ + { type: 'simple', number: 42 } as SimpleNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [43]; // User's answer + const result = checkIfIsCorrect(answer, 5, mockQuestionsNumerical); + expect(result).toBe(false); + }); + + test('returns true for a correct range numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '6', + type: 'Numerical', + choices: [ + { type: 'range', number: 50, range: 5 } as RangeNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [52]; // User's answer within the range (50 ± 5) + const result = checkIfIsCorrect(answer, 6, mockQuestionsNumerical); + expect(result).toBe(true); + }); + + test('returns false for an out-of-range numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '6', + type: 'Numerical', + choices: [ + { type: 'range', number: 50, range: 5 } as RangeNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [56]; // User's answer outside the range (50 ± 5) + const result = checkIfIsCorrect(answer, 6, mockQuestionsNumerical); + expect(result).toBe(false); + }); + + test('returns true for a correct high-low numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '7', + type: 'Numerical', + choices: [ + { type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [95]; // User's answer within the range (90 to 100) + const result = checkIfIsCorrect(answer, 7, mockQuestionsNumerical); + expect(result).toBe(true); + }); + + test('returns false for an out-of-range high-low numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '7', + type: 'Numerical', + choices: [ + { type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [105]; // User's answer outside the range (90 to 100) + const result = checkIfIsCorrect(answer, 7, mockQuestionsNumerical); + expect(result).toBe(false); + }); + + test('returns true for a correct multiple numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '8', + type: 'Numerical', + choices: [ + { + isCorrect: true, + answer: { type: 'simple', number: 42 } as SimpleNumericalAnswer, + } as MultipleNumericalAnswer, + { + isCorrect: false, + answer: { type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer, + formattedFeedback: { text: 'You guessed way too high' }, + } + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [42]; // User's answer matches the correct multiple numerical answer + const result = checkIfIsCorrect(answer, 8, mockQuestionsNumerical); + expect(result).toBe(true); + }); + + test('returns false for an incorrect multiple numerical answer', () => { + const mockQuestionsNumerical: QuestionType[] = [ + { + question: { + id: '8', + type: 'Numerical', + choices: [ + { + type: 'multiple', + isCorrect: true, + answer: { type: 'simple', number: 42 } as SimpleNumericalAnswer, + } as MultipleNumericalAnswer, + ], + } as NumericalQuestion, + }, + ]; + const answer: AnswerType = [43]; // User's answer does not match the correct multiple numerical answer + const result = checkIfIsCorrect(answer, 8, mockQuestionsNumerical); + expect(result).toBe(false); + }); + +}); diff --git a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx index 142f1f3..01957df 100644 --- a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx +++ b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx @@ -44,7 +44,7 @@ const mockStudents: StudentType[] = [ ]; const mockAnswerData: AnswerReceptionFromBackendType = { - answer: 'Answer1', + answer: ['Answer1'], idQuestion: 1, idUser: '1', username: 'Student 1', @@ -233,7 +233,7 @@ describe('ManageRoom', () => { await act(async () => { const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1]; - createSuccessCallback('test-room-name'); + createSuccessCallback('Test Room'); }); const launchButton = screen.getByText('Lancer'); @@ -256,6 +256,7 @@ describe('ManageRoom', () => { }); await waitFor(() => { + // console.info(consoleSpy.mock.calls); expect(consoleSpy).toHaveBeenCalledWith( 'Received answer from Student 1 for question 1: Answer1' ); @@ -294,3 +295,4 @@ describe('ManageRoom', () => { }); }); }); + diff --git a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx index 1218694..ee507b3 100644 --- a/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/StudentModeQuiz/StudentModeQuiz.test.tsx @@ -8,7 +8,7 @@ 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} + `::Sample Question 1:: Sample Question 1 {=Option A =Option B ~Option C} ::Sample Question 2:: Sample Question 2 {T}`); @@ -23,9 +23,6 @@ const mockSubmitAnswer = jest.fn(); const mockDisconnectWebSocket = jest.fn(); beforeEach(() => { - // Clear local storage before each test - // localStorage.clear(); - render( { fireEvent.click(screen.getByText('Répondre')); }); - expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); + expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1); }); test('handles shows feedback for an already answered question', async () => { @@ -65,13 +62,13 @@ describe('StudentModeQuiz', () => { act(() => { fireEvent.click(screen.getByText('Répondre')); }); - expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); + expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1); const firstButtonA = screen.getByRole("button", {name: '✅ A Option A'}); expect(firstButtonA).toBeInTheDocument(); expect(firstButtonA.querySelector('.selected')).toBeInTheDocument(); - expect(screen.getByRole("button", {name: '❌ B Option B'})).toBeInTheDocument(); + expect(screen.getByRole("button", {name: '✅ B Option B'})).toBeInTheDocument(); expect(screen.queryByText('Répondre')).not.toBeInTheDocument(); // Navigate to the next question @@ -87,12 +84,12 @@ describe('StudentModeQuiz', () => { }); expect(await screen.findByText('Sample Question 1')).toBeInTheDocument(); - // Since answers are mocked, the it doesn't recognize the question as already answered + // Since answers are mocked, it doesn't recognize the question as already answered // TODO these tests are partially faked, need to be fixed if we can mock the answers // const buttonA = screen.getByRole("button", {name: '✅ A Option A'}); const buttonA = screen.getByRole("button", {name: 'A Option A'}); expect(buttonA).toBeInTheDocument(); - // const buttonB = screen.getByRole("button", {name: '❌ B Option B'}); + // const buttonB = screen.getByRole("button", {name: '✅ B Option B'}); const buttonB = screen.getByRole("button", {name: 'B Option B'}); expect(buttonB).toBeInTheDocument(); // // "Option A" div inside the name of button should have selected class @@ -122,4 +119,28 @@ describe('StudentModeQuiz', () => { expect(screen.getByText('Sample Question 2')).toBeInTheDocument(); expect(screen.getByText('Répondre')).toBeInTheDocument(); }); + + test('allows multiple answers to be selected for a question', async () => { + // Simulate selecting multiple answers + act(() => { + fireEvent.click(screen.getByText('Option A')); + }); + act(() => { + fireEvent.click(screen.getByText('Option B')); + }); + + // Simulate submitting the answers + act(() => { + fireEvent.click(screen.getByText('Répondre')); + }); + + // Verify that the mockSubmitAnswer function is called with both answers + expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A', 'Option B'], 1); + + // Verify that the selected answers are displayed as selected + const buttonA = screen.getByRole('button', { name: '✅ A Option A' }); + const buttonB = screen.getByRole('button', { name: '✅ B Option B' }); + expect(buttonA).toBeInTheDocument(); + expect(buttonB).toBeInTheDocument(); + }); }); diff --git a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx index df46193..829752d 100644 --- a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx @@ -15,76 +15,95 @@ interface Props { const MultipleChoiceQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; - const [answer, setAnswer] = useState(passedAnswer || ''); - + const [answer, setAnswer] = useState(passedAnswer || []); let disableButton = false; - if(handleOnSubmitAnswer === undefined){ + if (handleOnSubmitAnswer === undefined) { disableButton = true; } useEffect(() => { - if (passedAnswer !== undefined) { - setAnswer(passedAnswer); - } + if (passedAnswer !== undefined) { + setAnswer(passedAnswer); + } }, [passedAnswer]); const handleOnClickAnswer = (choice: string) => { - setAnswer(choice); + setAnswer((prevAnswer) => { + if (prevAnswer.includes(choice)) { + // Remove the choice if it's already selected + return prevAnswer.filter((selected) => selected !== choice); + } else { + // Add the choice if it's not already selected + return [...prevAnswer, choice]; + } + }); }; + const alpha = Array.from(Array(26)).map((_e, i) => i + 65); const alphabet = alpha.map((x) => String.fromCharCode(x)); - return ( + return (
- {question.choices.map((choice, i) => { - const selected = answer === choice.formattedText.text ? 'selected' : ''; + const selected = answer.includes(choice.formattedText.text) ? 'selected' : ''; return (
-
); })}
{question.formattedGlobalFeedback && showAnswer && (
-
-
+
+
)} - {!showAnswer && handleOnSubmitAnswer && ( - )}
diff --git a/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx index 525501d..be28f57 100644 --- a/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx @@ -17,7 +17,7 @@ interface Props { const NumericalQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; - const [answer, setAnswer] = useState(passedAnswer || ''); + const [answer, setAnswer] = useState(passedAnswer || []); const correctAnswers = question.choices; let correctAnswer = ''; @@ -69,7 +69,7 @@ const NumericalQuestionDisplay: React.FC = (props) => { id={question.formattedStem.text} name={question.formattedStem.text} onChange={(e: React.ChangeEvent) => { - setAnswer(e.target.valueAsNumber); + setAnswer([e.target.valueAsNumber]); }} inputProps={{ 'data-testid': 'number-input' }} /> @@ -87,7 +87,7 @@ const NumericalQuestionDisplay: React.FC = (props) => { handleOnSubmitAnswer && handleOnSubmitAnswer(answer) } - disabled={answer === "" || isNaN(answer as number)} + disabled={answer === undefined || answer === null || isNaN(answer[0] as number)} > Répondre diff --git a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx index 28876f9..4b15e4d 100644 --- a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx @@ -16,7 +16,7 @@ interface Props { const ShortAnswerQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; - const [answer, setAnswer] = useState(passedAnswer || ''); + const [answer, setAnswer] = useState(passedAnswer || []); useEffect(() => { if (passedAnswer !== undefined) { @@ -58,7 +58,7 @@ const ShortAnswerQuestionDisplay: React.FC = (props) => { id={question.formattedStem.text} name={question.formattedStem.text} onChange={(e) => { - setAnswer(e.target.value); + setAnswer([e.target.value]); }} disabled={showAnswer} aria-label="short-answer-input" @@ -72,7 +72,7 @@ const ShortAnswerQuestionDisplay: React.FC = (props) => { handleOnSubmitAnswer && handleOnSubmitAnswer(answer) } - disabled={answer === null || answer === ''} + disabled={answer === null || answer === undefined || answer.length === 0} > Répondre diff --git a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx index 8908338..3409f47 100644 --- a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx @@ -1,5 +1,5 @@ // TrueFalseQuestion.tsx -import React, { useState,useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import '../questionStyle.css'; import { Button } from '@mui/material'; import { TrueFalseQuestion } from 'gift-pegjs'; @@ -8,37 +8,37 @@ import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; interface Props { question: TrueFalseQuestion; - handleOnSubmitAnswer?: (answer: AnswerType) => void; + handleOnSubmitAnswer?: (answer: AnswerType) => void; showAnswer?: boolean; passedAnswer?: AnswerType; } const TrueFalseQuestionDisplay: React.FC = (props) => { - const { question, showAnswer, handleOnSubmitAnswer, passedAnswer} = + const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; let disableButton = false; - if(handleOnSubmitAnswer === undefined){ + if (handleOnSubmitAnswer === undefined) { disableButton = true; } useEffect(() => { - console.log("passedAnswer", answer); - if (passedAnswer === true || passedAnswer === false) { - setAnswer(passedAnswer); - } else { - setAnswer(undefined); - } - }, [passedAnswer, question.id]); + console.log("passedAnswer", passedAnswer); + if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) { + setAnswer(passedAnswer[0]); + } else { + setAnswer(undefined); + } + }, [passedAnswer, question.id]); const [answer, setAnswer] = useState(() => { - if (passedAnswer === true || passedAnswer === false) { - return passedAnswer; + if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) { + return passedAnswer[0]; } return undefined; - }); + }); const handleOnClickAnswer = (choice: boolean) => { setAnswer(choice); @@ -49,7 +49,7 @@ const TrueFalseQuestionDisplay: React.FC = (props) => { return (
-
+
{question.formattedGlobalFeedback && showAnswer && ( @@ -95,8 +95,7 @@ const TrueFalseQuestionDisplay: React.FC = (props) => {