Compare commits

...

51 commits

Author SHA1 Message Date
Juba.M
00aaeeeb9a
Merge branch 'main' into JubaAzul/issue230 2025-02-13 11:16:40 -05:00
Juba.M
e50b3cee65
Merge pull request #246 from ets-cfuhrman-pfe/JubaAzul/issue245
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 20s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 19s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 19s
Tests / tests (client) (push) Failing after 1m0s
Tests / tests (server) (push) Failing after 51s
Ajuster les modèles de questions de l'éditeur et corriger une possible erreur
2025-02-13 11:04:25 -05:00
Christopher (Cris) Fuhrman
6e610ee284
Merge pull request #238 from ets-cfuhrman-pfe/JubaAzul/issue236
Refactoriser la classe LiveResult
2025-02-13 10:58:57 -05:00
C. Fuhrman
69fd13e327 Title réaliste 2025-02-13 08:50:53 -05:00
JubaAzul
9c9c17cd0f Refactoriser la LiveResultTable en fonction de ses composants
Fixes #236
2025-02-12 14:01:36 -05:00
JubaAzul
01e96f7b37 Ajuster les modèles de questions de l'éditeur et corriger une possible erreur
Fixes #245
2025-02-12 12:02:14 -05:00
Christopher (Cris) Fuhrman
9595556758
Merge pull request #244 from ets-cfuhrman-pfe/JubaAzul/issue242
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 20s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 18s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 17s
Tests / tests (client) (push) Failing after 53s
Tests / tests (server) (push) Failing after 1m7s
Erreur lors de la création d'un quiz en cours d'édition avec l'utilisation du \
2025-02-12 11:48:22 -05:00
JubaAzul
bd194a583e Erreur lors de la création d'un quiz en cours d'édition avec l'utilisation du \
Fixes #242
2025-02-12 09:24:41 -05:00
JubaAzul
a026f868d1 Erreur lors de la création d'un quiz en cours d'édition avec l'utilisation du \
Fixes #242
2025-02-11 19:07:15 -05:00
Christopher (Cris) Fuhrman
a5f2d5fe60
Merge pull request #243 from ets-cfuhrman-pfe/eddi3as-patch-1
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 53s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 49s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 17s
Tests / tests (client) (push) Failing after 1m26s
Tests / tests (server) (push) Failing after 50s
Update LICENSE
2025-02-11 09:00:25 -05:00
Edwin S Lopez
79c4965e80
Update LICENSE
ajout PFE-H25
2025-02-10 15:58:09 -05:00
Christopher (Cris) Fuhrman
0febb5a394
Merge pull request #239 from ets-cfuhrman-pfe/revert-217-PFEH25-merge-it2main
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 19s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 17s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 17s
Tests / tests (client) (push) Failing after 1m2s
Tests / tests (server) (push) Failing after 1m0s
Revert "PFEH2025 - merge entre main et dev-it2-PFEA2024 pour activation SSO"
2025-02-07 19:40:27 -05:00
Christopher (Cris) Fuhrman
a2650d89f1
Revert "PFEH2025 - merge entre main et dev-it2-PFEA2024 pour activation SSO" 2025-02-07 19:36:11 -05:00
Edwin S Lopez
687f5be45a
Merge pull request #217 from ets-cfuhrman-pfe/PFEH25-merge-it2main
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 19s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 18s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 18s
Tests / lint-and-tests (client) (push) Failing after 1m4s
Tests / lint-and-tests (server) (push) Failing after 59s
PFEH2025 - merge entre main et dev-it2-PFEA2024 pour activation SSO
2025-02-07 14:10:46 -05:00
JubaAzul
b66fbc09b2 Merge branch 'JubaAzul/issue236' of https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir into JubaAzul/issue236 2025-02-07 13:37:04 -05:00
JubaAzul
3aa51900d7 Création de classes de tests. 2025-02-07 13:33:38 -05:00
Juba.M
571fdb0d15
Merge branch 'main' into JubaAzul/issue236 2025-02-07 13:02:50 -05:00
JubaAzul
df617db28b Refactoriser la classe LiveResult
Fixes #236
2025-02-07 12:47:33 -05:00
Christopher (Cris) Fuhrman
06b62cc869
Merge pull request #235 from ets-cfuhrman-pfe/JubaAzul/issue233
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 18s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 17s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 18s
Tests / tests (client) (push) Failing after 57s
Tests / tests (server) (push) Failing after 1m6s
Faire l'appel de TextTypeTemplate au lieu de format() et sanitize()
2025-02-07 11:21:48 -05:00
Christopher (Cris) Fuhrman
7552cbae01
Merge pull request #234 from ets-cfuhrman-pfe/JubaAzul/issue222
[Rythme enseignant] Fenêtre de rétroaction reste ouverte pour les prochaines questions si l'étudiant ne l'enlève pas
2025-02-07 11:16:53 -05:00
Christopher (Cris) Fuhrman
80a31f5b38
Merge pull request #227 from ets-cfuhrman-pfe/JubaAzul/issue226
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 52s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 51s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 17s
Tests / tests (client) (push) Failing after 1m17s
Tests / tests (server) (push) Failing after 54s
[Katex] La L'affichage du tableau AVEC réponses pose problème lorsqu'il y a des équations
2025-02-07 11:08:23 -05:00
Christopher (Cris) Fuhrman
5ba7e0d573
Merge pull request #224 from ets-cfuhrman-pfe/JubaAzul/issue220
Problème dans l'affichage des rétroactions  Katek pour les questions réponses courtes et numériques
2025-02-07 11:05:20 -05:00
Christopher (Cris) Fuhrman
3cc0c8f20b
Merge pull request #223 from ets-cfuhrman-pfe/JubaAzul/issue221
Problème dans l'affichage des rétroactions  Katek pour les prévisualisations (Éditeur de quiz)
2025-02-07 11:01:00 -05:00
JubaAzul
c396b60734 Faire l'appel de TextTypeTemplate au lieu de format() et sanitize()
Fixes #233
2025-02-06 13:09:17 -05:00
JubaAzul
a539284813 [Katex] La L'affichage du tableau AVEC réponses pose problème lorsqu'il y a des équations
Fixes #226
2025-02-06 12:52:53 -05:00
JubaAzul
fc623582e0 [Rythme enseignant] Fenêtre de rétroaction reste ouverte pour les prochaines questions si l'étudiant ne l'enlève pas
Fixes #222
2025-02-05 19:45:55 -05:00
JubaAzul
705b0f1ddb [Katex] La L'affichage du tableau AVEC réponses pose problème lorsqu'il y a des équations
Fixes #226
2025-02-05 10:29:36 -05:00
Christopher (Cris) Fuhrman
4b7e93a199
Merge pull request #208 from ets-cfuhrman-pfe/JubaAzul/issue207
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 1m10s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 50s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 17s
Tests / tests (client) (push) Failing after 1m26s
Tests / tests (server) (push) Failing after 1m1s
Indiquer le numéro de la question dans l'affichage enseignant
2025-02-05 08:35:50 -05:00
JubaAzul
1aba1d6098 Indiquer le numéro de la question dans l'affichage enseignant
Fixes #207
2025-02-04 23:14:10 -05:00
JubaAzul
8edc77d217 [Katex] La L'affichage du tableau AVEC réponses pose problème lorsqu'il y a des équations
Fixes #226
2025-02-04 20:21:32 -05:00
Eddi3_As
a2de9a07ec add apperror to oidc 2025-02-04 16:26:30 -05:00
Eddi3_As
88561d5321 FIX import de apperror 2025-02-04 15:56:53 -05:00
Eddi3_As
78dd0542d3 fix error on tests 2025-02-03 19:00:25 -05:00
Eddi3_As
68f301b972 Merge remote-tracking branch 'test/main' into PFEH25-merge-it2main 2025-02-03 18:45:48 -05:00
Eddi3_As
3b6e009d73 FIX eslint et tests 2025-02-03 18:44:31 -05:00
Eddi3_As
d52078acbf skip user tests 2025-02-03 18:07:18 -05:00
JubaAzul
44f36a3a6b Problème dans l'affichage des rétroactions Katek pour les prévisualisations (Éditeur de quiz)
Fixes #221
2025-02-03 15:47:05 -05:00
JubaAzul
d7b33423e8 Problème dans l'affichage des rétroactions Katek pour les questions réponses courtes et numériques
Fixes #220
2025-02-03 13:53:40 -05:00
C. Fuhrman
10f02e576f clean up tests action 2025-02-02 15:31:16 -05:00
C. Fuhrman
c473212f2a Désactiver warning pour jest disableed tests 2025-02-02 12:59:47 -05:00
C. Fuhrman
f781173695 Afficher error (console) pour éviter warnings eslint 2025-02-02 12:54:25 -05:00
C. Fuhrman
2de7671666 Corriger des tests et auto-fix des problèmes de style 2025-02-02 12:42:05 -05:00
C. Fuhrman
23e053e24f normaliser config eslint
Même config pour extension VS Code et npx eslint
2025-02-02 12:40:12 -05:00
C. Fuhrman
896bd588bd ignorer dist pour eslint 2025-01-31 15:48:38 -05:00
C. Fuhrman
e88dafa01c Workspace in VSCode (c'est pratique pour ouvrir un mono repo) 2025-01-31 15:23:20 -05:00
C. Fuhrman
29d9ed31ac npm install (client et server) pour que l'action roule sur GitHub 2025-01-31 15:22:37 -05:00
Eddi3_As
6b4364c7c7 PFEH2025 - merge entre main et dev-it2-PFEA2024 pour activation SSO 2025-01-29 21:45:41 -05:00
JubaAzul
3e1a88377a Merge branch 'main' into JubaAzul/issue207 2025-01-29 13:10:15 -05:00
JubaAzul
98f2b917ac Merge branch 'main' into JubaAzul/issue207 2025-01-28 16:03:13 -05:00
JubaAzul
b49057a8ff Only show the current question for teacher mode 2025-01-23 20:23:15 -05:00
JubaAzul
226cc24532 Indiquer le numéro de la question dans l'affichage enseignant
Fixes #207
2025-01-23 20:12:47 -05:00
22 changed files with 1207 additions and 240 deletions

View file

@ -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

View file

@ -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",

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

@ -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 (
<div className="gift-cheat-sheet">
<h2 className="subtitle">Informations pratiques sur l&apos;éditeur</h2>
@ -79,7 +79,7 @@ const GiftCheatSheet: React.FC = () => {
</div>
<div className="question-type">
<h4> 5. Question numérique </h4>
<h4> 5. Questions numériques </h4>
<pre>
<code className="question-code-block selectable-text">
{

View file

@ -74,7 +74,8 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
<div className="error">{error}</div>
) : isPreviewReady ? (
<div data-testid="preview-container">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: '', text: items }) }}></div>
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: 'html', text: items }) }}></div>
</div>
) : (
<div className="loading">Chargement de la prévisualisation...</div>

View file

@ -5,13 +5,23 @@ import { TextFormat } from 'gift-pegjs';
import DOMPurify from 'dompurify'; // cleans HTML to prevent XSS attacks, etc.
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: 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;
}
/**

View file

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

View file

@ -1,26 +1,16 @@
// LiveResults.tsx
import React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import { Socket } from 'socket.io-client';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons';
import { QuestionType } from '../../Types/QuestionType';
import './liveResult.css';
import {
FormControlLabel,
FormGroup,
Paper,
Switch,
Table,
TableBody,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableRow
} from '@mui/material';
import { StudentType } from '../../Types/StudentType';
import { FormattedTextTemplate } from '../GiftTemplate/templates/TextTypeTemplate';
import LiveResultsTable from './LiveResultsTable/LiveResultsTable';
interface LiveResultsProps {
socket: Socket | null;
@ -35,52 +25,6 @@ const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuesti
const [showUsernames, setShowUsernames] = useState<boolean>(false);
const [showCorrectAnswers, setShowCorrectAnswers] = useState<boolean>(false);
const maxQuestions = questions.length;
const getStudentGrade = (student: StudentType): number => {
if (student.answers.length === 0) {
return 0;
}
const uniqueQuestions = new Set();
let correctAnswers = 0;
for (const answer of student.answers) {
const { idQuestion, isCorrect } = answer;
if (!uniqueQuestions.has(idQuestion)) {
uniqueQuestions.add(idQuestion);
if (isCorrect) {
correctAnswers++;
}
}
}
return (correctAnswers / questions.length) * 100;
};
const classAverage: number = useMemo(() => {
let classTotal = 0;
students.forEach((student) => {
classTotal += getStudentGrade(student);
});
return classTotal / students.length;
}, [students]);
const getCorrectAnswersPerQuestion = (index: number): number => {
return (
(students.filter((student) =>
student.answers.some(
(answer) =>
parseInt(answer.idQuestion.toString()) === index + 1 && answer.isCorrect
)
).length / students.length) * 100
);
};
return (
<div>
<div className="action-bar mb-1">
@ -112,146 +56,14 @@ const LiveResults: React.FC<LiveResultsProps> = ({ questions, showSelectedQuesti
</div>
<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}
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>
<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} />
) : (
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>
<LiveResultsTable
students={students}
questions={questions}
showCorrectAnswers={showCorrectAnswers}
showSelectedQuestion={showSelectedQuestion}
showUsernames={showUsernames}
/>
</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 {
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%);

View file

@ -40,8 +40,11 @@ const TeacherModeQuiz: React.FC<TeacherModeQuizProps> = ({
);}
};
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;

View file

@ -8,6 +8,7 @@ import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
// import { QuestionService } from '../../../services/QuestionService';
import webSocketService, { AnswerReceptionFromBackendType } from '../../../services/WebsocketService';
import { QuizType } from '../../../Types/QuizType';
import GroupIcon from '@mui/icons-material/Group';
import './manageRoom.css';
import { ENV_VARIABLES } from 'src/constants';
@ -33,7 +34,8 @@ const ManageRoom: React.FC = () => {
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
const [connectingError, setConnectingError] = useState<string>('');
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(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?`} />
<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 className='dumb'></div>
@ -441,8 +458,12 @@ const ManageRoom: React.FC = () => {
{quizQuestions ? (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<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' && (
@ -479,23 +500,23 @@ const ManageRoom: React.FC = () => {
</div>
{quizMode === 'teacher' && (
<div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}>
<div className="previousQuestionButton">
<Button onClick={previousQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) <= 1}>
Question précédente
</Button>
</div>
<div className="nextQuestionButton">
<Button onClick={nextQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) >=quizQuestions.length}
>
Prochaine question
</Button>
</div>
</div> )}
<div className="questionNavigationButtons" style={{ display: 'flex', justifyContent: 'center' }}>
<div className="previousQuestionButton">
<Button onClick={previousQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) <= 1}>
Question précédente
</Button>
</div>
<div className="nextQuestionButton">
<Button onClick={nextQuestion}
variant="contained"
disabled={Number(currentQuestion?.question.id) >= quizQuestions.length}
>
Prochaine question
</Button>
</div>
</div>)}
</div>

View file

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