Affichage du tableau AVEC réponses longue devrait être adaptés

Fixes #228
This commit is contained in:
JubaAzul 2025-02-13 18:03:55 -05:00
commit 744af1efc0
24 changed files with 1298 additions and 569 deletions

View file

@ -3,6 +3,7 @@ MIT License
Copyright (c) 2023 ETS-PFE004-Plateforme-sondage-minitest 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 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) 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

114
client/package-lock.json generated
View file

@ -14,7 +14,7 @@
"@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@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/lab": "^5.0.0-alpha.153",
"@mui/material": "^6.1.0", "@mui/material": "^6.1.0",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
@ -2093,15 +2093,15 @@
} }
}, },
"node_modules/@emotion/serialize": { "node_modules/@emotion/serialize": {
"version": "1.3.2", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@emotion/hash": "^0.9.2", "@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0", "@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0", "@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.1", "@emotion/utils": "^1.4.2",
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
@ -3355,9 +3355,9 @@
} }
}, },
"node_modules/@mui/core-downloads-tracker": { "node_modules/@mui/core-downloads-tracker": {
"version": "6.1.6", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.6.tgz", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.1.tgz",
"integrity": "sha512-nz1SlR9TdBYYPz4qKoNasMPRiGb4PaIHFkzLzhju0YVYS5QSuFF2+n7CsiHMIDcHv3piPu/xDWI53ruhOqvZwQ==", "integrity": "sha512-SfDLWMV5b5oXgDf3NTa2hCTPC1d2defhDH2WgFKmAiejC4mSfXYbyi+AFCLzpizauXhgBm8OaZy9BHKnrSpahQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@ -3365,9 +3365,9 @@
} }
}, },
"node_modules/@mui/icons-material": { "node_modules/@mui/icons-material": {
"version": "6.1.6", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.6.tgz", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.1.tgz",
"integrity": "sha512-5r9urIL2lxXb/sPN3LFfFYEibsXJUb986HhhIeu1gOcte460pwdSiEhBSxkAuyT8Dj7jvu9MjqSBmSumQELo8A==", "integrity": "sha512-wsxFcUTQxt4s+7Bg4GgobqRjyaHLmZGNOs+HJpbwrwmLbT6mhIJxhpqsKzzWq9aDY8xIe7HCjhpH7XI5UD6teA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.26.0" "@babel/runtime": "^7.26.0"
@ -3380,7 +3380,7 @@
"url": "https://opencollective.com/mui-org" "url": "https://opencollective.com/mui-org"
}, },
"peerDependencies": { "peerDependencies": {
"@mui/material": "^6.1.6", "@mui/material": "^6.4.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"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": { "node_modules/@mui/material": {
"version": "6.1.6", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.6.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.1.tgz",
"integrity": "sha512-1yvejiQ/601l5AK3uIdUlAVElyCxoqKnl7QA+2oFB/2qYPWfRwDgavW/MoywS5Y2gZEslcJKhe0s2F3IthgFgw==", "integrity": "sha512-MFBfia6UiKxyoLeGkAh8M15bkeDmfnsUTMRJd/vTQue6YQ8AQ6lw9HqDthyYghzDEWIvZO/lQQzLrZE8XwNJLA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.26.0", "@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.1.6", "@mui/core-downloads-tracker": "^6.4.1",
"@mui/system": "^6.1.6", "@mui/system": "^6.4.1",
"@mui/types": "^7.2.19", "@mui/types": "^7.2.21",
"@mui/utils": "^6.1.6", "@mui/utils": "^6.4.1",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.11", "@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"csstype": "^3.1.3", "csstype": "^3.1.3",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react-is": "^18.3.1", "react-is": "^19.0.0",
"react-transition-group": "^4.4.5" "react-transition-group": "^4.4.5"
}, },
"engines": { "engines": {
@ -3492,7 +3492,7 @@
"peerDependencies": { "peerDependencies": {
"@emotion/react": "^11.5.0", "@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.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", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"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" "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@ -3513,13 +3513,13 @@
} }
}, },
"node_modules/@mui/material/node_modules/@mui/private-theming": { "node_modules/@mui/material/node_modules/@mui/private-theming": {
"version": "6.1.6", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.6.tgz", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.1.tgz",
"integrity": "sha512-ioAiFckaD/fJSnTrUMWgjl9HYBWt7ixCh7zZw7gDZ+Tae7NuprNV6QJK95EidDT7K0GetR2rU3kAeIR61Myttw==", "integrity": "sha512-DcT7mwK89owwgcEuiE7w458te4CIjHbYWW6Kn6PiR6eLtxBsoBYphA968uqsQAOBQDpbYxvkuFLwhgk4bxoN/Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.26.0", "@babel/runtime": "^7.26.0",
"@mui/utils": "^6.1.6", "@mui/utils": "^6.4.1",
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
}, },
"engines": { "engines": {
@ -3540,14 +3540,14 @@
} }
}, },
"node_modules/@mui/material/node_modules/@mui/styled-engine": { "node_modules/@mui/material/node_modules/@mui/styled-engine": {
"version": "6.1.6", "version": "6.4.0",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.6.tgz", "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.0.tgz",
"integrity": "sha512-I+yS1cSuSvHnZDBO7e7VHxTWpj+R7XlSZvTC4lS/OIbUNJOMMSd3UDP6V2sfwzAdmdDNBi7NGCRv2SZ6O9hGDA==", "integrity": "sha512-ek/ZrDujrger12P6o4luQIfRd2IziH7jQod2WMbLqGE03Iy0zUwYmckRTVhRQTLPNccpD8KXGcALJF+uaUQlbg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.26.0", "@babel/runtime": "^7.26.0",
"@emotion/cache": "^11.13.1", "@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.2", "@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0", "@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3", "csstype": "^3.1.3",
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
@ -3574,16 +3574,16 @@
} }
}, },
"node_modules/@mui/material/node_modules/@mui/system": { "node_modules/@mui/material/node_modules/@mui/system": {
"version": "6.1.6", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.6.tgz", "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.1.tgz",
"integrity": "sha512-qOf1VUE9wK8syiB0BBCp82oNBAVPYdj4Trh+G1s+L+ImYiKlubWhhqlnvWt3xqMevR+D2h1CXzA1vhX2FvA+VQ==", "integrity": "sha512-rgQzgcsHCTtzF9MZ+sL0tOhf2ZBLazpjrujClcb4Siju5lTrK0xX4PsiropActzCemNfM+mOu+0jezAVnfRK8g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.26.0", "@babel/runtime": "^7.26.0",
"@mui/private-theming": "^6.1.6", "@mui/private-theming": "^6.4.1",
"@mui/styled-engine": "^6.1.6", "@mui/styled-engine": "^6.4.0",
"@mui/types": "^7.2.19", "@mui/types": "^7.2.21",
"@mui/utils": "^6.1.6", "@mui/utils": "^6.4.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"csstype": "^3.1.3", "csstype": "^3.1.3",
"prop-types": "^15.8.1" "prop-types": "^15.8.1"
@ -3614,17 +3614,17 @@
} }
}, },
"node_modules/@mui/material/node_modules/@mui/utils": { "node_modules/@mui/material/node_modules/@mui/utils": {
"version": "6.1.6", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.6.tgz", "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.1.tgz",
"integrity": "sha512-sBS6D9mJECtELASLM+18WUcXF6RH3zNxBRFeyCRg8wad6NbyNrdxLuwK+Ikvc38sTZwBzAz691HmSofLqHd9sQ==", "integrity": "sha512-iQUDUeYh87SvR4lVojaRaYnQix8BbRV51MxaV6MBmqthecQoxwSbS5e2wnbDJUeFxY2ppV505CiqPLtd0OWkqw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.26.0", "@babel/runtime": "^7.26.0",
"@mui/types": "^7.2.19", "@mui/types": "^7.2.21",
"@types/prop-types": "^15.7.13", "@types/prop-types": "^15.7.14",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react-is": "^18.3.1" "react-is": "^19.0.0"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "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": { "node_modules/@mui/private-theming": {
"version": "5.16.14", "version": "5.16.14",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.14.tgz", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.14.tgz",
@ -3743,9 +3749,9 @@
} }
}, },
"node_modules/@mui/types": { "node_modules/@mui/types": {
"version": "7.2.19", "version": "7.2.21",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz",
"integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
@ -4682,9 +4688,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.13", "version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
@ -4718,11 +4724,11 @@
} }
}, },
"node_modules/@types/react-transition-group": { "node_modules/@types/react-transition-group": {
"version": "4.4.11", "version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT", "license": "MIT",
"dependencies": { "peerDependencies": {
"@types/react": "*" "@types/react": "*"
} }
}, },

View file

@ -18,7 +18,7 @@
"@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@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/lab": "^5.0.0-alpha.153",
"@mui/material": "^6.1.0", "@mui/material": "^6.1.0",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",

View file

@ -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(
<LiveResults
socket={null}
questions={mockQuestions}
showSelectedQuestion={mockShowSelectedQuestion}
quizMode="teacher"
students={mockStudents}
/>
);
expect(screen.getByText('Résultats du quiz')).toBeInTheDocument();
});
test('toggles show usernames switch', () => {
render(
<LiveResults
socket={null}
questions={mockQuestions}
showSelectedQuestion={mockShowSelectedQuestion}
quizMode="teacher"
students={mockStudents}
/>
);
const switchElement = screen.getByLabelText('Afficher les noms');
expect(switchElement).toBeInTheDocument();
fireEvent.click(switchElement);
expect(switchElement).toBeChecked();
});
test('toggles show correct answers switch', () => {
render(
<LiveResults
socket={null}
questions={mockQuestions}
showSelectedQuestion={mockShowSelectedQuestion}
quizMode="teacher"
students={mockStudents}
/>
);
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(
<LiveResults
socket={null}
questions={mockQuestions}
showSelectedQuestion={mockShowSelectedQuestion}
quizMode="teacher"
students={mockStudents}
/>
);
const tableCell = screen.getByText('Q1');
fireEvent.click(tableCell);
expect(mockShowSelectedQuestion).toHaveBeenCalled();
});
});

View file

@ -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(
<LiveResultsTable
questions={mockQuestions}
students={mockStudents}
showCorrectAnswers={false}
showSelectedQuestion={mockShowSelectedQuestion}
showUsernames={true}
/>
);
expect(screen.getByText('Student 1')).toBeInTheDocument();
expect(screen.getByText('Student 2')).toBeInTheDocument();
});
test('displays correct and incorrect answers', () => {
render(
<LiveResultsTable
questions={mockQuestions}
students={mockStudents}
showCorrectAnswers={true}
showSelectedQuestion={mockShowSelectedQuestion}
showUsernames={true}
/>
);
expect(screen.getByText('Answer 1')).toBeInTheDocument();
expect(screen.getByText('Answer 2')).toBeInTheDocument();
});
test('calls showSelectedQuestion when a table cell is clicked', () => {
render(
<LiveResultsTable
questions={mockQuestions}
students={mockStudents}
showCorrectAnswers={true}
showSelectedQuestion={mockShowSelectedQuestion}
showUsernames={true}
/>
);
const tableCell = screen.getByText('Q1');
fireEvent.click(tableCell);
expect(mockShowSelectedQuestion).toHaveBeenCalled();
});
test('calculates and displays student grades', () => {
render(
<LiveResultsTable
questions={mockQuestions}
students={mockStudents}
showCorrectAnswers={true}
showSelectedQuestion={mockShowSelectedQuestion}
showUsernames={true}
/>
);
//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(
<LiveResultsTable
questions={mockQuestions}
students={mockStudents}
showCorrectAnswers={true}
showSelectedQuestion={mockShowSelectedQuestion}
showUsernames={true}
/>
);
//1 good answer out of 4 possible good answers (the second question has not been answered)
expect(screen.getByText('25 %')).toBeInTheDocument();
});
});

View file

@ -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(
<LiveResultsTableBody
maxQuestions={2}
students={mockStudents}
showUsernames={true}
showCorrectAnswers={false}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByText('Student 1')).toBeInTheDocument();
expect(screen.getByText('Student 2')).toBeInTheDocument();
});
test('displays correct and incorrect answers', () => {
render(
<LiveResultsTableBody
maxQuestions={2}
students={mockStudents}
showUsernames={true}
showCorrectAnswers={true}
getStudentGrade={mockGetStudentGrade}
/>
);
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(
<LiveResultsTableBody
maxQuestions={2}
students={mockStudents}
showUsernames={true}
showCorrectAnswers={false}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByLabelText('correct')).toBeInTheDocument();
expect(screen.getByLabelText('incorrect')).toBeInTheDocument();
});
test('hides usernames when showUsernames is false', () => {
render(
<LiveResultsTableBody
maxQuestions={2}
students={mockStudents}
showUsernames={false}
showCorrectAnswers={true}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getAllByText('******').length).toBe(2);
});
});

View file

@ -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(
<LiveResultsTableFooter
maxQuestions={2}
students={mockStudents}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByText('% réussite')).toBeInTheDocument();
});
test('calculates and displays correct answers per question', () => {
render(
<LiveResultsTableFooter
maxQuestions={2}
students={mockStudents}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByText('50 %')).toBeInTheDocument();
expect(screen.getByText('0 %')).toBeInTheDocument();
});
test('calculates and displays class average', () => {
render(
<LiveResultsTableFooter
maxQuestions={2}
students={mockStudents}
getStudentGrade={mockGetStudentGrade}
/>
);
expect(screen.getByText('50 %')).toBeInTheDocument();
});
});

View file

@ -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(
<LiveResultsTableHeader
maxQuestions={5}
showSelectedQuestion={mockShowSelectedQuestion}
/>
);
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(
<LiveResultsTableHeader
maxQuestions={5}
showSelectedQuestion={mockShowSelectedQuestion}
/>
);
const questionHeader = screen.getByText('Q1');
fireEvent.click(questionHeader);
expect(mockShowSelectedQuestion).toHaveBeenCalledWith(0);
});
test('renders the correct number of question headers', () => {
render(
<LiveResultsTableHeader
maxQuestions={3}
showSelectedQuestion={mockShowSelectedQuestion}
/>
);
for (let i = 1; i <= 3; i++) {
expect(screen.getByText(`Q${i}`)).toBeInTheDocument();
}
});
});

View file

@ -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(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
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(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
// 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(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
// 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(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
// 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(
<LiveResults
socket={mockSocket}
questions={mockQuestions}
showSelectedQuestion={jest.fn()}
quizMode="teacher"
students={mockStudents}
/>
);
// 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();
});
});

View file

@ -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(
<MemoryRouter>
<ManageRoom />
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-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(
<MemoryRouter>
<ManageRoom />
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-name');
});
await waitFor(() => {
expect(screen.getByText('Salle: test-room-name')).toBeInTheDocument();
});
});
test('handles user-joined event', async () => {
await act(async () => {
render(
<MemoryRouter>
<ManageRoom />
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-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(
<MemoryRouter>
<ManageRoom />
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-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(
<MemoryRouter>
<ManageRoom />
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-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(
<MemoryRouter>
<ManageRoom />
</MemoryRouter>
);
});
await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-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');
});
});
});

View file

@ -53,7 +53,7 @@ describe('TeacherModeQuiz', () => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); 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', () => { test('handles disconnect button click', () => {

View file

@ -21,11 +21,11 @@ const GiftCheatSheet: React.FC = () => {
}; };
const QuestionVraiFaux = "2+2 \\= 4 ? {T}\n// Utilisez les valeurs {T}, {F}, {TRUE} \net {FALSE}."; const QuestionVraiFaux = "::Exemple de question vrai/faux:: \n 2+2 \\= 4 ? {T} //Utilisez les valeurs {T}, {F}, {TRUE} et {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 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 = "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 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 ="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 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 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 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 ( return (
<div className="gift-cheat-sheet"> <div className="gift-cheat-sheet">
<h2 className="subtitle">Informations pratiques sur l&apos;éditeur</h2> <h2 className="subtitle">Informations pratiques sur l&apos;éditeur</h2>
@ -79,7 +79,7 @@ const GiftCheatSheet: React.FC = () => {
</div> </div>
<div className="question-type"> <div className="question-type">
<h4> 5. Question numérique </h4> <h4> 5. Questions numériques </h4>
<pre> <pre>
<code className="question-code-block selectable-text"> <code className="question-code-block selectable-text">
{ {

View file

@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
import Template, { ErrorTemplate } from './templates'; import Template, { ErrorTemplate } from './templates';
import { parse } from 'gift-pegjs'; import { parse } from 'gift-pegjs';
import './styles.css'; import './styles.css';
import DOMPurify from 'dompurify'; import { FormattedTextTemplate } from './templates/TextTypeTemplate';
interface GIFTTemplatePreviewProps { interface GIFTTemplatePreviewProps {
questions: string[]; questions: string[];
@ -74,7 +74,8 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
<div className="error">{error}</div> <div className="error">{error}</div>
) : isPreviewReady ? ( ) : isPreviewReady ? (
<div data-testid="preview-container"> <div data-testid="preview-container">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(items) }}></div>
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: 'html', text: items }) }}></div>
</div> </div>
) : ( ) : (
<div className="loading">Chargement de la prévisualisation...</div> <div className="loading">Chargement de la prévisualisation...</div>

View file

@ -4,14 +4,24 @@ import katex from 'katex';
import { TextFormat } from 'gift-pegjs'; import { TextFormat } from 'gift-pegjs';
import DOMPurify from 'dompurify'; // cleans HTML to prevent XSS attacks, etc. import DOMPurify from 'dompurify'; // cleans HTML to prevent XSS attacks, etc.
export function formatLatex(text: string): string { function formatLatex(text: string): string {
return text
let renderedText = '';
try {
renderedText = text
.replace(/\$\$(.*?)\$\$/g, (_, inner) => katex.renderToString(inner, { displayMode: true })) .replace(/\$\$(.*?)\$\$/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
.replace(/\$(.*?)\$/g, (_, inner) => katex.renderToString(inner, { displayMode: false })) .replace(/\$(.*?)\$/g, (_, inner) => katex.renderToString(inner, { displayMode: false }))
.replace(/\\\[(.*?)\\\]/g, (_, inner) => katex.renderToString(inner, { displayMode: true })) .replace(/\\\[(.*?)\\\]/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
.replace(/\\\((.*?)\\\)/g, (_, inner) => .replace(/\\\((.*?)\\\)/g, (_, inner) =>
katex.renderToString(inner, { displayMode: false }) katex.renderToString(inner, { displayMode: false })
); );
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
renderedText = text;
}
return renderedText;
} }
/** /**

View file

@ -47,10 +47,10 @@ const LaunchQuizDialog: React.FC<Props> = ({ open, handleOnClose, launchQuiz, se
<DialogActions> <DialogActions>
<Button variant="outlined" onClick={handleOnClose}> <Button variant="outlined" onClick={handleOnClose}>
Annuler <div>Annuler</div>
</Button> </Button>
<Button variant="contained" onClick={launchQuiz}> <Button variant="contained" onClick={launchQuiz}>
Lancer <div>Lancer</div>
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View file

@ -1,27 +1,16 @@
// LiveResults.tsx // LiveResults.tsx
import React, { useMemo, useState } from 'react'; import React, { useState } from 'react';
import { Socket } from 'socket.io-client'; 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 { QuestionType } from '../../Types/QuestionType';
import './liveResult.css'; import './liveResult.css';
import { import {
Button,
FormControlLabel, FormControlLabel,
FormGroup, FormGroup,
Paper,
Switch, Switch,
Table,
TableBody,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableRow
} from '@mui/material'; } from '@mui/material';
import { StudentType } from '../../Types/StudentType'; import { StudentType } from '../../Types/StudentType';
import { formatLatex } from '../GiftTemplate/templates/TextTypeTemplate';
import LiveResultsTable from './LiveResultsTable/LiveResultsTable';
interface LiveResultsProps { interface LiveResultsProps {
socket: Socket | null; socket: Socket | null;
@ -31,269 +20,10 @@ interface LiveResultsProps {
students: StudentType[] students: StudentType[]
} }
// interface Answer {
// answer: string | number | boolean;
// isCorrect: boolean;
// idQuestion: number;
// }
// interface StudentResult {
// username: string;
// idUser: string;
// answers: Answer[];
// }
const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuestion, students }) => { const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuestion, students }) => {
const [showUsernames, setShowUsernames] = useState<boolean>(false); const [showUsernames, setShowUsernames] = useState<boolean>(false);
const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false); const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(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 (
<>
<span>{shortAnswer}</span>
{answer.length > 20 && (
<button onClick={() => handleShowAnswer(answer)}
style={{
backgroundColor: '#D3D3D3', // Darker background color
color: '#fff',
border: 'none',
borderRadius: '4px',
padding: '0.1rem 0.4rem',
cursor: 'pointer',
marginLeft: '0.5rem'
}}>...</button>
)}
</>
);
};
// const [students, setStudents] = useState<StudentType[]>(initialStudents);
// const [studentResultsMap, setStudentResultsMap] = useState<Map<string, StudentResult>>(new Map());
const maxQuestions = questions.length;
// useEffect(() => {
// // Initialize the map with the current students
// const newStudentResultsMap = new Map<string, StudentResult>();
// 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 ( return (
@ -328,201 +58,15 @@ const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuesti
</div> </div>
<div className="table-container"> <div className="table-container">
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell className="sticky-column">
<div className="text-base text-bold">Nom d&apos;utilisateur</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index} <LiveResultsTable
sx={{ students={students}
textAlign: 'center', questions={questions}
cursor: 'pointer', showCorrectAnswers={showCorrectAnswers}
borderStyle: 'solid', showSelectedQuestion={showSelectedQuestion}
borderWidth: 1, showUsernames={showUsernames}
borderColor: 'rgba(224, 224, 224, 1)' />
}}
onClick={() => showSelectedQuestion(index)}
>
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
</TableCell>
))}
<TableCell
className="sticky-header"
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base text-bold">% réussite</div>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{students.map((student) => (
<TableRow key={student.id}>
<TableCell
className="sticky-column"
sx={{
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base">
{showUsernames ? student.name : '******'}
</div>
</TableCell>
{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 (
<TableCell
style={{ maxWidth: '65px'}}
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
className={
answerText === ''
? ''
: isCorrect
? 'correct-answer'
: 'incorrect-answer'
}
>
{showCorrectAnswers ? (
<div>{renderAnswerCell(formatLatex(answerText))}</div>
) : isCorrect ? (
<FontAwesomeIcon icon={faCheck} />
) : (
answerText !== '' && (
<FontAwesomeIcon icon={faCircleXmark} />
)
)}
</TableCell>
);
})}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{getStudentGrade(student).toFixed()} %
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
<TableCell className="sticky-column" sx={{ color: 'black' }}>
<div className="text-base text-bold">% réussite</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0
? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
: '-'}
</TableCell>
))}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
fontSize: '1rem',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
</div> </div>
{showFullAnswer && (
<div
onClick={() => setShowFullAnswer(false)}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.3)',
zIndex: 9999,}}>
<dialog
open
onClick={(e) => e.stopPropagation()}
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
border: 'none',
padding: '1rem',
background: '#fff',
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.3)',
minWidth: '300px',
minHeight: '200px',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
maxWidth: '600px',
wordWrap: 'break-word',
}}
>
<p style={{ margin: 0 }}>{selectedAnswer}</p>
<Button
variant="contained"
color="primary"
onClick={() => setShowFullAnswer(false)}
sx={{
position: 'absolute',
bottom: '1rem',
right: '1rem',
}}
>
Fermer
</Button>
</div>
</dialog>
</div>
)}
</div> </div>
); );
}; };

View file

@ -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<LiveResultsTableProps> = ({
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 (
<TableContainer component={Paper}>
<Table size="small">
<LiveResultsTableHeader
maxQuestions={maxQuestions}
showSelectedQuestion={showSelectedQuestion}
/>
<LiveResultsTableBody
maxQuestions={maxQuestions}
students={students}
showUsernames={showUsernames}
showCorrectAnswers={showCorrectAnswers}
getStudentGrade={getStudentGrade}
/>
<LiveResultsTableFooter
students={students}
maxQuestions={maxQuestions}
getStudentGrade={getStudentGrade}
/>
</Table>
</TableContainer>
);
};
export default LiveResultsTable;

View file

@ -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<LiveResultsFooterProps> = ({
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 (
<TableFooter>
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
<TableCell className="sticky-column" sx={{ color: 'black' }}>
<div className="text-base text-bold">% réussite</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0
? `${getCorrectAnswersPerQuestion(index).toFixed()} %`
: '-'}
</TableCell>
))}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
fontSize: '1rem',
color: 'rgba(0, 0, 0)'
}}
>
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
</TableCell>
</TableRow>
</TableFooter>
);
};
export default LiveResultsTableFooter;

View file

@ -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<LiveResultsFooterProps> = ({
maxQuestions,
students,
showUsernames,
showCorrectAnswers,
getStudentGrade
}) => {
return (
<TableBody>
{students.map((student) => (
<TableRow key={student.id}>
<TableCell
className="sticky-column"
sx={{
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base">
{showUsernames ? student.name : '******'}
</div>
</TableCell>
{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 (
<TableCell
key={index}
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
className={
answerText === ''
? ''
: isCorrect
? 'correct-answer'
: 'incorrect-answer'
}
>
{showCorrectAnswers ? (
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: '', text: answerText }) }}></div>
) : isCorrect ? (
<FontAwesomeIcon icon={faCheck} aria-label="correct" />
) : (
answerText !== '' && (
<FontAwesomeIcon icon={faCircleXmark} aria-label="incorrect"/>
)
)}
</TableCell>
);
})}
<TableCell
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold',
color: 'rgba(0, 0, 0)'
}}
>
{getStudentGrade(student).toFixed()} %
</TableCell>
</TableRow>
))}
</TableBody>
);
};
export default LiveResultsTableFooter;

View file

@ -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<LiveResultsFooterProps> = ({
maxQuestions,
showSelectedQuestion,
}) => {
return (
<TableHead>
<TableRow>
<TableCell className="sticky-column">
<div className="text-base text-bold">Nom d&apos;utilisateur</div>
</TableCell>
{Array.from({ length: maxQuestions }, (_, index) => (
<TableCell
key={index}
sx={{
textAlign: 'center',
cursor: 'pointer',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
onClick={() => showSelectedQuestion(index)}
>
<div className="text-base text-bold blue">{`Q${index + 1}`}</div>
</TableCell>
))}
<TableCell
className="sticky-header"
sx={{
textAlign: 'center',
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base text-bold">% réussite</div>
</TableCell>
</TableRow>
</TableHead>
);
};
export default LiveResultsTableFooter;

View file

@ -27,7 +27,6 @@
} }
.question-wrapper .katex { .question-wrapper .katex {
display: block;
text-align: center; text-align: center;
} }
@ -120,9 +119,9 @@
} }
.feedback-container { .feedback-container {
margin-left: 1.1rem; display: inline-block !important; /* override the parent */
display: inline-flex !important; /* override the parent */
align-items: center; align-items: center;
margin-left: 1.1rem;
position: relative; position: relative;
padding: 0 0.5rem; padding: 0 0.5rem;
background-color: hsl(43, 100%, 94%); background-color: hsl(43, 100%, 94%);

View file

@ -23,16 +23,33 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
}) => { }) => {
const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false);
const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false);
const [feedbackMessage, setFeedbackMessage] = useState(''); const [feedbackMessage, setFeedbackMessage] = useState<React.ReactNode>('');
const renderFeedbackMessage = (answer: string) => {
if(answer === 'true' || answer === 'false'){
return (<span>
<strong>Votre réponse est: </strong>{answer==="true" ? 'Vrai' : 'Faux'}
</span>)
}
else{
return (
<span>
<strong>Votre réponse est: </strong>{answer.toString()}
</span>
);}
};
useEffect(() => { useEffect(() => {
// Close the feedback dialog when the question changes
handleFeedbackDialogClose();
setIsAnswerSubmitted(false); setIsAnswerSubmitted(false);
}, [questionInfos]);
}, [questionInfos.question]);
const handleOnSubmitAnswer = (answer: string | number | boolean) => { const handleOnSubmitAnswer = (answer: string | number | boolean) => {
const idQuestion = Number(questionInfos.question.id) || -1; const idQuestion = Number(questionInfos.question.id) || -1;
submitAnswer(answer, idQuestion); submitAnswer(answer, idQuestion);
setFeedbackMessage(`Votre réponse est "${answer.toString()}".`); setFeedbackMessage(renderFeedbackMessage(answer.toString()));
setIsFeedbackDialogOpen(true); setIsFeedbackDialogOpen(true);
}; };
@ -74,7 +91,17 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
> >
<DialogTitle>Rétroaction</DialogTitle> <DialogTitle>Rétroaction</DialogTitle>
<DialogContent> <DialogContent>
<div style={{
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
maxHeight: '400px',
overflowY: 'auto',
}}>
{feedbackMessage} {feedbackMessage}
<div style={{ textAlign: 'left', fontWeight: 'bold', marginTop: '10px'}}
>Question : </div>
</div>
<QuestionComponent <QuestionComponent
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
question={questionInfos.question as Question} question={questionInfos.question as Question}

View file

@ -8,6 +8,7 @@ import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService'; // import { QuestionService } from '../../../services/QuestionService';
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService'; import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
import { QuizType } from '../../../Types/QuizType'; import { QuizType } from '../../../Types/QuizType';
import GroupIcon from '@mui/icons-material/Group';
import './manageRoom.css'; import './manageRoom.css';
import { ENV_VARIABLES } from 'src/constants'; import { ENV_VARIABLES } from 'src/constants';
@ -33,6 +34,7 @@ const ManageRoom: React.FC = () => {
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher'); const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
const [connectingError, setConnectingError] = useState<string>(''); const [connectingError, setConnectingError] = useState<string>('');
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined); const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
const [quizStarted, setQuizStarted] = useState(false);
useEffect(() => { useEffect(() => {
if (quizId.id) { if (quizId.id) {
@ -173,7 +175,7 @@ const ManageRoom: React.FC = () => {
updatedAnswers = [...student.answers, newAnswer]; updatedAnswers = [...student.answers, newAnswer];
} }
return { ...student, answers: updatedAnswers }; return { ...student, answers: updatedAnswers };
} }
return student; return student;
}); });
if (!foundStudent) { if (!foundStudent) {
@ -316,13 +318,18 @@ const ManageRoom: React.FC = () => {
if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) { if (!socket || !roomName || !quiz?.content || quiz?.content.length === 0) {
// TODO: This error happens when token expires! Need to handle it properly // TODO: This error happens when token expires! Need to handle it properly
console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`); console.log(`Error launching quiz. socket: ${socket}, roomName: ${roomName}, quiz: ${quiz}`);
setQuizStarted(true);
return; return;
} }
switch (quizMode) { switch (quizMode) {
case 'student': case 'student':
setQuizStarted(true);
return launchStudentMode(); return launchStudentMode();
case 'teacher': case 'teacher':
setQuizStarted(true);
return launchTeacherMode(); return launchTeacherMode();
} }
}; };
@ -427,9 +434,19 @@ const ManageRoom: React.FC = () => {
askConfirm askConfirm
message={`Êtes-vous sûr de vouloir quitter?`} /> message={`Êtes-vous sûr de vouloir quitter?`} />
<div className='centerTitle'>
<div className='title'>Salle: {roomName}</div>
<div className='userCount subtitle'>Utilisateurs: {students.length}/60</div>
<div className='headerContent' style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
<div style={{ flex: 1, display: 'flex', justifyContent: 'center' }}>
<div className='title'>Salle: {roomName}</div>
</div>
{quizStarted && (
<div className='userCount subtitle smallText' style={{ display: 'flex', alignItems: 'center' }}>
<GroupIcon style={{ marginRight: '5px' }} />
{students.length}/60
</div>
)}
</div> </div>
<div className='dumb'></div> <div className='dumb'></div>
@ -441,8 +458,12 @@ const ManageRoom: React.FC = () => {
{quizQuestions ? ( {quizQuestions ? (
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<div className="title center-h-align mb-2">{quiz?.title}</div> <div className="title center-h-align mb-2">{quiz?.title}</div>
{!isNaN(Number(currentQuestion?.question.id)) && (
<strong className='number of questions'>
Question {Number(currentQuestion?.question.id)}/{quizQuestions?.length}
</strong>
)}
{quizMode === 'teacher' && ( {quizMode === 'teacher' && (
@ -479,23 +500,23 @@ const ManageRoom: React.FC = () => {
</div> </div>
{quizMode === 'teacher' && ( {quizMode === 'teacher' && (
<div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}> <div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}>
<div className="previousQuestionButton"> <div className="previousQuestionButton">
<Button onClick={previousQuestion} <Button onClick={previousQuestion}
variant="contained" variant="contained"
disabled={Number(currentQuestion?.question.id) <= 1}> disabled={Number(currentQuestion?.question.id) <= 1}>
Question précédente Question précédente
</Button> </Button>
</div> </div>
<div className="nextQuestionButton"> <div className="nextQuestionButton">
<Button onClick={nextQuestion} <Button onClick={nextQuestion}
variant="contained" variant="contained"
disabled={Number(currentQuestion?.question.id) >=quizQuestions.length} disabled={Number(currentQuestion?.question.id) >= quizQuestions.length}
> >
Prochaine question Prochaine question
</Button> </Button>
</div> </div>
</div> )} </div>)}
</div> </div>

View file

@ -18,8 +18,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: flex-end;
align-items: center; align-items: flex-end;
} }