diff --git a/LICENSE b/LICENSE
index cf016c7..406c5af 100644
--- a/LICENSE
+++ b/LICENSE
@@ -3,6 +3,7 @@ MIT License
Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest
Copyright (c) 2024 Louis-Antoine Caron, Mathieu Roy, Mélanie St-Hilaire, Samy Waddah
Copyright (c) 2024 Gabriel Moisan-Matte, Mathieu Sévigny-Lavallée, Jerry Kwok Hiu Fung, Bruno Roesner, Florent Serres
+Copyright (c) 2025 Nouhaïla Aâter, Kendrick Chan Hing Wah, Philippe Côté, Edwin Stanley Lopez Andino, Ana Lucia Munteanu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/client/package-lock.json b/client/package-lock.json
index 8105575..3c24ffc 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -14,7 +14,7 @@
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
- "@mui/icons-material": "^6.1.0",
+ "@mui/icons-material": "^6.4.1",
"@mui/lab": "^5.0.0-alpha.153",
"@mui/material": "^6.1.0",
"@types/uuid": "^9.0.7",
@@ -2093,15 +2093,15 @@
}
},
"node_modules/@emotion/serialize": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz",
- "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
- "@emotion/utils": "^1.4.1",
+ "@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
@@ -3355,9 +3355,9 @@
}
},
"node_modules/@mui/core-downloads-tracker": {
- "version": "6.1.6",
- "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.6.tgz",
- "integrity": "sha512-nz1SlR9TdBYYPz4qKoNasMPRiGb4PaIHFkzLzhju0YVYS5QSuFF2+n7CsiHMIDcHv3piPu/xDWI53ruhOqvZwQ==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.1.tgz",
+ "integrity": "sha512-SfDLWMV5b5oXgDf3NTa2hCTPC1d2defhDH2WgFKmAiejC4mSfXYbyi+AFCLzpizauXhgBm8OaZy9BHKnrSpahQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -3365,9 +3365,9 @@
}
},
"node_modules/@mui/icons-material": {
- "version": "6.1.6",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.6.tgz",
- "integrity": "sha512-5r9urIL2lxXb/sPN3LFfFYEibsXJUb986HhhIeu1gOcte460pwdSiEhBSxkAuyT8Dj7jvu9MjqSBmSumQELo8A==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.1.tgz",
+ "integrity": "sha512-wsxFcUTQxt4s+7Bg4GgobqRjyaHLmZGNOs+HJpbwrwmLbT6mhIJxhpqsKzzWq9aDY8xIe7HCjhpH7XI5UD6teA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0"
@@ -3380,7 +3380,7 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@mui/material": "^6.1.6",
+ "@mui/material": "^6.4.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -3464,22 +3464,22 @@
}
},
"node_modules/@mui/material": {
- "version": "6.1.6",
- "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.6.tgz",
- "integrity": "sha512-1yvejiQ/601l5AK3uIdUlAVElyCxoqKnl7QA+2oFB/2qYPWfRwDgavW/MoywS5Y2gZEslcJKhe0s2F3IthgFgw==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.1.tgz",
+ "integrity": "sha512-MFBfia6UiKxyoLeGkAh8M15bkeDmfnsUTMRJd/vTQue6YQ8AQ6lw9HqDthyYghzDEWIvZO/lQQzLrZE8XwNJLA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
- "@mui/core-downloads-tracker": "^6.1.6",
- "@mui/system": "^6.1.6",
- "@mui/types": "^7.2.19",
- "@mui/utils": "^6.1.6",
+ "@mui/core-downloads-tracker": "^6.4.1",
+ "@mui/system": "^6.4.1",
+ "@mui/types": "^7.2.21",
+ "@mui/utils": "^6.4.1",
"@popperjs/core": "^2.11.8",
- "@types/react-transition-group": "^4.4.11",
+ "@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
- "react-is": "^18.3.1",
+ "react-is": "^19.0.0",
"react-transition-group": "^4.4.5"
},
"engines": {
@@ -3492,7 +3492,7 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
- "@mui/material-pigment-css": "^6.1.6",
+ "@mui/material-pigment-css": "^6.4.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -3513,13 +3513,13 @@
}
},
"node_modules/@mui/material/node_modules/@mui/private-theming": {
- "version": "6.1.6",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.6.tgz",
- "integrity": "sha512-ioAiFckaD/fJSnTrUMWgjl9HYBWt7ixCh7zZw7gDZ+Tae7NuprNV6QJK95EidDT7K0GetR2rU3kAeIR61Myttw==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.1.tgz",
+ "integrity": "sha512-DcT7mwK89owwgcEuiE7w458te4CIjHbYWW6Kn6PiR6eLtxBsoBYphA968uqsQAOBQDpbYxvkuFLwhgk4bxoN/Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
- "@mui/utils": "^6.1.6",
+ "@mui/utils": "^6.4.1",
"prop-types": "^15.8.1"
},
"engines": {
@@ -3540,14 +3540,14 @@
}
},
"node_modules/@mui/material/node_modules/@mui/styled-engine": {
- "version": "6.1.6",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.6.tgz",
- "integrity": "sha512-I+yS1cSuSvHnZDBO7e7VHxTWpj+R7XlSZvTC4lS/OIbUNJOMMSd3UDP6V2sfwzAdmdDNBi7NGCRv2SZ6O9hGDA==",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.0.tgz",
+ "integrity": "sha512-ek/ZrDujrger12P6o4luQIfRd2IziH7jQod2WMbLqGE03Iy0zUwYmckRTVhRQTLPNccpD8KXGcALJF+uaUQlbg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
- "@emotion/cache": "^11.13.1",
- "@emotion/serialize": "^1.3.2",
+ "@emotion/cache": "^11.13.5",
+ "@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@@ -3574,16 +3574,16 @@
}
},
"node_modules/@mui/material/node_modules/@mui/system": {
- "version": "6.1.6",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.6.tgz",
- "integrity": "sha512-qOf1VUE9wK8syiB0BBCp82oNBAVPYdj4Trh+G1s+L+ImYiKlubWhhqlnvWt3xqMevR+D2h1CXzA1vhX2FvA+VQ==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.1.tgz",
+ "integrity": "sha512-rgQzgcsHCTtzF9MZ+sL0tOhf2ZBLazpjrujClcb4Siju5lTrK0xX4PsiropActzCemNfM+mOu+0jezAVnfRK8g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
- "@mui/private-theming": "^6.1.6",
- "@mui/styled-engine": "^6.1.6",
- "@mui/types": "^7.2.19",
- "@mui/utils": "^6.1.6",
+ "@mui/private-theming": "^6.4.1",
+ "@mui/styled-engine": "^6.4.0",
+ "@mui/types": "^7.2.21",
+ "@mui/utils": "^6.4.1",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@@ -3614,17 +3614,17 @@
}
},
"node_modules/@mui/material/node_modules/@mui/utils": {
- "version": "6.1.6",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.6.tgz",
- "integrity": "sha512-sBS6D9mJECtELASLM+18WUcXF6RH3zNxBRFeyCRg8wad6NbyNrdxLuwK+Ikvc38sTZwBzAz691HmSofLqHd9sQ==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.1.tgz",
+ "integrity": "sha512-iQUDUeYh87SvR4lVojaRaYnQix8BbRV51MxaV6MBmqthecQoxwSbS5e2wnbDJUeFxY2ppV505CiqPLtd0OWkqw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
- "@mui/types": "^7.2.19",
- "@types/prop-types": "^15.7.13",
+ "@mui/types": "^7.2.21",
+ "@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
- "react-is": "^18.3.1"
+ "react-is": "^19.0.0"
},
"engines": {
"node": ">=14.0.0"
@@ -3643,6 +3643,12 @@
}
}
},
+ "node_modules/@mui/material/node_modules/react-is": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
+ "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
+ "license": "MIT"
+ },
"node_modules/@mui/private-theming": {
"version": "5.16.14",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.14.tgz",
@@ -3743,9 +3749,9 @@
}
},
"node_modules/@mui/types": {
- "version": "7.2.19",
- "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz",
- "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==",
+ "version": "7.2.21",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz",
+ "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==",
"license": "MIT",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -4682,9 +4688,9 @@
"license": "MIT"
},
"node_modules/@types/prop-types": {
- "version": "15.7.13",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
- "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
+ "version": "15.7.14",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
+ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT"
},
"node_modules/@types/react": {
@@ -4718,11 +4724,11 @@
}
},
"node_modules/@types/react-transition-group": {
- "version": "4.4.11",
- "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz",
- "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==",
+ "version": "4.4.12",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+ "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
- "dependencies": {
+ "peerDependencies": {
"@types/react": "*"
}
},
diff --git a/client/package.json b/client/package.json
index 690ed35..2a99173 100644
--- a/client/package.json
+++ b/client/package.json
@@ -18,7 +18,7 @@
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
- "@mui/icons-material": "^6.1.0",
+ "@mui/icons-material": "^6.4.1",
"@mui/lab": "^5.0.0-alpha.153",
"@mui/material": "^6.1.0",
"@types/uuid": "^9.0.7",
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/LiveResultsTable.test.tsx b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/LiveResultsTable.test.tsx
new file mode 100644
index 0000000..021b82d
--- /dev/null
+++ b/client/src/__tests__/components/GiftTemplate/LiveResults/LiveResultsTable/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/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
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/__tests__/components/LiveResults/LiveResults.test.tsx b/client/src/__tests__/components/LiveResults/LiveResults.test.tsx
new file mode 100644
index 0000000..ce6244e
--- /dev/null
+++ b/client/src/__tests__/components/LiveResults/LiveResults.test.tsx
@@ -0,0 +1,163 @@
+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 { Socket } from 'socket.io-client';
+import { BaseQuestion,parse } from 'gift-pegjs';
+
+const mockSocket: Socket = {
+ on: jest.fn(),
+ off: jest.fn(),
+ emit: jest.fn(),
+ connect: jest.fn(),
+ disconnect: jest.fn(),
+} as unknown as Socket;
+
+const mockGiftQuestions = parse(
+ `::Sample Question 1:: Question stem
+ {
+ =Choice 1
+ ~Choice 2
+ }`);
+
+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: 'Choice 1', isCorrect: true }] },
+ { id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: 'Choice 2', isCorrect: false }] },
+];
+
+describe('LiveResults', () => {
+ test('renders the component with questions and students', () => {
+ render(
+
+ );
+ expect(screen.getByText(`Q${1}`)).toBeInTheDocument();
+
+ // Toggle the display of usernames
+ const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
+
+ // 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();
+ });
+ });
+
+ test('toggles the display of usernames', () => {
+ render(
+
+ );
+
+ // Toggle the display of usernames
+ const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
+
+ // Toggle the display of usernames back
+ fireEvent.click(toggleUsernamesSwitch);
+
+ // Check if the usernames are shown again
+ mockStudents.forEach((student) => {
+ expect(screen.getByText(student.name)).toBeInTheDocument();
+ });
+ });
+
+});
+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 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();
+ });
+});
+
+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();
+ });
+});
\ No newline at end of file
diff --git a/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx
new file mode 100644
index 0000000..684f92e
--- /dev/null
+++ b/client/src/__tests__/pages/ManageRoom/ManageRoom.test.tsx
@@ -0,0 +1,253 @@
+import React from 'react';
+import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { MemoryRouter, useNavigate, useParams } from 'react-router-dom';
+import ManageRoom from 'src/pages/Teacher/ManageRoom/ManageRoom';
+import { StudentType } from 'src/Types/StudentType';
+import { QuizType } from 'src/Types/QuizType';
+import webSocketService, { AnswerReceptionFromBackendType } from 'src/services/WebsocketService';
+import ApiService from 'src/services/ApiService';
+import { Socket } from 'socket.io-client';
+
+jest.mock('src/services/WebsocketService');
+jest.mock('src/services/ApiService');
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useNavigate: jest.fn(),
+ useParams: jest.fn(),
+}));
+
+const mockSocket = {
+ on: jest.fn(),
+ off: jest.fn(),
+ emit: jest.fn(),
+ connect: jest.fn(),
+ disconnect: jest.fn(),
+} as unknown as Socket;
+
+const mockQuiz: QuizType = {
+ _id: 'test-quiz-id',
+ title: 'Test Quiz',
+ content: ['::Q1:: Question 1 { =Answer1 ~Answer2 }', '::Q2:: Question 2 { =Answer1 ~Answer2 }'],
+ folderId: 'folder-id',
+ folderName: 'folder-name',
+ userId: 'user-id',
+ created_at: new Date(),
+ updated_at: new Date()
+};
+
+const mockStudents: StudentType[] = [
+ { id: '1', name: 'Student 1', answers: [] },
+ { id: '2', name: 'Student 2', answers: [] },
+];
+
+const mockAnswerData: AnswerReceptionFromBackendType = {
+ answer: 'Answer1',
+ idQuestion: 1,
+ idUser: '1',
+ username: 'Student 1',
+};
+
+describe('ManageRoom', () => {
+ const navigate = jest.fn();
+ const useParamsMock = useParams as jest.Mock;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useNavigate as jest.Mock).mockReturnValue(navigate);
+ useParamsMock.mockReturnValue({ id: 'test-quiz-id' });
+ (ApiService.getQuiz as jest.Mock).mockResolvedValue(mockQuiz);
+ (webSocketService.connect as jest.Mock).mockReturnValue(mockSocket);
+ });
+
+ test('prepares to launch quiz and fetches quiz data', async () => {
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ await act(async () => {
+ const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
+ createSuccessCallback('test-room-name');
+ });
+
+ await waitFor(() => {
+ expect(ApiService.getQuiz).toHaveBeenCalledWith('test-quiz-id');
+ });
+
+ const launchButton = screen.getByText('Lancer');
+ fireEvent.click(launchButton);
+
+ const rythmeButton = screen.getByText('Rythme du professeur');
+ fireEvent.click(rythmeButton);
+
+ const secondLaunchButton = screen.getAllByText('Lancer');
+ fireEvent.click(secondLaunchButton[1]);
+
+ await waitFor(() => {
+ expect(screen.getByText('Test Quiz')).toBeInTheDocument();
+ expect(screen.getByText('Salle: test-room-name')).toBeInTheDocument();
+ expect(screen.getByText('0/60')).toBeInTheDocument();
+ expect(screen.getByText('Question 1/2')).toBeInTheDocument();
+ });
+ });
+
+ test('handles create-success event', async () => {
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ await act(async () => {
+ const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
+ createSuccessCallback('test-room-name');
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText('Salle: test-room-name')).toBeInTheDocument();
+ });
+ });
+
+ test('handles user-joined event', async () => {
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ await act(async () => {
+ const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
+ createSuccessCallback('test-room-name');
+ });
+
+ await act(async () => {
+ const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
+ userJoinedCallback(mockStudents[0]);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText('Student 1')).toBeInTheDocument();
+
+ });
+
+ const launchButton = screen.getByText('Lancer');
+ fireEvent.click(launchButton);
+
+ const rythmeButton = screen.getByText('Rythme du professeur');
+ fireEvent.click(rythmeButton);
+
+ const secondLaunchButton = screen.getAllByText('Lancer');
+ fireEvent.click(secondLaunchButton[1]);
+
+ await waitFor(() => {
+ expect(screen.getByText('1/60')).toBeInTheDocument();
+
+ });
+ });
+
+ test('handles submit-answer-room event', async () => {
+ const consoleSpy = jest.spyOn(console, 'log');
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ await act(async () => {
+ const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
+ createSuccessCallback('test-room-name');
+ });
+
+ const launchButton = screen.getByText('Lancer');
+ fireEvent.click(launchButton);
+
+ const rythmeButton = screen.getByText('Rythme du professeur');
+ fireEvent.click(rythmeButton);
+
+ const secondLaunchButton = screen.getAllByText('Lancer');
+ fireEvent.click(secondLaunchButton[1]);
+
+ await act(async () => {
+ const userJoinedCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'user-joined')[1];
+ userJoinedCallback(mockStudents[0]);
+ });
+
+ await act(async () => {
+ const submitAnswerCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'submit-answer-room')[1];
+ submitAnswerCallback(mockAnswerData);
+ });
+
+ await waitFor(() => {
+ expect(consoleSpy).toHaveBeenCalledWith('Received answer from Student 1 for question 1: Answer1');
+ });
+
+ consoleSpy.mockRestore();
+ });
+
+ test('handles next question', async () => {
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ await act(async () => {
+ const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
+ createSuccessCallback('test-room-name');
+ });
+
+ const launchButton = screen.getByText('Lancer');
+ fireEvent.click(launchButton);
+
+ const rythmeButton = screen.getByText('Rythme du professeur');
+ fireEvent.click(rythmeButton);
+
+ const secondLaunchButton = screen.getAllByText('Lancer');
+ fireEvent.click(secondLaunchButton[1]);
+
+ const nextQuestionButton = screen.getByText('Prochaine question');
+ fireEvent.click(nextQuestionButton);
+
+ await waitFor(() => {
+ expect(screen.getByText('Question 2/2')).toBeInTheDocument();
+ });
+ });
+
+ test('handles disconnect', async () => {
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ await act(async () => {
+ const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
+ createSuccessCallback('test-room-name');
+ });
+
+ const disconnectButton = screen.getByText('Quitter');
+ fireEvent.click(disconnectButton);
+
+ const confirmButton = screen.getAllByText('Confirmer');
+ fireEvent.click(confirmButton[1]);
+
+ await waitFor(() => {
+ expect(webSocketService.disconnect).toHaveBeenCalled();
+ expect(navigate).toHaveBeenCalledWith('/teacher/dashboard');
+ });
+ });
+});
\ No newline at end of file
diff --git a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx
index f3b3e57..0332f1a 100644
--- a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx
+++ b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx
@@ -53,7 +53,7 @@ describe('TeacherModeQuiz', () => {
fireEvent.click(screen.getByText('Répondre'));
});
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1);
- expect(screen.getByText('Votre réponse est "Option A".')).toBeInTheDocument();
+ expect(screen.getByText('Votre réponse est:')).toBeInTheDocument();
});
test('handles disconnect button click', () => {
diff --git a/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx b/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx
index 98d2b96..036f2d0 100644
--- a/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx
+++ b/client/src/components/GIFTCheatSheet/GiftCheatSheet.tsx
@@ -21,11 +21,11 @@ const GiftCheatSheet: React.FC = () => {
};
- const QuestionVraiFaux = "2+2 \\= 4 ? {T}\n// Utilisez les valeurs {T}, {F}, {TRUE} \net {FALSE}.";
- const QuestionChoixMul = "Quelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Bonne réponse!\n}\n// La bonne réponse est Ottawa";
- const QuestionChoixMulMany = "Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### La bonne réponse est Montréal, Ottawa et Vancouver \n}\n// Utilisez tilde (signe de vague) pour toutes les réponses.\n// On doit indiquer le pourcentage de chaque réponse.";
- const QuestionCourte ="Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n}\n// Permet de fournir plusieurs bonnes réponses.\n// Note: La casse n'est pas prise en compte.";
- const QuestionNum ="// Question de plage mathématique. \n Quel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
+ const QuestionVraiFaux = "::Exemple de question vrai/faux:: \n 2+2 \\= 4 ? {T} //Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}.";
+ const QuestionChoixMul = "::Ville capitale du Canada:: \nQuelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Rétroaction spécifique.\n} // Commentaire non visible (au besoin)";
+ const QuestionChoixMulMany = "::Villes canadiennes:: \n Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### Rétroaction globale de la question. \n} // Utilisez tilde (signe de vague) pour toutes les réponses. // On doit indiquer le pourcentage de chaque réponse.";
+ const QuestionCourte ="::Clé et porte:: \n Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n} // Permet de fournir plusieurs bonnes réponses. // Note: La casse n'est pas prise en compte.";
+ const QuestionNum ="::Question numérique avec marge:: \nQuel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n ::Question numérique avec plage:: \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\n::Question numérique avec plusieurs réponses::\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
return (
Informations pratiques sur l'éditeur
@@ -79,7 +79,7 @@ const GiftCheatSheet: React.FC = () => {
-
5. Question numérique
+
5. Questions numériques
{
diff --git a/client/src/components/GiftTemplate/GIFTTemplatePreview.tsx b/client/src/components/GiftTemplate/GIFTTemplatePreview.tsx
index 51dbd3f..1bacf8c 100644
--- a/client/src/components/GiftTemplate/GIFTTemplatePreview.tsx
+++ b/client/src/components/GiftTemplate/GIFTTemplatePreview.tsx
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
import Template, { ErrorTemplate } from './templates';
import { parse } from 'gift-pegjs';
import './styles.css';
-import DOMPurify from 'dompurify';
+import { FormattedTextTemplate } from './templates/TextTypeTemplate';
interface GIFTTemplatePreviewProps {
questions: string[];
@@ -74,7 +74,8 @@ const GIFTTemplatePreview: React.FC = ({
{error}
) : isPreviewReady ? (
) : (
Chargement de la prévisualisation...
diff --git a/client/src/components/GiftTemplate/templates/TextTypeTemplate.ts b/client/src/components/GiftTemplate/templates/TextTypeTemplate.ts
index 8a3e24b..c5569f4 100644
--- a/client/src/components/GiftTemplate/templates/TextTypeTemplate.ts
+++ b/client/src/components/GiftTemplate/templates/TextTypeTemplate.ts
@@ -4,14 +4,24 @@ import katex from 'katex';
import { TextFormat } from 'gift-pegjs';
import DOMPurify from 'dompurify'; // cleans HTML to prevent XSS attacks, etc.
-export function formatLatex(text: string): string {
- return text
+function formatLatex(text: string): string {
+
+ let renderedText = '';
+
+ try {
+ renderedText = text
.replace(/\$\$(.*?)\$\$/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
.replace(/\$(.*?)\$/g, (_, inner) => katex.renderToString(inner, { displayMode: false }))
.replace(/\\\[(.*?)\\\]/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
.replace(/\\\((.*?)\\\)/g, (_, inner) =>
katex.renderToString(inner, { displayMode: false })
);
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (error) {
+ renderedText = text;
+ }
+
+ return renderedText;
}
/**
diff --git a/client/src/components/LaunchQuizDialog/LaunchQuizDialog.tsx b/client/src/components/LaunchQuizDialog/LaunchQuizDialog.tsx
index 3bd307b..3aaf401 100644
--- a/client/src/components/LaunchQuizDialog/LaunchQuizDialog.tsx
+++ b/client/src/components/LaunchQuizDialog/LaunchQuizDialog.tsx
@@ -47,10 +47,10 @@ const LaunchQuizDialog: React.FC = ({ open, handleOnClose, launchQuiz, se
diff --git a/client/src/components/LiveResults/LiveResults.tsx b/client/src/components/LiveResults/LiveResults.tsx
index 4731aa8..f165e10 100644
--- a/client/src/components/LiveResults/LiveResults.tsx
+++ b/client/src/components/LiveResults/LiveResults.tsx
@@ -1,27 +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 {
- Button,
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/LiveResultsTable';
interface LiveResultsProps {
socket: Socket | null;
@@ -31,269 +20,10 @@ 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 [showFullAnswer, setShowFullAnswer] = useState(false);
- const [selectedAnswer, setSelectedAnswer] = useState('');
-
- const handleShowAnswer = (answer: string) => {
- setSelectedAnswer(answer);
- setShowFullAnswer(true);
- };
-
- const renderAnswerCell = (answer: string) => {
- if (!answer) return null;
- const shortAnswer = answer.length > 20 ? answer.slice(0, 20) : answer;
-
- return (
- <>
- {shortAnswer}
- {answer.length > 20 && (
-
- )}
- >
- );
- };
- // const [students, setStudents] = useState(initialStudents);
- // const [studentResultsMap, setStudentResultsMap] = useState
-
-
-
-
-
- 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 ? (
- {renderAnswerCell(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()} %` : '-'}
-
-
-
-
-
-
- {showFullAnswer && (
- setShowFullAnswer(false)}
- style={{
- position: 'fixed',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- background: 'rgba(0,0,0,0.3)',
- zIndex: 9999,}}>
-
-
-)}
+
+
+
);
};
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
diff --git a/client/src/components/QuestionsDisplay/questionStyle.css b/client/src/components/QuestionsDisplay/questionStyle.css
index 3958e92..cdf611f 100644
--- a/client/src/components/QuestionsDisplay/questionStyle.css
+++ b/client/src/components/QuestionsDisplay/questionStyle.css
@@ -27,7 +27,6 @@
}
.question-wrapper .katex {
- display: block;
text-align: center;
}
@@ -120,9 +119,9 @@
}
.feedback-container {
- margin-left: 1.1rem;
- display: inline-flex !important; /* override the parent */
+ display: inline-block !important; /* override the parent */
align-items: center;
+ margin-left: 1.1rem;
position: relative;
padding: 0 0.5rem;
background-color: hsl(43, 100%, 94%);
diff --git a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx
index ae2d382..3188211 100644
--- a/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx
+++ b/client/src/components/TeacherModeQuiz/TeacherModeQuiz.tsx
@@ -23,16 +23,33 @@ const TeacherModeQuiz: React.FC = ({
}) => {
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false);
- const [feedbackMessage, setFeedbackMessage] = useState('');
+ const [feedbackMessage, setFeedbackMessage] = useState('');
+
+ const renderFeedbackMessage = (answer: string) => {
+ if(answer === 'true' || answer === 'false'){
+ return (
+ Votre réponse est: {answer==="true" ? 'Vrai' : 'Faux'}
+ )
+ }
+ else{
+ return (
+
+ Votre réponse est: {answer.toString()}
+
+ );}
+ };
useEffect(() => {
+ // Close the feedback dialog when the question changes
+ handleFeedbackDialogClose();
setIsAnswerSubmitted(false);
- }, [questionInfos]);
+
+ }, [questionInfos.question]);
const handleOnSubmitAnswer = (answer: string | number | boolean) => {
const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion);
- setFeedbackMessage(`Votre réponse est "${answer.toString()}".`);
+ setFeedbackMessage(renderFeedbackMessage(answer.toString()));
setIsFeedbackDialogOpen(true);
};
@@ -74,7 +91,17 @@ const TeacherModeQuiz: React.FC = ({
>
Rétroaction
+
{feedbackMessage}
+
Question :
+
+
{
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
const [connectingError, setConnectingError] = useState('');
const [currentQuestion, setCurrentQuestion] = useState(undefined);
-
+ const [quizStarted, setQuizStarted] = useState(false);
+
useEffect(() => {
if (quizId.id) {
const fetchquiz = async () => {
@@ -145,7 +147,7 @@ const ManageRoom: React.FC = () => {
console.log('Quiz questions not found (cannot update answers without them).');
return;
}
-
+
// Update the students state using the functional form of setStudents
setStudents((prevStudents) => {
// print the list of current student names
@@ -153,7 +155,7 @@ const ManageRoom: React.FC = () => {
prevStudents.forEach((student) => {
console.log(student.name);
});
-
+
let foundStudent = false;
const updatedStudents = prevStudents.map((student) => {
console.log(`Comparing ${student.id} to ${idUser}`);
@@ -173,7 +175,7 @@ const ManageRoom: React.FC = () => {
updatedAnswers = [...student.answers, newAnswer];
}
return { ...student, answers: updatedAnswers };
- }
+ }
return student;
});
if (!foundStudent) {
@@ -316,13 +318,18 @@ const ManageRoom: React.FC = () => {
if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) {
// TODO: This error happens when token expires! Need to handle it properly
console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`);
+ setQuizStarted(true);
+
return;
}
switch (quizMode) {
case 'student':
+ setQuizStarted(true);
return launchStudentMode();
case 'teacher':
+ setQuizStarted(true);
return launchTeacherMode();
+
}
};
@@ -427,9 +434,19 @@ const ManageRoom: React.FC = () => {
askConfirm
message={`Êtes-vous sûr de vouloir quitter?`} />
-
-
Salle: {roomName}
-
Utilisateurs: {students.length}/60
+
+
+
+
+
+ {quizStarted && (
+
+
+ {students.length}/60
+
+ )}
@@ -441,8 +458,12 @@ const ManageRoom: React.FC = () => {
{quizQuestions ? (
-
{quiz?.title}
+ {!isNaN(Number(currentQuestion?.question.id)) && (
+
+ Question {Number(currentQuestion?.question.id)}/{quizQuestions?.length}
+
+ )}
{quizMode === 'teacher' && (
@@ -479,23 +500,23 @@ const ManageRoom: React.FC = () => {
{quizMode === 'teacher' && (
-
-
-
-
-
-
-
-
)}
+
+
+
+
+
+
+
+
)}
diff --git a/client/src/pages/Teacher/ManageRoom/manageRoom.css b/client/src/pages/Teacher/ManageRoom/manageRoom.css
index ffb83fa..ad870a9 100644
--- a/client/src/pages/Teacher/ManageRoom/manageRoom.css
+++ b/client/src/pages/Teacher/ManageRoom/manageRoom.css
@@ -18,8 +18,8 @@
display: flex;
flex-direction: column;
- justify-content: center;
- align-items: center;
+ justify-content: flex-end;
+ align-items: flex-end;
}