From df617db28ba31001c0608fc907c157533f566478 Mon Sep 17 00:00:00 2001 From: JubaAzul <118773284+JubaAzul@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:47:33 -0500 Subject: [PATCH 1/3] Refactoriser la classe LiveResult Fixes #236 --- .../components/LiveResults/LiveResults.tsx | 389 +----------------- .../LiveResults/LiveResultsTable.tsx | 215 ++++++++++ 2 files changed, 224 insertions(+), 380 deletions(-) create mode 100644 client/src/components/LiveResults/LiveResultsTable.tsx diff --git a/client/src/components/LiveResults/LiveResults.tsx b/client/src/components/LiveResults/LiveResults.tsx index 6bcd7ce..a230630 100644 --- a/client/src/components/LiveResults/LiveResults.tsx +++ b/client/src/components/LiveResults/LiveResults.tsx @@ -1,26 +1,16 @@ // LiveResults.tsx -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { Socket } from 'socket.io-client'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'; import { QuestionType } from '../../Types/QuestionType'; import './liveResult.css'; import { FormControlLabel, FormGroup, - Paper, Switch, - Table, - TableBody, - TableCell, - TableContainer, - TableFooter, - TableHead, - TableRow } from '@mui/material'; import { StudentType } from '../../Types/StudentType'; -import { formatLatex } from '../GiftTemplate/templates/TextTypeTemplate'; +import LiveResultsTable from './LiveResultsTable'; interface LiveResultsProps { socket: Socket | null; @@ -30,239 +20,11 @@ interface LiveResultsProps { students: StudentType[] } -// interface Answer { -// answer: string | number | boolean; -// isCorrect: boolean; -// idQuestion: number; -// } - -// interface StudentResult { -// username: string; -// idUser: string; -// answers: Answer[]; -// } const LiveResults: React.FC = ({ questions, showSelectedQuestion, students }) => { const [showUsernames, setShowUsernames] = useState(false); const [showCorrectAnswers, setShowCorrectAnswers] = useState(false); - // const [students, setStudents] = useState(initialStudents); - // const [studentResultsMap, setStudentResultsMap] = useState>(new Map()); - const maxQuestions = questions.length; - - // useEffect(() => { - // // Initialize the map with the current students - // const newStudentResultsMap = new Map(); - - // for (const student of students) { - // newStudentResultsMap.set(student.id, { username: student.name, idUser: student.id, answers: [] }); - // } - - // setStudentResultsMap(newStudentResultsMap); - // }, []) - - // update when students change - // useEffect(() => { - // // studentResultsMap is inconsistent with students -- need to update - - // for (const student of students as StudentType[]) { - // } - - // }, [students]) - - // useEffect(() => { - // if (socket) { - // const submitAnswerHandler = ({ - // idUser, - // answer, - // idQuestion - // }: { - // idUser: string; - // username: string; - // answer: string | number | boolean; - // idQuestion: number; - // }) => { - // console.log(`Received answer from ${idUser} for question ${idQuestion}: ${answer}`); - - // // print the list of current student names - // console.log('Current students:'); - // students.forEach((student) => { - // console.log(student.name); - // }); - - // // Update the students state using the functional form of setStudents - // setStudents((prevStudents) => { - // let foundStudent = false; - // const updatedStudents = prevStudents.map((student) => { - // if (student.id === idUser) { - // foundStudent = true; - // const updatedAnswers = student.answers.map((ans) => { - // const newAnswer: Answer = { answer, isCorrect: checkIfIsCorrect(answer, idQuestion), idQuestion }; - // console.log(`Updating answer for ${student.name} for question ${idQuestion} to ${answer}`); - // return (ans.idQuestion === idQuestion ? { ...ans, newAnswer } : ans); - // } - // ); - // return { ...student, answers: updatedAnswers }; - // } - // return student; - // }); - // if (!foundStudent) { - // console.log(`Student ${idUser} not found in the list of students in LiveResults`); - // } - // return updatedStudents; - // }); - - - // // make a copy of the students array so we can update it - // // const updatedStudents = [...students]; - - // // const student = updatedStudents.find((student) => student.id === idUser); - // // if (!student) { - // // // this is a bad thing if an answer was submitted but the student isn't in the list - // // console.log(`Student ${idUser} not found in the list of students in LiveResults`); - // // return; - // // } - - // // const isCorrect = checkIfIsCorrect(answer, idQuestion); - // // const newAnswer: Answer = { answer, isCorrect, idQuestion }; - // // student.answers.push(newAnswer); - // // // print list of answers - // // console.log('Answers:'); - // // student.answers.forEach((answer) => { - // // console.log(answer.answer); - // // }); - // // setStudents(updatedStudents); // update the state - // }; - - // socket.on('submit-answer', submitAnswerHandler); - // return () => { - // socket.off('submit-answer'); - // }; - // } - // }, [socket]); - - const getStudentGrade = (student: StudentType): number => { - if (student.answers.length === 0) { - return 0; - } - - const uniqueQuestions = new Set(); - let correctAnswers = 0; - - for (const answer of student.answers) { - const { idQuestion, isCorrect } = answer; - - if (!uniqueQuestions.has(idQuestion)) { - uniqueQuestions.add(idQuestion); - - if (isCorrect) { - correctAnswers++; - } - } - } - - return (correctAnswers / questions.length) * 100; - }; - - const classAverage: number = useMemo(() => { - let classTotal = 0; - - students.forEach((student) => { - classTotal += getStudentGrade(student); - }); - - return classTotal / students.length; - }, [students]); - - const getCorrectAnswersPerQuestion = (index: number): number => { - return ( - (students.filter((student) => - student.answers.some( - (answer) => - parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect - ) - ).length / students.length) * 100 - ); - }; - - // (studentResults.filter((student) => - // student.answers.some( - // (answer) => - // parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect - // ) - // ).length / - // studentResults.length) * - // 100 - // ); - // }; - - // function checkIfIsCorrect(answer: string | number | boolean, idQuestion: number): boolean { - // const questionInfo = questions.find((q) => - // q.question.id ? q.question.id === idQuestion.toString() : false - // ) as QuestionType | undefined; - - // const answerText = answer.toString(); - // if (questionInfo) { - // const question = questionInfo.question as GIFTQuestion; - // if (question.type === 'TF') { - // return ( - // (question.isTrue && answerText == 'true') || - // (!question.isTrue && answerText == 'false') - // ); - // } else if (question.type === 'MC') { - // return question.choices.some( - // (choice) => choice.isCorrect && choice.text.text === answerText - // ); - // } else if (question.type === 'Numerical') { - // if (question.choices && !Array.isArray(question.choices)) { - // if ( - // question.choices.type === 'high-low' && - // question.choices.numberHigh && - // question.choices.numberLow - // ) { - // const answerNumber = parseFloat(answerText); - // if (!isNaN(answerNumber)) { - // return ( - // answerNumber <= question.choices.numberHigh && - // answerNumber >= question.choices.numberLow - // ); - // } - // } - // } - // if (question.choices && Array.isArray(question.choices)) { - // if ( - // question.choices[0].text.type === 'range' && - // question.choices[0].text.number && - // question.choices[0].text.range - // ) { - // const answerNumber = parseFloat(answerText); - // const range = question.choices[0].text.range; - // const correctAnswer = question.choices[0].text.number; - // if (!isNaN(answerNumber)) { - // return ( - // answerNumber <= correctAnswer + range && - // answerNumber >= correctAnswer - range - // ); - // } - // } - // if ( - // question.choices[0].text.type === 'simple' && - // question.choices[0].text.number - // ) { - // const answerNumber = parseFloat(answerText); - // if (!isNaN(answerNumber)) { - // return answerNumber === question.choices[0].text.number; - // } - // } - // } - // } else if (question.type === 'Short') { - // return question.choices.some( - // (choice) => choice.text.text.toUpperCase() === answerText.toUpperCase() - // ); - // } - // } - // return false; - // } return (
@@ -295,146 +57,13 @@ const LiveResults: React.FC = ({ questions, showSelectedQuesti
- - - - - -
Nom d'utilisateur
-
- {Array.from({ length: maxQuestions }, (_, index) => ( - showSelectedQuestion(index)} - > -
{`Q${index + 1}`}
-
- ))} - -
% réussite
-
-
-
- - {students.map((student) => ( - - -
- {showUsernames ? student.name : '******'} -
-
- {Array.from({ length: maxQuestions }, (_, index) => { - const answer = student.answers.find( - (answer) => parseInt(answer.idQuestion.toString()) === index + 1 - ); - const answerText = answer ? answer.answer.toString() : ''; - const isCorrect = answer ? answer.isCorrect : false; - - return ( - - {showCorrectAnswers ? ( -
{formatLatex(answerText)}
- ) : isCorrect ? ( - - ) : ( - answerText !== '' && ( - - ) - )} -
- ); - })} - - {getStudentGrade(student).toFixed()} % - -
- ))} -
- - - -
% réussite
-
- {Array.from({ length: maxQuestions }, (_, index) => ( - - {students.length > 0 - ? `${getCorrectAnswersPerQuestion(index).toFixed()} %` - : '-'} - - ))} - - {students.length > 0 ? `${classAverage.toFixed()} %` : '-'} - -
-
-
-
+
); diff --git a/client/src/components/LiveResults/LiveResultsTable.tsx b/client/src/components/LiveResults/LiveResultsTable.tsx new file mode 100644 index 0000000..bb81b80 --- /dev/null +++ b/client/src/components/LiveResults/LiveResultsTable.tsx @@ -0,0 +1,215 @@ +import React, { useMemo } from 'react'; +import { Paper, Table, TableBody, TableCell, TableContainer, TableFooter, TableHead, TableRow } from '@mui/material'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'; +import { StudentType } from 'src/Types/StudentType'; +import { QuestionType } from '../../Types/QuestionType'; +import { FormattedTextTemplate } from '../GiftTemplate/templates/TextTypeTemplate'; + +interface LiveResultsTableProps { + students: StudentType[]; + questions: QuestionType[]; + showCorrectAnswers: boolean; + showSelectedQuestion: (index: number) => void; + showUsernames: boolean; +} + +const LiveResultsTable: React.FC = ({ + questions, + students, + showCorrectAnswers, + showSelectedQuestion, + showUsernames +}) => { + + const maxQuestions = questions.length; + + const getStudentGrade = (student: StudentType): number => { + if (student.answers.length === 0) { + return 0; + } + + const uniqueQuestions = new Set(); + let correctAnswers = 0; + + for (const answer of student.answers) { + const { idQuestion, isCorrect } = answer; + + if (!uniqueQuestions.has(idQuestion)) { + uniqueQuestions.add(idQuestion); + + if (isCorrect) { + correctAnswers++; + } + } + } + + return (correctAnswers / questions.length) * 100; + }; + + const classAverage: number = useMemo(() => { + let classTotal = 0; + + students.forEach((student) => { + classTotal += getStudentGrade(student); + }); + + return classTotal / students.length; + }, [students]); + + const getCorrectAnswersPerQuestion = (index: number): number => { + return ( + (students.filter((student) => + student.answers.some( + (answer) => + parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect + ) + ).length / students.length) * 100 + ); + }; + + return ( + + + + + +
Nom d'utilisateur
+
+ {Array.from({ length: maxQuestions }, (_, index) => ( + showSelectedQuestion(index)} + > +
{`Q${index + 1}`}
+
+ ))} + +
% réussite
+
+
+
+ + {students.map((student) => ( + + +
+ {showUsernames ? student.name : '******'} +
+
+ {Array.from({ length: maxQuestions }, (_, index) => { + const answer = student.answers.find( + (answer) => parseInt(answer.idQuestion.toString()) === index + 1 + ); + const answerText = answer ? answer.answer.toString() : ''; + const isCorrect = answer ? answer.isCorrect : false; + + return ( + + {showCorrectAnswers ? ( +
+ ) : isCorrect ? ( + + ) : ( + answerText !== '' && ( + + ) + )} +
+ ); + })} + + {getStudentGrade(student).toFixed()} % + +
+ ))} +
+ + + +
% réussite
+
+ {Array.from({ length: maxQuestions }, (_, index) => ( + + {students.length > 0 + ? `${getCorrectAnswersPerQuestion(index).toFixed()} %` + : '-'} + + ))} + + {students.length > 0 ? `${classAverage.toFixed()} %` : '-'} + +
+
+
+
+ ); +}; + +export default LiveResultsTable; \ No newline at end of file From 3aa51900d71f56d6fc25394ea9db9beba4a688c5 Mon Sep 17 00:00:00 2001 From: JubaAzul <118773284+JubaAzul@users.noreply.github.com> Date: Fri, 7 Feb 2025 13:33:38 -0500 Subject: [PATCH 2/3] =?UTF-8?q?Cr=C3=A9ation=20de=20classes=20de=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LiveResults/LiveResults.test.tsx | 95 +++++++++++++++ .../LiveResults/LiveResultsTable.test.tsx | 110 ++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx create mode 100644 client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable.test.tsx diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx new file mode 100644 index 0000000..3431b73 --- /dev/null +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResults.test.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import LiveResults from 'src/components/LiveResults/LiveResults'; +import { QuestionType } from 'src/Types/QuestionType'; +import { StudentType } from 'src/Types/StudentType'; +import { BaseQuestion, parse } from 'gift-pegjs'; + +const mockGiftQuestions = parse( + `::Sample Question 1:: Sample Question 1 {=Answer 1 ~Answer 2} + + ::Sample Question 2:: Sample Question 2 {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}; +}); + +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 }] }, +]; + +const mockShowSelectedQuestion = jest.fn(); + +describe('LiveResults', () => { + test('renders LiveResults component', () => { + render( + + ); + + expect(screen.getByText('Résultats du quiz')).toBeInTheDocument(); + }); + + test('toggles show usernames switch', () => { + render( + + ); + + const switchElement = screen.getByLabelText('Afficher les noms'); + expect(switchElement).toBeInTheDocument(); + + fireEvent.click(switchElement); + expect(switchElement).toBeChecked(); + }); + + test('toggles show correct answers switch', () => { + render( + + ); + + const switchElement = screen.getByLabelText('Afficher les réponses'); + expect(switchElement).toBeInTheDocument(); + + fireEvent.click(switchElement); + expect(switchElement).toBeChecked(); + }); + + test('calls showSelectedQuestion when a table cell is clicked', () => { + render( + + ); + + const tableCell = screen.getByText('Q1'); + fireEvent.click(tableCell); + + expect(mockShowSelectedQuestion).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable.test.tsx new file mode 100644 index 0000000..7f4c8a0 --- /dev/null +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable.test.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { StudentType } from 'src/Types/StudentType'; +import LiveResultsTable from 'src/components/LiveResults/LiveResultsTable'; +import { QuestionType } from 'src/Types/QuestionType'; +import { BaseQuestion, parse } from 'gift-pegjs'; + +const mockGiftQuestions = parse( + `::Sample Question 1:: Sample Question 1 {=Answer 1 ~Answer 2} + + ::Sample Question 2:: Sample Question 2 {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}; +}); + + +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 }] }, +]; + +const mockShowSelectedQuestion = jest.fn(); + +describe('LiveResultsTable', () => { + test('renders LiveResultsTable component', () => { + render( + + ); + + expect(screen.getByText('Student 1')).toBeInTheDocument(); + expect(screen.getByText('Student 2')).toBeInTheDocument(); + }); + + test('displays correct and incorrect answers', () => { + render( + + ); + + expect(screen.getByText('Answer 1')).toBeInTheDocument(); + expect(screen.getByText('Answer 2')).toBeInTheDocument(); + }); + + test('calls showSelectedQuestion when a table cell is clicked', () => { + render( + + ); + + const tableCell = screen.getByText('Q1'); + fireEvent.click(tableCell); + + expect(mockShowSelectedQuestion).toHaveBeenCalled(); + }); + + test('calculates and displays student grades', () => { + render( + + ); + + //50% because only one of the two questions have been answered (getALLByText, because there are a value 50% for the %reussite de la question + // and a second one for the student grade) + const gradeElements = screen.getAllByText('50 %'); + expect(gradeElements.length).toBe(2); + + const gradeElements2 = screen.getAllByText('0 %'); + expect(gradeElements2.length).toBe(2); }); + + test('calculates and displays class average', () => { + render( + + ); + + //1 good answer out of 4 possible good answers (the second question has not been answered) + expect(screen.getByText('25 %')).toBeInTheDocument(); + }); +}); \ No newline at end of file From 9c9c17cd0f82c73145c85e3af839b507180a3529 Mon Sep 17 00:00:00 2001 From: JubaAzul <118773284+JubaAzul@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:01:36 -0500 Subject: [PATCH 3/3] Refactoriser la LiveResultTable en fonction de ses composants Fixes #236 --- .../LiveResultsTable.test.tsx | 2 +- .../LiveResultsTableBody.test.tsx | 95 ++++++++ .../LiveResultsTableFooter.test.tsx | 55 +++++ .../LiveResultsTableHeader.test.tsx | 51 +++++ .../components/LiveResults/LiveResults.tsx | 20 +- .../LiveResults/LiveResultsTable.tsx | 215 ------------------ .../LiveResultsTable/LiveResultsTable.tsx | 75 ++++++ .../TableComponents/LiveResultTableFooter.tsx | 79 +++++++ .../TableComponents/LiveResultsTableBody.tsx | 94 ++++++++ .../LiveResultsTableHeader.tsx | 50 ++++ 10 files changed, 509 insertions(+), 227 deletions(-) rename client/src/__tests__/components/GiftTemplate/LiveResults/{ => LiveResultsTable}/LiveResultsTable.test.tsx (99%) create mode 100644 client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.test.tsx create mode 100644 client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableFooter.test.tsx create mode 100644 client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader.test.tsx delete mode 100644 client/src/components/LiveResults/LiveResultsTable.tsx create mode 100644 client/src/components/LiveResults/LiveResultsTable/LiveResultsTable.tsx create mode 100644 client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter.tsx create mode 100644 client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.tsx create mode 100644 client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader.tsx diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx similarity index 99% rename from client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable.test.tsx rename to client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx index 7f4c8a0..021b82d 100644 --- a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable.test.tsx +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import { StudentType } from 'src/Types/StudentType'; -import LiveResultsTable from 'src/components/LiveResults/LiveResultsTable'; +import LiveResultsTable from 'src/components/LiveResults/LiveResultsTable/LiveResultsTable'; import { QuestionType } from 'src/Types/QuestionType'; import { BaseQuestion, parse } from 'gift-pegjs'; 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 new file mode 100644 index 0000000..11e41f1 --- /dev/null +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.test.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { StudentType } from 'src/Types/StudentType'; +import LiveResultsTableBody from 'src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody'; +import { QuestionType } from 'src/Types/QuestionType'; +import { BaseQuestion, parse } from 'gift-pegjs'; + + +const mockGiftQuestions = parse( + `::Sample Question 1:: Sample Question 1 {=Answer 1 ~Answer 2} + + ::Sample Question 2:: Sample Question 2 {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}; +}); + +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 }] }, +]; + +const mockGetStudentGrade = jest.fn((student: StudentType) => { + const correctAnswers = student.answers.filter(answer => answer.isCorrect).length; + return (correctAnswers / mockQuestions.length) * 100; +}); + +describe('LiveResultsTableBody', () => { + test('renders LiveResultsTableBody component', () => { + render( + + ); + + expect(screen.getByText('Student 1')).toBeInTheDocument(); + expect(screen.getByText('Student 2')).toBeInTheDocument(); + }); + + test('displays correct and incorrect answers', () => { + render( + + ); + + expect(screen.getByText('Answer 1')).toBeInTheDocument(); + expect(screen.getByText('Answer 2')).toBeInTheDocument(); + }); + + test('displays icons for correct and incorrect answers when showCorrectAnswers is false', () => { + render( + + ); + + expect(screen.getByLabelText('correct')).toBeInTheDocument(); + expect(screen.getByLabelText('incorrect')).toBeInTheDocument(); + }); + + test('hides usernames when showUsernames is false', () => { + render( + + ); + + expect(screen.getAllByText('******').length).toBe(2); + }); +}); \ No newline at end of file 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 new file mode 100644 index 0000000..99a6dc3 --- /dev/null +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableFooter.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { StudentType } from 'src/Types/StudentType'; +import LiveResultsTableFooter from 'src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter'; + + +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 }] }, +]; + +const mockGetStudentGrade = jest.fn((student: StudentType) => { + const correctAnswers = student.answers.filter(answer => answer.isCorrect).length; + return (correctAnswers / 2) * 100; // Assuming there are 2 questions +}); + +describe('LiveResultsTableFooter', () => { + test('renders LiveResultsTableFooter component', () => { + render( + + ); + + expect(screen.getByText('% réussite')).toBeInTheDocument(); + }); + + test('calculates and displays correct answers per question', () => { + render( + + ); + + expect(screen.getByText('50 %')).toBeInTheDocument(); + expect(screen.getByText('0 %')).toBeInTheDocument(); + }); + + test('calculates and displays class average', () => { + render( + + ); + + expect(screen.getByText('50 %')).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader.test.tsx new file mode 100644 index 0000000..5dff41a --- /dev/null +++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import LiveResultsTableHeader from 'src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader'; + + +const mockShowSelectedQuestion = jest.fn(); + +describe('LiveResultsTableHeader', () => { + test('renders LiveResultsTableHeader component', () => { + render( + + ); + + expect(screen.getByText("Nom d'utilisateur")).toBeInTheDocument(); + for (let i = 1; i <= 5; i++) { + expect(screen.getByText(`Q${i}`)).toBeInTheDocument(); + } + expect(screen.getByText('% réussite')).toBeInTheDocument(); + }); + + test('calls showSelectedQuestion when a question header is clicked', () => { + render( + + ); + + const questionHeader = screen.getByText('Q1'); + fireEvent.click(questionHeader); + + expect(mockShowSelectedQuestion).toHaveBeenCalledWith(0); + }); + + test('renders the correct number of question headers', () => { + render( + + ); + + for (let i = 1; i <= 3; i++) { + expect(screen.getByText(`Q${i}`)).toBeInTheDocument(); + } + }); +}); \ No newline at end of file diff --git a/client/src/components/LiveResults/LiveResults.tsx b/client/src/components/LiveResults/LiveResults.tsx index 13611eb..44c998f 100644 --- a/client/src/components/LiveResults/LiveResults.tsx +++ b/client/src/components/LiveResults/LiveResults.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import { Socket } from 'socket.io-client'; import { QuestionType } from '../../Types/QuestionType'; - import './liveResult.css'; import { FormControlLabel, @@ -10,7 +9,7 @@ import { Switch, } from '@mui/material'; import { StudentType } from '../../Types/StudentType'; -import LiveResultsTable from './LiveResultsTable'; +import LiveResultsTable from './LiveResultsTable/LiveResultsTable'; interface LiveResultsProps { @@ -21,12 +20,11 @@ interface LiveResultsProps { students: StudentType[] } - const LiveResults: React.FC = ({ questions, showSelectedQuestion, students }) => { const [showUsernames, setShowUsernames] = useState(false); const [showCorrectAnswers, setShowCorrectAnswers] = useState(false); - + return (
@@ -58,13 +56,13 @@ const LiveResults: React.FC = ({ questions, showSelectedQuesti
- +
); diff --git a/client/src/components/LiveResults/LiveResultsTable.tsx b/client/src/components/LiveResults/LiveResultsTable.tsx deleted file mode 100644 index bb81b80..0000000 --- a/client/src/components/LiveResults/LiveResultsTable.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import React, { useMemo } from 'react'; -import { Paper, Table, TableBody, TableCell, TableContainer, TableFooter, TableHead, TableRow } from '@mui/material'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'; -import { StudentType } from 'src/Types/StudentType'; -import { QuestionType } from '../../Types/QuestionType'; -import { FormattedTextTemplate } from '../GiftTemplate/templates/TextTypeTemplate'; - -interface LiveResultsTableProps { - students: StudentType[]; - questions: QuestionType[]; - showCorrectAnswers: boolean; - showSelectedQuestion: (index: number) => void; - showUsernames: boolean; -} - -const LiveResultsTable: React.FC = ({ - questions, - students, - showCorrectAnswers, - showSelectedQuestion, - showUsernames -}) => { - - const maxQuestions = questions.length; - - const getStudentGrade = (student: StudentType): number => { - if (student.answers.length === 0) { - return 0; - } - - const uniqueQuestions = new Set(); - let correctAnswers = 0; - - for (const answer of student.answers) { - const { idQuestion, isCorrect } = answer; - - if (!uniqueQuestions.has(idQuestion)) { - uniqueQuestions.add(idQuestion); - - if (isCorrect) { - correctAnswers++; - } - } - } - - return (correctAnswers / questions.length) * 100; - }; - - const classAverage: number = useMemo(() => { - let classTotal = 0; - - students.forEach((student) => { - classTotal += getStudentGrade(student); - }); - - return classTotal / students.length; - }, [students]); - - const getCorrectAnswersPerQuestion = (index: number): number => { - return ( - (students.filter((student) => - student.answers.some( - (answer) => - parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect - ) - ).length / students.length) * 100 - ); - }; - - return ( - - - - - -
Nom d'utilisateur
-
- {Array.from({ length: maxQuestions }, (_, index) => ( - showSelectedQuestion(index)} - > -
{`Q${index + 1}`}
-
- ))} - -
% réussite
-
-
-
- - {students.map((student) => ( - - -
- {showUsernames ? student.name : '******'} -
-
- {Array.from({ length: maxQuestions }, (_, index) => { - const answer = student.answers.find( - (answer) => parseInt(answer.idQuestion.toString()) === index + 1 - ); - const answerText = answer ? answer.answer.toString() : ''; - const isCorrect = answer ? answer.isCorrect : false; - - return ( - - {showCorrectAnswers ? ( -
- ) : isCorrect ? ( - - ) : ( - answerText !== '' && ( - - ) - )} -
- ); - })} - - {getStudentGrade(student).toFixed()} % - -
- ))} -
- - - -
% réussite
-
- {Array.from({ length: maxQuestions }, (_, index) => ( - - {students.length > 0 - ? `${getCorrectAnswersPerQuestion(index).toFixed()} %` - : '-'} - - ))} - - {students.length > 0 ? `${classAverage.toFixed()} %` : '-'} - -
-
-
-
- ); -}; - -export default LiveResultsTable; \ No newline at end of file diff --git a/client/src/components/LiveResults/LiveResultsTable/LiveResultsTable.tsx b/client/src/components/LiveResults/LiveResultsTable/LiveResultsTable.tsx new file mode 100644 index 0000000..e23c4a6 --- /dev/null +++ b/client/src/components/LiveResults/LiveResultsTable/LiveResultsTable.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Paper, Table, TableContainer } from '@mui/material'; +import { StudentType } from 'src/Types/StudentType'; +import { QuestionType } from '../../../Types/QuestionType'; +import LiveResultsTableFooter from './TableComponents/LiveResultTableFooter'; +import LiveResultsTableHeader from './TableComponents/LiveResultsTableHeader'; +import LiveResultsTableBody from './TableComponents/LiveResultsTableBody'; + +interface LiveResultsTableProps { + students: StudentType[]; + questions: QuestionType[]; + showCorrectAnswers: boolean; + showSelectedQuestion: (index: number) => void; + showUsernames: boolean; +} + +const LiveResultsTable: React.FC = ({ + questions, + students, + showSelectedQuestion, + showUsernames, + showCorrectAnswers +}) => { + + const maxQuestions = questions.length; + + const getStudentGrade = (student: StudentType): number => { + if (student.answers.length === 0) { + return 0; + } + + const uniqueQuestions = new Set(); + let correctAnswers = 0; + + for (const answer of student.answers) { + const { idQuestion, isCorrect } = answer; + + if (!uniqueQuestions.has(idQuestion)) { + uniqueQuestions.add(idQuestion); + + if (isCorrect) { + correctAnswers++; + } + } + } + + return (correctAnswers / questions.length) * 100; + }; + + + return ( + + + + + +
+
+ ); +}; + +export default LiveResultsTable; \ No newline at end of file diff --git a/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter.tsx b/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter.tsx new file mode 100644 index 0000000..a24694e --- /dev/null +++ b/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultTableFooter.tsx @@ -0,0 +1,79 @@ +import { TableCell, TableFooter, TableRow } from "@mui/material"; +import React, { useMemo } from "react"; +import { StudentType } from "src/Types/StudentType"; + +interface LiveResultsFooterProps { + students: StudentType[]; + maxQuestions: number; + getStudentGrade: (student: StudentType) => number; +} + +const LiveResultsTableFooter: React.FC = ({ + maxQuestions, + students, + getStudentGrade + +}) => { + + const getCorrectAnswersPerQuestion = (index: number): number => { + return ( + (students.filter((student) => + student.answers.some( + (answer) => + parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect + ) + ).length / students.length) * 100 + ); + }; + + const classAverage: number = useMemo(() => { + let classTotal = 0; + + students.forEach((student) => { + classTotal += getStudentGrade(student); + }); + + return classTotal / students.length; + }, [students]); + + return ( + + + +
% réussite
+
+ {Array.from({ length: maxQuestions }, (_, index) => ( + + {students.length > 0 + ? `${getCorrectAnswersPerQuestion(index).toFixed()} %` + : '-'} + + ))} + + {students.length > 0 ? `${classAverage.toFixed()} %` : '-'} + +
+
+ ); +}; +export default LiveResultsTableFooter; \ No newline at end of file diff --git a/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.tsx b/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.tsx new file mode 100644 index 0000000..a0c67f7 --- /dev/null +++ b/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableBody.tsx @@ -0,0 +1,94 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { TableBody, TableCell, TableRow } from "@mui/material"; +import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'; +import { FormattedTextTemplate } from '../../../GiftTemplate/templates/TextTypeTemplate'; +import React from "react"; +import { StudentType } from "src/Types/StudentType"; + +interface LiveResultsFooterProps { + maxQuestions: number; + students: StudentType[]; + showUsernames: boolean; + showCorrectAnswers: boolean; + getStudentGrade: (student: StudentType) => number; + +} + +const LiveResultsTableFooter: React.FC = ({ + maxQuestions, + students, + showUsernames, + showCorrectAnswers, + getStudentGrade +}) => { + + return ( + + {students.map((student) => ( + + +
+ {showUsernames ? student.name : '******'} +
+
+ {Array.from({ length: maxQuestions }, (_, index) => { + const answer = student.answers.find( + (answer) => parseInt(answer.idQuestion.toString()) === index + 1 + ); + const answerText = answer ? answer.answer.toString() : ''; + const isCorrect = answer ? answer.isCorrect : false; + + return ( + + {showCorrectAnswers ? ( +
+ ) : isCorrect ? ( + + ) : ( + answerText !== '' && ( + + ) + )} +
+ ); + })} + + {getStudentGrade(student).toFixed()} % + +
+ ))} +
+ ); +}; +export default LiveResultsTableFooter; \ No newline at end of file diff --git a/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader.tsx b/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader.tsx new file mode 100644 index 0000000..fb4219b --- /dev/null +++ b/client/src/components/LiveResults/LiveResultsTable/TableComponents/LiveResultsTableHeader.tsx @@ -0,0 +1,50 @@ +import { TableCell, TableHead, TableRow } from "@mui/material"; +import React from "react"; + +interface LiveResultsFooterProps { + maxQuestions: number; + showSelectedQuestion: (index: number) => void; +} + +const LiveResultsTableFooter: React.FC = ({ + maxQuestions, + showSelectedQuestion, +}) => { + + return ( + + + +
Nom d'utilisateur
+
+ {Array.from({ length: maxQuestions }, (_, index) => ( + showSelectedQuestion(index)} + > +
{`Q${index + 1}`}
+
+ ))} + +
% réussite
+
+
+
+ ); +}; +export default LiveResultsTableFooter; \ No newline at end of file