Merge branch 'main' into JubaAzul/issue230

This commit is contained in:
Juba.M 2025-02-13 11:16:40 -05:00 committed by GitHub
commit 00aaeeeb9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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,6 +34,7 @@ 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) {
@ -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;
}