Compare commits

...

13 commits

Author SHA1 Message Date
Edwin S Lopez
f8ee575b85
Merge 755d14a5b7 into ee7a7a0544 2025-03-22 03:29:26 +00:00
Eddi3_As
755d14a5b7 ajout dernier tests 2025-03-21 23:29:20 -04:00
Christopher (Cris) Fuhrman
ee7a7a0544
Update README.md
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 19s
Tests / lint-and-tests (client) (push) Failing after 1m10s
Tests / lint-and-tests (server) (push) Failing after 57s
fix link
2025-03-21 13:15:20 -04:00
Christopher (Cris) Fuhrman
8c57a8759f
Merge pull request #302 from ets-cfuhrman-pfe/Multilingual-readme
Update readme (bilingual)
2025-03-21 13:11:55 -04:00
C. Fuhrman
22a2754e31 Update readme (bilingual) 2025-03-21 11:58:44 -04:00
C. Fuhrman
3d9015febd Fix bug with multiple choice (radio vs checkbox)
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 / lint-and-tests (client) (push) Failing after 57s
Tests / lint-and-tests (server) (push) Failing after 1m2s
Snapshot update
2025-03-21 11:25:06 -04:00
C. Fuhrman
fc15d2c3bd refactor
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 / lint-and-tests (client) (push) Failing after 1m3s
Tests / lint-and-tests (server) (push) Failing after 1m1s
2025-03-21 11:08:21 -04:00
C. Fuhrman
13136b9e91 fix bugs that showed in dev 2025-03-21 11:05:16 -04:00
C. Fuhrman
b92f81cc0e tests passing
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 1m5s
Tests / lint-and-tests (server) (push) Failing after 59s
2025-03-21 09:31:36 -04:00
C. Fuhrman
42e3041830 first cut, with tests
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 1m1s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 59s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 18s
Tests / lint-and-tests (client) (push) Failing after 1m27s
Tests / lint-and-tests (server) (push) Failing after 1m3s
2025-03-21 00:25:25 -04:00
Christopher (Cris) Fuhrman
112062c0b2
Merge pull request #284 from ets-cfuhrman-pfe/fuhrmanator/issue283
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 0s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 0s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 0s
Tests / lint-and-tests (client) (push) Failing after 0s
Tests / lint-and-tests (server) (push) Failing after 0s
[BUG] étudiant qui se joint à une salle après le démarrage du quiz es…
2025-03-11 02:52:58 -04:00
C. Fuhrman
29de2a7671 Correction de bogue trouvé par test! 2025-03-09 01:19:31 -05:00
C. Fuhrman
fe67f020eb [BUG] étudiant qui se joint à une salle après le démarrage du quiz est bloqué
Fixes #283
Valeurs de l'état de la page (quizStarted) n'ont pas leur valeur actuelle dans un on(). Alors, on déplace la logique du traitement du nouvel étudiant dans un useEffect et on provoque le useEffect dans le on()
2025-03-09 00:54:21 -05:00
29 changed files with 1078 additions and 329 deletions

30
README.fr-ca.md Normal file
View file

@ -0,0 +1,30 @@
[![CI/CD Pipeline for Frontend](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/frontend-deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/frontend-deploy.yml)
[![CI/CD Pipeline for Backend](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml)
[![CI/CD Pipeline for Nginx Router](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml)
[![en](https://img.shields.io/badge/lang-en-red.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/blob/master/README.md)
# EvalueTonSavoir
EvalueTonSavoir est une plateforme open source et auto-hébergée qui poursuit le développement du code provenant de https://github.com/ETS-PFE004-Plateforme-sondage-minitest. Cette plateforme minimaliste est conçue comme un outil d'apprentissage et d'enseignement, offrant une solution simple et efficace pour la création de quiz utilisant le format GIFT, similaire à Moodle.
## Fonctionnalités clés
* Open Source et Auto-hébergé : Possédez et contrôlez vos données en déployant la plateforme sur votre propre infrastructure.
* Compatibilité GIFT : Créez des quiz facilement en utilisant le format GIFT, permettant une intégration transparente avec d'autres systèmes d'apprentissage.
* Minimaliste et Efficace : Une approche bare bones pour garantir la simplicité et la facilité d'utilisation, mettant l'accent sur l'essentiel de l'apprentissage.
## Contribution
Actuellement, il n'y a pas de modèle établi pour les contributions. Si vous constatez quelque chose de manquant ou si vous pensez qu'une amélioration est possible, n'hésitez pas à ouvrir un issue et/ou une PR)
## Liens utiles
* [Dépôt d'origine Frontend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Frontend)
* [Dépôt d'origine Backend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Backend)
* [Documentation (Wiki)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki)
## License
EvalueTonSavoir is open-sourced and licensed under the [MIT License](/LICENSE).

View file

@ -2,24 +2,26 @@
[![CI/CD Pipeline for Backend](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml) [![CI/CD Pipeline for Backend](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml)
[![CI/CD Pipeline for Nginx Router](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml) [![CI/CD Pipeline for Nginx Router](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml/badge.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml)
# EvalueTonSavoir [![fr-ca](https://img.shields.io/badge/lang-fr--ca-green.svg)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/blob/main/README.fr-ca.md)
EvalueTonSavoir est une plateforme open source et auto-hébergée qui poursuit le développement du code provenant de https://github.com/ETS-PFE004-Plateforme-sondage-minitest. Cette plateforme minimaliste est conçue comme un outil d'apprentissage et d'enseignement, offrant une solution simple et efficace pour la création de quiz utilisant le format GIFT, similaire à Moodle. # EvalueTonSavoir
## Fonctionnalités clés EvalueTonSavoir is an open-source and self-hosted platform that continues the development of the code from https://github.com/ETS-PFE004-Plateforme-sondage-minitest. This minimalist platform is designed as a learning and teaching tool, offering a simple and effective solution for creating quizzes using the GIFT format, similar to Moodle.
* Open Source et Auto-hébergé : Possédez et contrôlez vos données en déployant la plateforme sur votre propre infrastructure. ## Key Features
* Compatibilité GIFT : Créez des quiz facilement en utilisant le format GIFT, permettant une intégration transparente avec d'autres systèmes d'apprentissage.
* Minimaliste et Efficace : Une approche bare bones pour garantir la simplicité et la facilité d'utilisation, mettant l'accent sur l'essentiel de l'apprentissage. * **Open Source and Self-Hosted**: Own and control your data by deploying the platform on your own infrastructure.
* **GIFT Compatibility**: Easily create quizzes using the GIFT format, enabling seamless integration with other learning systems.
* **Minimalist and Efficient**: A bare-bones approach to ensure simplicity and ease of use, focusing on the essentials of learning.
## Contribution ## Contribution
Actuellement, il n'y a pas de modèle établi pour les contributions. Si vous constatez quelque chose de manquant ou si vous pensez qu'une amélioration est possible, n'hésitez pas à ouvrir un issue et/ou une PR) Currently, there is no established model for contributions. If you notice something missing or think an improvement is possible, feel free to open an issue and/or a PR.
## Liens utiles ## Useful Links
* [Dépôt d'origine Frontend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Frontend) * [Original Frontend Repository](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Frontend)
* [Dépôt d'origine Backend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Backend) * [Original Backend Repository](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Backend)
* [Documentation (Wiki)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki) * [Documentation (Wiki)](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/wiki)
## License ## License

View file

@ -19,8 +19,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
}); });
const mockStudents: StudentType[] = [ const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, { 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 }] }, { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
]; ];
const mockShowSelectedQuestion = jest.fn(); const mockShowSelectedQuestion = jest.fn();
@ -92,4 +92,4 @@ describe('LiveResults', () => {
expect(mockShowSelectedQuestion).toHaveBeenCalled(); expect(mockShowSelectedQuestion).toHaveBeenCalled();
}); });
}); });

View file

@ -20,8 +20,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
const mockStudents: StudentType[] = [ const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, { 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 }] }, { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
]; ];
const mockShowSelectedQuestion = jest.fn(); const mockShowSelectedQuestion = jest.fn();

View file

@ -20,8 +20,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
}); });
const mockStudents: StudentType[] = [ const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, { 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 }] }, { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
]; ];
const mockGetStudentGrade = jest.fn((student: StudentType) => { const mockGetStudentGrade = jest.fn((student: StudentType) => {

View file

@ -6,8 +6,8 @@ import LiveResultsTableFooter from 'src/components/LiveResults/LiveResultsTable/
const mockStudents: StudentType[] = [ const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, { 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 }] }, { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
]; ];
const mockGetStudentGrade = jest.fn((student: StudentType) => { const mockGetStudentGrade = jest.fn((student: StudentType) => {
@ -52,4 +52,4 @@ describe('LiveResultsTableFooter', () => {
expect(screen.getByText('50 %')).toBeInTheDocument(); expect(screen.getByText('50 %')).toBeInTheDocument();
}); });
}); });

View file

@ -29,7 +29,7 @@ const katekMock: TemplateOptions & MultipleChoiceQuestion = {
formattedStem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'}, formattedStem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'},
choices: [ choices: [
{ formattedText: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 }, { formattedText: { format: 'plain' , text: 'Choice 1'}, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 },
{ formattedText: { format: 'plain', text: 'Choice 2' }, isCorrect: true, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 1 } { formattedText: { format: 'plain', text: 'Choice 2' }, isCorrect: false, formattedFeedback: { format: 'plain' , text: 'Correct!'}, weight: 0 }
], ],
formattedGlobalFeedback: { format: 'plain', text: 'Sample Global Feedback' } formattedGlobalFeedback: { format: 'plain', text: 'Sample Global Feedback' }
}; };

View file

@ -733,7 +733,7 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
<div class='multiple-choice-answers-container'> <div class='multiple-choice-answers-container'>
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"> <input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
<span class="answer-weight-container answer-positive-weight">1%</span>
<label style=" <label style="
display: inline-block; display: inline-block;
padding: 0.2em 0 0.2em 0; padding: 0.2em 0 0.2em 0;
@ -742,15 +742,15 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
" for="idmocked-id"> " for="idmocked-id">
Choice 2 Choice 2
</label> </label>
<svg data-testid="correct-icon" style=" <svg data-testid="incorrect-icon" style="
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline-block; display: inline-block;
margin-left: 0.1rem; margin-left: 0.1rem;
margin-right: 0.2rem; margin-right: 0.2rem;
width: 1em; width: 0.75em;
color: hsl(120, 39%, 54%); color: hsl(2, 64%, 58%);
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg> " role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>
<span class="feedback-container">Correct!</span> <span class="feedback-container">Correct!</span>
</input> </input>
</div> </div>

View file

@ -1,9 +1,10 @@
import React, { act } from "react"; import React, { act } from "react";
import "@testing-library/jest-dom";
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import ImageGallery from "../../../components/ImageGallery/ImageGallery"; import ImageGallery from "../../../components/ImageGallery/ImageGallery";
import ApiService from "../../../services/ApiService"; import ApiService from "../../../services/ApiService";
import { Images } from "../../../Types/Images"; import { Images } from "../../../Types/Images";
import "@testing-library/jest-dom"; import userEvent from "@testing-library/user-event";
jest.mock("../../../services/ApiService"); jest.mock("../../../services/ApiService");
@ -14,6 +15,7 @@ const mockImages: Images[] = [
]; ];
beforeAll(() => { beforeAll(() => {
global.URL.createObjectURL = jest.fn(() => 'mockedObjectUrl');
Object.assign(navigator, { Object.assign(navigator, {
clipboard: { clipboard: {
writeText: jest.fn(), writeText: jest.fn(),
@ -22,15 +24,16 @@ beforeAll(() => {
}); });
describe("ImageGallery", () => { describe("ImageGallery", () => {
let mockHandleDelete: jest.Mock; let mockHandleDelete: jest.Mock;
beforeEach(() => {
beforeEach(async () => {
(ApiService.getUserImages as jest.Mock).mockResolvedValue({ images: mockImages, total: 3 }); (ApiService.getUserImages as jest.Mock).mockResolvedValue({ images: mockImages, total: 3 });
(ApiService.deleteImage as jest.Mock).mockResolvedValue(true); (ApiService.deleteImage as jest.Mock).mockResolvedValue(true);
(ApiService.uploadImage as jest.Mock).mockResolvedValue('mockImageUrl'); (ApiService.uploadImage as jest.Mock).mockResolvedValue('mockImageUrl');
await act(async () => {
render(<ImageGallery />);
});
mockHandleDelete = jest.fn(); mockHandleDelete = jest.fn();
render(<ImageGallery />);
}); });
it("should render images correctly", async () => { it("should render images correctly", async () => {
@ -44,8 +47,9 @@ describe("ImageGallery", () => {
it("should handle copy action", async () => { it("should handle copy action", async () => {
const handleCopyMock = jest.fn(); const handleCopyMock = jest.fn();
await act(async () => {
render(<ImageGallery handleCopy={handleCopyMock} />); render(<ImageGallery handleCopy={handleCopyMock} />);
});
const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/)); const copyButtons = await waitFor(() => screen.findAllByTestId(/gallery-tab-copy-/));
await act(async () => { await act(async () => {
@ -60,7 +64,9 @@ describe("ImageGallery", () => {
(ApiService.getUserImages as jest.Mock).mockImplementation(fetchImagesMock); (ApiService.getUserImages as jest.Mock).mockImplementation(fetchImagesMock);
render(<ImageGallery handleDelete={mockHandleDelete} />); await act(async () => {
render(<ImageGallery handleDelete={mockHandleDelete} />);
});
await act(async () => { await act(async () => {
await screen.findByAltText("Image image1.jpg"); await screen.findByAltText("Image image1.jpg");
@ -88,4 +94,54 @@ describe("ImageGallery", () => {
}); });
}); });
it("should upload an image and display success message", async () => {
const importTab = screen.getByRole("tab", { name: /import/i });
fireEvent.click(importTab);
const fileInputs = await screen.findAllByTestId("file-input");
const fileInput = fileInputs[1];
expect(fileInput).toBeInTheDocument();
const file = new File(["image"], "image.jpg", { type: "image/jpeg" });
await userEvent.upload(fileInput, file);
await waitFor(() => screen.getByAltText("Preview"));
const previewImage = screen.getByAltText("Preview");
expect(previewImage).toBeInTheDocument();
const uploadButton = screen.getByRole('button', { name: /téléverser/i });
fireEvent.click(uploadButton);
const successMessage = await screen.findByText(/téléversée avec succès/i);
expect(successMessage).toBeInTheDocument();
});
it("should close the image preview dialog when close button is clicked", async () => {
const imageCard = screen.getByAltText("Image image1.jpg");
fireEvent.click(imageCard);
const dialogImage = await screen.findByAltText("Enlarged view");
expect(dialogImage).toBeInTheDocument();
const closeButton = screen.getByTestId("close-button");
fireEvent.click(closeButton);
await waitFor(() => {
expect(screen.queryByAltText("Enlarged view")).not.toBeInTheDocument();
});
});
it("should show an error message when no file is selected", async () => {
const importTab = screen.getByRole("tab", { name: /import/i });
fireEvent.click(importTab);
const uploadButton = screen.getByRole('button', { name: /téléverser/i });
fireEvent.click(uploadButton);
await waitFor(() => {
expect(screen.getByText("Veuillez choisir une image à téléverser.")).toBeInTheDocument();
});
});
}); });

View file

@ -5,7 +5,7 @@ import LiveResults from 'src/components/LiveResults/LiveResults';
import { QuestionType } from 'src/Types/QuestionType'; import { QuestionType } from 'src/Types/QuestionType';
import { StudentType } from 'src/Types/StudentType'; import { StudentType } from 'src/Types/StudentType';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { BaseQuestion,parse } from 'gift-pegjs'; import { BaseQuestion, parse } from 'gift-pegjs';
const mockSocket: Socket = { const mockSocket: Socket = {
on: jest.fn(), on: jest.fn(),
@ -19,19 +19,28 @@ const mockGiftQuestions = parse(
`::Sample Question 1:: Question stem `::Sample Question 1:: Question stem
{ {
=Choice 1 =Choice 1
~Choice 2 =Choice 2
}`); ~Choice 3
~Choice 4
}
::Sample Question 2:: Question stem {TRUE}
`);
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => { const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
if (question.type !== "Category") if (question.type !== "Category")
question.id = (index + 1).toString(); question.id = (index + 1).toString();
const newMockQuestion = question; const newMockQuestion = question;
return {question : newMockQuestion as BaseQuestion}; return { question: newMockQuestion as BaseQuestion };
}); });
console.log(`mockQuestions: ${JSON.stringify(mockQuestions)}`);
// each student should have a different score for the tests to pass
const mockStudents: StudentType[] = [ const mockStudents: StudentType[] = [
{ id: '1', name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Choice 1', isCorrect: true }] }, { id: '1', name: 'Student 1', answers: [] },
{ id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: 'Choice 2', isCorrect: false }] }, { id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: ['Choice 3'], isCorrect: false }, { idQuestion: 2, answer: [true], isCorrect: true}] },
{ id: '3', name: 'Student 3', answers: [{ idQuestion: 1, answer: ['Choice 1', 'Choice 2'], isCorrect: true }, { idQuestion: 2, answer: [true], isCorrect: true}] },
]; ];
describe('LiveResults', () => { describe('LiveResults', () => {
@ -52,7 +61,7 @@ describe('LiveResults', () => {
// Toggle the display of usernames back // Toggle the display of usernames back
fireEvent.click(toggleUsernamesSwitch); fireEvent.click(toggleUsernamesSwitch);
// Check if the component renders the students // Check if the component renders the students
mockStudents.forEach((student) => { mockStudents.forEach((student) => {
expect(screen.getByText(student.name)).toBeInTheDocument(); expect(screen.getByText(student.name)).toBeInTheDocument();
@ -82,82 +91,88 @@ describe('LiveResults', () => {
}); });
}); });
}); test('calculates and displays the correct student grades', () => {
test('calculates and displays the correct student grades', () => { render(
render( <LiveResults
<LiveResults socket={mockSocket}
socket={mockSocket} questions={mockQuestions}
questions={mockQuestions} showSelectedQuestion={jest.fn()}
showSelectedQuestion={jest.fn()} quizMode="teacher"
quizMode="teacher" students={mockStudents}
students={mockStudents} />
/> );
);
// Toggle the display of usernames // Toggle the display of usernames
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
// Toggle the display of usernames back // Toggle the display of usernames back
fireEvent.click(toggleUsernamesSwitch); fireEvent.click(toggleUsernamesSwitch);
// Check if the student grades are calculated and displayed correctly // Check if the student grades are calculated and displayed correctly
mockStudents.forEach((student) => { const getByTextInTableCellBody = (text: string) => {
const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100; const elements = screen.getAllByText(text); // Get all elements with the specified text
expect(screen.getByText(`${grade.toFixed()} %`)).toBeInTheDocument(); return elements.find((element) => element.closest('.MuiTableCell-body')); // don't get the footer element(s)
};
mockStudents.forEach((student) => {
const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100;
const element = getByTextInTableCellBody(`${grade.toFixed()} %`);
expect(element).toBeInTheDocument();
});
}); });
});
test('calculates and displays the class average', () => { test('calculates and displays the class average', () => {
render( render(
<LiveResults <LiveResults
socket={mockSocket} socket={mockSocket}
questions={mockQuestions} questions={mockQuestions}
showSelectedQuestion={jest.fn()} showSelectedQuestion={jest.fn()}
quizMode="teacher" quizMode="teacher"
students={mockStudents} students={mockStudents}
/> />
); );
// Toggle the display of usernames // Toggle the display of usernames
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
// Toggle the display of usernames back // Toggle the display of usernames back
fireEvent.click(toggleUsernamesSwitch); 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 // Calculate the class average
const classAverageElements = screen.getAllByText(`${classAverage.toFixed()} %`); const totalGrades = mockStudents.reduce((total, student) => {
const classAverageElement = classAverageElements.find((element) => { return total + (student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100);
return element.closest('td')?.classList.contains('MuiTableCell-footer'); }, 0);
}); const classAverage = totalGrades / mockStudents.length;
expect(classAverageElement).toBeInTheDocument();
});
test('displays the correct answers per question', () => { // Check if the class average is displayed correctly
render( const classAverageElements = screen.getAllByText(`${classAverage.toFixed()} %`);
<LiveResults const classAverageElement = classAverageElements.find((element) => {
socket={mockSocket} return element.closest('td')?.classList.contains('MuiTableCell-footer');
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(); 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

@ -12,14 +12,23 @@ const questions = parse(
{ {
=Choice 1 =Choice 1
~Choice 2 ~Choice 2
}`) as MultipleChoiceQuestion[]; }
::Sample Question 2:: Question stem
{
=Choice 1
=Choice 2
~Choice 3
}
`) as MultipleChoiceQuestion[];
const question = questions[0]; const questionWithOneCorrectChoice = questions[0];
const questionWithMultipleCorrectChoices = questions[1];
describe('MultipleChoiceQuestionDisplay', () => { describe('MultipleChoiceQuestionDisplay', () => {
const mockHandleOnSubmitAnswer = jest.fn(); const mockHandleOnSubmitAnswer = jest.fn();
const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => { const TestWrapper = ({ showAnswer, question }: { showAnswer: boolean; question: MultipleChoiceQuestion }) => {
const [showAnswerState, setShowAnswerState] = useState(showAnswer); const [showAnswerState, setShowAnswerState] = useState(showAnswer);
const handleOnSubmitAnswer = (answer: AnswerType) => { const handleOnSubmitAnswer = (answer: AnswerType) => {
@ -38,28 +47,51 @@ describe('MultipleChoiceQuestionDisplay', () => {
); );
}; };
const choices = question.choices; const twoChoices = questionWithOneCorrectChoice.choices;
const threeChoices = questionWithMultipleCorrectChoices.choices;
beforeEach(() => { test('renders a question (that has only one correct choice) and its choices', () => {
render(<TestWrapper showAnswer={false} />); render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
});
test('renders the question and choices', () => { expect(screen.getByText(questionWithOneCorrectChoice.formattedStem.text)).toBeInTheDocument();
expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument(); twoChoices.forEach((choice) => {
choices.forEach((choice) => {
expect(screen.getByText(choice.formattedText.text)).toBeInTheDocument(); expect(screen.getByText(choice.formattedText.text)).toBeInTheDocument();
}); });
}); });
test('only allows one choice to be selected when question only has one correct answer', () => {
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
const choiceButton1 = screen.getByText('Choice 1').closest('button');
const choiceButton2 = screen.getByText('Choice 2').closest('button');
if (!choiceButton1 || !choiceButton2) throw new Error('Choice buttons not found');
// Simulate selecting multiple answers
act(() => {
fireEvent.click(choiceButton1);
});
act(() => {
fireEvent.click(choiceButton2);
});
// Verify that only the last answer is selected
expect(choiceButton1.querySelector('.answer-text.selected')).not.toBeInTheDocument();
expect(choiceButton2.querySelector('.answer-text.selected')).toBeInTheDocument();
});
test('does not submit when no answer is selected', () => { test('does not submit when no answer is selected', () => {
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
act(() => { act(() => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
}); });
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled(); expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
mockHandleOnSubmitAnswer.mockClear();
}); });
test('submits the selected answer', () => { test('submits the selected answer', () => {
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
const choiceButton = screen.getByText('Choice 1').closest('button'); const choiceButton = screen.getByText('Choice 1').closest('button');
if (!choiceButton) throw new Error('Choice button not found'); if (!choiceButton) throw new Error('Choice button not found');
act(() => { act(() => {
@ -70,10 +102,68 @@ describe('MultipleChoiceQuestionDisplay', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
}); });
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith('Choice 1'); expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(['Choice 1']);
mockHandleOnSubmitAnswer.mockClear();
});
test('renders a question (that has multiple correct choices) and its choices', () => {
render(<TestWrapper showAnswer={false} question={questionWithMultipleCorrectChoices} />);
expect(screen.getByText(questionWithMultipleCorrectChoices.formattedStem.text)).toBeInTheDocument();
threeChoices.forEach((choice) => {
expect(screen.getByText(choice.formattedText.text)).toBeInTheDocument();
});
});
test('allows multiple choices to be selected when question has multiple correct answers', () => {
render(<TestWrapper showAnswer={false} question={questionWithMultipleCorrectChoices} />);
const choiceButton1 = screen.getByText('Choice 1').closest('button');
const choiceButton2 = screen.getByText('Choice 2').closest('button');
const choiceButton3 = screen.getByText('Choice 3').closest('button');
if (!choiceButton1 || !choiceButton2 || !choiceButton3) throw new Error('Choice buttons not found');
act(() => {
fireEvent.click(choiceButton1);
});
act(() => {
fireEvent.click(choiceButton2);
});
expect(choiceButton1.querySelector('.answer-text.selected')).toBeInTheDocument();
expect(choiceButton2.querySelector('.answer-text.selected')).toBeInTheDocument();
expect(choiceButton3.querySelector('.answer-text.selected')).not.toBeInTheDocument(); // didn't click
});
test('submits multiple selected answers', () => {
render(<TestWrapper showAnswer={false} question={questionWithMultipleCorrectChoices} />);
const choiceButton1 = screen.getByText('Choice 1').closest('button');
const choiceButton2 = screen.getByText('Choice 2').closest('button');
if (!choiceButton1 || !choiceButton2) throw new Error('Choice buttons not found');
// Simulate selecting multiple answers
act(() => {
fireEvent.click(choiceButton1);
});
act(() => {
fireEvent.click(choiceButton2);
});
// Simulate submitting the answers
const submitButton = screen.getByText('Répondre');
act(() => {
fireEvent.click(submitButton);
});
// Verify that the mockHandleOnSubmitAnswer function is called with both answers
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(['Choice 1', 'Choice 2']);
mockHandleOnSubmitAnswer.mockClear();
}); });
it('should show ✅ next to the correct answer and ❌ next to the wrong answers when showAnswer is true', async () => { it('should show ✅ next to the correct answer and ❌ next to the wrong answers when showAnswer is true', async () => {
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
const choiceButton = screen.getByText('Choice 1').closest('button'); const choiceButton = screen.getByText('Choice 1').closest('button');
if (!choiceButton) throw new Error('Choice button not found'); if (!choiceButton) throw new Error('Choice button not found');
@ -89,16 +179,17 @@ describe('MultipleChoiceQuestionDisplay', () => {
}); });
// Wait for the DOM to update // Wait for the DOM to update
const correctAnswer = screen.getByText("Choice 1").closest('button'); const correctAnswer = screen.getByText("Choice 1").closest('button');
expect(correctAnswer).toBeInTheDocument(); expect(correctAnswer).toBeInTheDocument();
expect(correctAnswer?.textContent).toContain('✅'); expect(correctAnswer?.textContent).toContain('✅');
const wrongAnswer1 = screen.getByText("Choice 2").closest('button'); const wrongAnswer1 = screen.getByText("Choice 2").closest('button');
expect(wrongAnswer1).toBeInTheDocument(); expect(wrongAnswer1).toBeInTheDocument();
expect(wrongAnswer1?.textContent).toContain('❌'); expect(wrongAnswer1?.textContent).toContain('❌');
}); });
it('should not show ✅ or ❌ when repondre button is not clicked', async () => { it('should not show ✅ or ❌ when Répondre button is not clicked', async () => {
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
const choiceButton = screen.getByText('Choice 1').closest('button'); const choiceButton = screen.getByText('Choice 1').closest('button');
if (!choiceButton) throw new Error('Choice button not found'); if (!choiceButton) throw new Error('Choice button not found');
@ -118,5 +209,5 @@ describe('MultipleChoiceQuestionDisplay', () => {
expect(wrongAnswer1?.textContent).not.toContain('❌'); expect(wrongAnswer1?.textContent).not.toContain('❌');
}); });
}); });

View file

@ -67,6 +67,7 @@ describe('NumericalQuestion Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled(); expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
mockHandleOnSubmitAnswer.mockClear();
}); });
it('submits answer correctly', () => { it('submits answer correctly', () => {
@ -77,6 +78,7 @@ describe('NumericalQuestion Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(7); expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith([7]);
mockHandleOnSubmitAnswer.mockClear();
}); });
}); });

View file

@ -29,23 +29,24 @@ describe('Questions Component', () => {
render(<QuestionDisplay question={question} {...sampleProps} />); render(<QuestionDisplay question={question} {...sampleProps} />);
}; };
describe('question type parsing', () => { // describe('question type parsing', () => {
it('parses true/false question type correctly', () => { // it('parses true/false question type correctly', () => {
expect(sampleTrueFalseQuestion.type).toBe('TF'); // expect(sampleTrueFalseQuestion.type).toBe('TF');
}); // });
it('parses multiple choice question type correctly', () => { // it('parses multiple choice question type correctly', () => {
expect(sampleMultipleChoiceQuestion.type).toBe('MC'); // expect(sampleMultipleChoiceQuestion.type).toBe('MC');
}); // });
it('parses numerical question type correctly', () => { // it('parses numerical question type correctly', () => {
expect(sampleNumericalQuestion.type).toBe('Numerical'); // expect(sampleNumericalQuestion.type).toBe('Numerical');
}); // });
// it('parses short answer question type correctly', () => {
// expect(sampleShortAnswerQuestion.type).toBe('Short');
// });
// });
it('parses short answer question type correctly', () => {
expect(sampleShortAnswerQuestion.type).toBe('Short');
});
});
it('renders correctly for True/False question', () => { it('renders correctly for True/False question', () => {
renderComponent(sampleTrueFalseQuestion); renderComponent(sampleTrueFalseQuestion);
@ -73,7 +74,8 @@ describe('Questions Component', () => {
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('Choice 1'); expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['Choice 1']);
mockHandleSubmitAnswer.mockClear();
}); });
it('renders correctly for Numerical question', () => { it('renders correctly for Numerical question', () => {
@ -93,7 +95,8 @@ describe('Questions Component', () => {
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7); expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([7]);
mockHandleSubmitAnswer.mockClear();
}); });
it('renders correctly for Short Answer question', () => { it('renders correctly for Short Answer question', () => {
@ -117,7 +120,7 @@ describe('Questions Component', () => {
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input'); expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']);
}); });
}); });

View file

@ -47,6 +47,7 @@ describe('ShortAnswerQuestion Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled(); expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
mockHandleSubmitAnswer.mockClear();
}); });
it('submits answer correctly', () => { it('submits answer correctly', () => {
@ -60,6 +61,7 @@ describe('ShortAnswerQuestion Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input'); expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']);
mockHandleSubmitAnswer.mockClear();
}); });
}); });

View file

@ -56,6 +56,7 @@ describe('TrueFalseQuestion Component', () => {
}); });
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled(); expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
mockHandleSubmitAnswer.mockClear();
}); });
it('submits answer correctly for True', () => { it('submits answer correctly for True', () => {
@ -70,7 +71,8 @@ describe('TrueFalseQuestion Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
}); });
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(true); expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([true]);
mockHandleSubmitAnswer.mockClear();
}); });
it('submits answer correctly for False', () => { it('submits answer correctly for False', () => {
@ -83,7 +85,8 @@ describe('TrueFalseQuestion Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
}); });
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(false); expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([false]);
mockHandleSubmitAnswer.mockClear();
}); });
@ -112,7 +115,7 @@ describe('TrueFalseQuestion Component', () => {
expect(wrongAnswer1?.textContent).toContain('❌'); expect(wrongAnswer1?.textContent).toContain('❌');
}); });
it('should not show ✅ or ❌ when repondre button is not clicked', async () => { it('should not show ✅ or ❌ when pondre button is not clicked', async () => {
const choiceButton = screen.getByText('Vrai').closest('button'); const choiceButton = screen.getByText('Vrai').closest('button');
if (!choiceButton) throw new Error('Choice button not found'); if (!choiceButton) throw new Error('Choice button not found');

View file

@ -0,0 +1,359 @@
import { checkIfIsCorrect } from 'src/pages/Teacher/ManageRoom/useRooms';
import { HighLowNumericalAnswer, MultipleChoiceQuestion, MultipleNumericalAnswer, NumericalQuestion, RangeNumericalAnswer, ShortAnswerQuestion, SimpleNumericalAnswer, TrueFalseQuestion } from 'gift-pegjs';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
import { QuestionType } from 'src/Types/QuestionType';
describe('checkIfIsCorrect', () => {
const mockQuestions: QuestionType[] = [
{
question: {
id: '1',
type: 'MC',
choices: [
{ isCorrect: true, formattedText: { text: 'Answer1' } },
{ isCorrect: true, formattedText: { text: 'Answer2' } },
{ isCorrect: false, formattedText: { text: 'Answer3' } },
],
} as MultipleChoiceQuestion,
},
];
test('returns true when all selected answers are correct', () => {
const answer: AnswerType = ['Answer1', 'Answer2'];
const result = checkIfIsCorrect(answer, 1, mockQuestions);
expect(result).toBe(true);
});
test('returns false when some selected answers are incorrect', () => {
const answer: AnswerType = ['Answer1', 'Answer3'];
const result = checkIfIsCorrect(answer, 1, mockQuestions);
expect(result).toBe(false);
});
test('returns false when all correct answers are selected, but one incorrect is also selected', () => {
const answer: AnswerType = ['Answer1', 'Answer2', 'Answer3'];
const result = checkIfIsCorrect(answer, 1, mockQuestions);
expect(result).toBe(false);
});
test('returns false when no answers are selected', () => {
const answer: AnswerType = [];
const result = checkIfIsCorrect(answer, 1, mockQuestions);
expect(result).toBe(false);
});
test('returns false when no correct answers are provided in the question', () => {
const mockQuestionsWithNoCorrectAnswers: QuestionType[] = [
{
question: {
id: '1',
type: 'MC',
choices: [
{ isCorrect: false, formattedText: { text: 'Answer1' } },
{ isCorrect: false, formattedText: { text: 'Answer2' } },
],
} as MultipleChoiceQuestion,
},
];
const answer: AnswerType = ['Answer1'];
const result = checkIfIsCorrect(answer, 1, mockQuestionsWithNoCorrectAnswers);
expect(result).toBe(false);
});
test('returns true for a correct true/false answer', () => {
const mockQuestionsTF: QuestionType[] = [
{
question: {
id: '2',
type: 'TF',
isTrue: true,
} as TrueFalseQuestion,
},
];
const answer: AnswerType = [true];
const result = checkIfIsCorrect(answer, 2, mockQuestionsTF);
expect(result).toBe(true);
});
test('returns false for an incorrect true/false answer', () => {
const mockQuestionsTF: QuestionType[] = [
{
question: {
id: '2',
type: 'TF',
isTrue: true,
} as TrueFalseQuestion,
},
];
const answer: AnswerType = [false];
const result = checkIfIsCorrect(answer, 2, mockQuestionsTF);
expect(result).toBe(false);
});
test('returns false for a true/false question with no answer', () => {
const mockQuestionsTF: QuestionType[] = [
{
question: {
id: '2',
type: 'TF',
isTrue: true,
} as TrueFalseQuestion,
},
];
const answer: AnswerType = [];
const result = checkIfIsCorrect(answer, 2, mockQuestionsTF);
expect(result).toBe(false);
});
test('returns true for a correct true/false answer when isTrue is false', () => {
const mockQuestionsTF: QuestionType[] = [
{
question: {
id: '3',
type: 'TF',
isTrue: false, // Correct answer is false
} as TrueFalseQuestion,
},
];
const answer: AnswerType = [false];
const result = checkIfIsCorrect(answer, 3, mockQuestionsTF);
expect(result).toBe(true);
});
test('returns false for an incorrect true/false answer when isTrue is false', () => {
const mockQuestionsTF: QuestionType[] = [
{
question: {
id: '3',
type: 'TF',
isTrue: false, // Correct answer is false
} as TrueFalseQuestion,
},
];
const answer: AnswerType = [true];
const result = checkIfIsCorrect(answer, 3, mockQuestionsTF);
expect(result).toBe(false);
});
test('returns true for a correct short answer', () => {
const mockQuestionsShort: QuestionType[] = [
{
question: {
id: '4',
type: 'Short',
choices: [
{ text: 'CorrectAnswer1' },
{ text: 'CorrectAnswer2' },
],
} as ShortAnswerQuestion,
},
];
const answer: AnswerType = ['CorrectAnswer1'];
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
expect(result).toBe(true);
});
test('returns false for an incorrect short answer', () => {
const mockQuestionsShort: QuestionType[] = [
{
question: {
id: '4',
type: 'Short',
choices: [
{ text: 'CorrectAnswer1' },
{ text: 'CorrectAnswer2' },
],
} as ShortAnswerQuestion,
},
];
const answer: AnswerType = ['WrongAnswer'];
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
expect(result).toBe(false);
});
test('returns true for a correct short answer with case insensitivity', () => {
const mockQuestionsShort: QuestionType[] = [
{
question: {
id: '4',
type: 'Short',
choices: [
{ text: 'CorrectAnswer1' },
{ text: 'CorrectAnswer2' },
],
} as ShortAnswerQuestion,
},
];
const answer: AnswerType = ['correctanswer1']; // Lowercase version of the correct answer
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
expect(result).toBe(true);
});
test('returns false for a short answer question with no answer', () => {
const mockQuestionsShort: QuestionType[] = [
{
question: {
id: '4',
type: 'Short',
choices: [
{ text: 'CorrectAnswer1' },
{ text: 'CorrectAnswer2' },
],
} as ShortAnswerQuestion,
},
];
const answer: AnswerType = [];
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
expect(result).toBe(false);
});
test('returns true for a correct simple numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '5',
type: 'Numerical',
choices: [
{ type: 'simple', number: 42 } as SimpleNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [42]; // User's answer
const result = checkIfIsCorrect(answer, 5, mockQuestionsNumerical);
expect(result).toBe(true);
});
test('returns false for an incorrect simple numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '5',
type: 'Numerical',
choices: [
{ type: 'simple', number: 42 } as SimpleNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [43]; // User's answer
const result = checkIfIsCorrect(answer, 5, mockQuestionsNumerical);
expect(result).toBe(false);
});
test('returns true for a correct range numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '6',
type: 'Numerical',
choices: [
{ type: 'range', number: 50, range: 5 } as RangeNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [52]; // User's answer within the range (50 ± 5)
const result = checkIfIsCorrect(answer, 6, mockQuestionsNumerical);
expect(result).toBe(true);
});
test('returns false for an out-of-range numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '6',
type: 'Numerical',
choices: [
{ type: 'range', number: 50, range: 5 } as RangeNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [56]; // User's answer outside the range (50 ± 5)
const result = checkIfIsCorrect(answer, 6, mockQuestionsNumerical);
expect(result).toBe(false);
});
test('returns true for a correct high-low numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '7',
type: 'Numerical',
choices: [
{ type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [95]; // User's answer within the range (90 to 100)
const result = checkIfIsCorrect(answer, 7, mockQuestionsNumerical);
expect(result).toBe(true);
});
test('returns false for an out-of-range high-low numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '7',
type: 'Numerical',
choices: [
{ type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [105]; // User's answer outside the range (90 to 100)
const result = checkIfIsCorrect(answer, 7, mockQuestionsNumerical);
expect(result).toBe(false);
});
test('returns true for a correct multiple numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '8',
type: 'Numerical',
choices: [
{
isCorrect: true,
answer: { type: 'simple', number: 42 } as SimpleNumericalAnswer,
} as MultipleNumericalAnswer,
{
isCorrect: false,
answer: { type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer,
formattedFeedback: { text: 'You guessed way too high' },
}
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [42]; // User's answer matches the correct multiple numerical answer
const result = checkIfIsCorrect(answer, 8, mockQuestionsNumerical);
expect(result).toBe(true);
});
test('returns false for an incorrect multiple numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '8',
type: 'Numerical',
choices: [
{
type: 'multiple',
isCorrect: true,
answer: { type: 'simple', number: 42 } as SimpleNumericalAnswer,
} as MultipleNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [43]; // User's answer does not match the correct multiple numerical answer
const result = checkIfIsCorrect(answer, 8, mockQuestionsNumerical);
expect(result).toBe(false);
});
});

View file

@ -44,7 +44,7 @@ const mockStudents: StudentType[] = [
]; ];
const mockAnswerData: AnswerReceptionFromBackendType = { const mockAnswerData: AnswerReceptionFromBackendType = {
answer: 'Answer1', answer: ['Answer1'],
idQuestion: 1, idQuestion: 1,
idUser: '1', idUser: '1',
username: 'Student 1', username: 'Student 1',
@ -233,7 +233,7 @@ describe('ManageRoom', () => {
await act(async () => { await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1]; const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-name'); createSuccessCallback('Test Room');
}); });
const launchButton = screen.getByText('Lancer'); const launchButton = screen.getByText('Lancer');
@ -256,6 +256,7 @@ describe('ManageRoom', () => {
}); });
await waitFor(() => { await waitFor(() => {
// console.info(consoleSpy.mock.calls);
expect(consoleSpy).toHaveBeenCalledWith( expect(consoleSpy).toHaveBeenCalledWith(
'Received answer from Student 1 for question 1: Answer1' 'Received answer from Student 1 for question 1: Answer1'
); );
@ -294,3 +295,4 @@ describe('ManageRoom', () => {
}); });
}); });
}); });

View file

@ -8,7 +8,7 @@ import { QuestionType } from 'src/Types/QuestionType';
import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService';
const mockGiftQuestions = parse( const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B} `::Sample Question 1:: Sample Question 1 {=Option A =Option B ~Option C}
::Sample Question 2:: Sample Question 2 {T}`); ::Sample Question 2:: Sample Question 2 {T}`);
@ -23,9 +23,6 @@ const mockSubmitAnswer = jest.fn();
const mockDisconnectWebSocket = jest.fn(); const mockDisconnectWebSocket = jest.fn();
beforeEach(() => { beforeEach(() => {
// Clear local storage before each test
// localStorage.clear();
render( render(
<MemoryRouter> <MemoryRouter>
<StudentModeQuiz <StudentModeQuiz
@ -54,7 +51,7 @@ describe('StudentModeQuiz', () => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1);
}); });
test('handles shows feedback for an already answered question', async () => { test('handles shows feedback for an already answered question', async () => {
@ -65,13 +62,13 @@ describe('StudentModeQuiz', () => {
act(() => { act(() => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1);
const firstButtonA = screen.getByRole("button", {name: '✅ A Option A'}); const firstButtonA = screen.getByRole("button", {name: '✅ A Option A'});
expect(firstButtonA).toBeInTheDocument(); expect(firstButtonA).toBeInTheDocument();
expect(firstButtonA.querySelector('.selected')).toBeInTheDocument(); expect(firstButtonA.querySelector('.selected')).toBeInTheDocument();
expect(screen.getByRole("button", {name: ' B Option B'})).toBeInTheDocument(); expect(screen.getByRole("button", {name: ' B Option B'})).toBeInTheDocument();
expect(screen.queryByText('Répondre')).not.toBeInTheDocument(); expect(screen.queryByText('Répondre')).not.toBeInTheDocument();
// Navigate to the next question // Navigate to the next question
@ -87,12 +84,12 @@ describe('StudentModeQuiz', () => {
}); });
expect(await screen.findByText('Sample Question 1')).toBeInTheDocument(); expect(await screen.findByText('Sample Question 1')).toBeInTheDocument();
// Since answers are mocked, the it doesn't recognize the question as already answered // Since answers are mocked, it doesn't recognize the question as already answered
// TODO these tests are partially faked, need to be fixed if we can mock the answers // TODO these tests are partially faked, need to be fixed if we can mock the answers
// const buttonA = screen.getByRole("button", {name: '✅ A Option A'}); // const buttonA = screen.getByRole("button", {name: '✅ A Option A'});
const buttonA = screen.getByRole("button", {name: 'A Option A'}); const buttonA = screen.getByRole("button", {name: 'A Option A'});
expect(buttonA).toBeInTheDocument(); expect(buttonA).toBeInTheDocument();
// const buttonB = screen.getByRole("button", {name: ' B Option B'}); // const buttonB = screen.getByRole("button", {name: ' B Option B'});
const buttonB = screen.getByRole("button", {name: 'B Option B'}); const buttonB = screen.getByRole("button", {name: 'B Option B'});
expect(buttonB).toBeInTheDocument(); expect(buttonB).toBeInTheDocument();
// // "Option A" div inside the name of button should have selected class // // "Option A" div inside the name of button should have selected class
@ -122,4 +119,30 @@ describe('StudentModeQuiz', () => {
expect(screen.getByText('Sample Question 2')).toBeInTheDocument(); expect(screen.getByText('Sample Question 2')).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument(); expect(screen.getByText('Répondre')).toBeInTheDocument();
}); });
// le test suivant est fait dans MultipleChoiceQuestionDisplay.test.tsx
// test('allows multiple answers to be selected for a question', async () => {
// // Simulate selecting multiple answers
// act(() => {
// fireEvent.click(screen.getByText('Option A'));
// });
// act(() => {
// fireEvent.click(screen.getByText('Option B'));
// });
// // Simulate submitting the answers
// act(() => {
// fireEvent.click(screen.getByText('Répondre'));
// });
// // Verify that the mockSubmitAnswer function is called with both answers
// expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A', 'Option B'], 1);
// // Verify that the selected answers are displayed as selected
// const buttonA = screen.getByRole('button', { name: '✅ A Option A' });
// const buttonB = screen.getByRole('button', { name: '✅ B Option B' });
// expect(buttonA).toBeInTheDocument();
// expect(buttonB).toBeInTheDocument();
// });
}); });

View file

@ -63,7 +63,8 @@ describe('TeacherModeQuiz', () => {
act(() => { act(() => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1);
mockSubmitAnswer.mockClear();
}); });
test('handles shows feedback for an already answered question', () => { test('handles shows feedback for an already answered question', () => {
@ -74,7 +75,8 @@ describe('TeacherModeQuiz', () => {
act(() => { act(() => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1);
mockSubmitAnswer.mockClear();
mockQuestion = mockQuestions[1].question as MultipleChoiceQuestion; mockQuestion = mockQuestions[1].question as MultipleChoiceQuestion;
// Navigate to the next question by re-rendering with new props // Navigate to the next question by re-rendering with new props
act(() => { act(() => {

View file

@ -13,14 +13,14 @@ type AnswerFeedbackOptions = TemplateOptions & Pick<TextChoice, 'formattedFeedba
interface AnswerWeightOptions extends TemplateOptions { interface AnswerWeightOptions extends TemplateOptions {
weight: TextChoice['weight']; weight: TextChoice['weight'];
} }
// careful -- this template is re-used by True/False questions!
export default function MultipleChoiceAnswersTemplate({ choices }: MultipleChoiceAnswerOptions) { export default function MultipleChoiceAnswersTemplate({ choices }: MultipleChoiceAnswerOptions) {
const id = `id${nanoid(8)}`; const id = `id${nanoid(8)}`;
const isMultipleAnswer = choices.filter(({ isCorrect }) => isCorrect === true).length === 0; const hasManyCorrectChoices = choices.filter(({ isCorrect }) => isCorrect === true).length > 1;
const prompt = `<span style="${ParagraphStyle(state.theme)}">Choisir une réponse${ const prompt = `<span style="${ParagraphStyle(state.theme)}">Choisir une réponse${
isMultipleAnswer ? ` ou plusieurs` : `` hasManyCorrectChoices ? ` ou plusieurs` : ``
}:</span>`; }:</span>`;
const result = choices const result = choices
.map(({ weight, isCorrect, formattedText, formattedFeedback }) => { .map(({ weight, isCorrect, formattedText, formattedFeedback }) => {
@ -32,12 +32,12 @@ export default function MultipleChoiceAnswersTemplate({ choices }: MultipleChoic
const inputId = `id${nanoid(6)}`; const inputId = `id${nanoid(6)}`;
const isPositiveWeight = (weight != undefined) && (weight > 0); const isPositiveWeight = (weight != undefined) && (weight > 0);
const isCorrectOption = isMultipleAnswer ? isPositiveWeight : isCorrect; const isCorrectOption = hasManyCorrectChoices ? isPositiveWeight || isCorrect : isCorrect;
return ` return `
<div class='multiple-choice-answers-container'> <div class='multiple-choice-answers-container'>
<input class="gift-input" type="${ <input class="gift-input" type="${
isMultipleAnswer ? 'checkbox' : 'radio' hasManyCorrectChoices ? 'checkbox' : 'radio'
}" id="${inputId}" name="${id}"> }" id="${inputId}" name="${id}">
${AnswerWeight({ weight: weight })} ${AnswerWeight({ weight: weight })}
<label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}"> <label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}">

View file

@ -101,7 +101,7 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy, handleDelete }) => {
const handleSaveImage = async () => { const handleSaveImage = async () => {
try { try {
if (!importedImage) { if (!importedImage) {
setSnackbarMessage("Veuillez d'abord choisir une image à téléverser."); setSnackbarMessage("Veuillez choisir une image à téléverser.");
setSnackbarSeverity("error"); setSnackbarSeverity("error");
setSnackbarOpen(true); setSnackbarOpen(true);
return; return;
@ -131,6 +131,10 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy, handleDelete }) => {
} }
}; };
const handleCloseSnackbar = () => {
setSnackbarOpen(false);
};
return ( return (
<Box p={3}> <Box p={3}>
<Tabs value={tabValue} onChange={(_, newValue) => setTabValue(newValue)}> <Tabs value={tabValue} onChange={(_, newValue) => setTabValue(newValue)}>
@ -224,9 +228,11 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy, handleDelete }) => {
<Box display="flex" flexDirection="row" alignItems="center" width="100%" maxWidth={400}> <Box display="flex" flexDirection="row" alignItems="center" width="100%" maxWidth={400}>
<TextField <TextField
type="file" type="file"
data-testid="file-input"
onChange={handleImageUpload} onChange={handleImageUpload}
slotProps={{ slotProps={{
htmlInput: { htmlInput: {
"data-testid": "file-input",
accept: "image/*", accept: "image/*",
}, },
}} }}
@ -244,7 +250,8 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy, handleDelete }) => {
</Box> </Box>
)} )}
<Dialog open={!!selectedImage} onClose={() => setSelectedImage(null)} maxWidth="md"> <Dialog open={!!selectedImage} onClose={() => setSelectedImage(null)} maxWidth="md">
<IconButton color="primary" onClick={() => setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}> <IconButton color="primary" onClick={() => setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}
data-testid="close-button">
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
<DialogContent> <DialogContent>
@ -274,8 +281,15 @@ const ImageGallery: React.FC<ImagesProps> = ({ handleCopy, handleDelete }) => {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Snackbar open={snackbarOpen} autoHideDuration={4000} onClose={() => setSnackbarOpen(false)}> <Snackbar
<Alert onClose={() => setSnackbarOpen(false)} severity={snackbarSeverity} sx={{ width: "100%" }}> open={snackbarOpen}
autoHideDuration={4000}
onClose={handleCloseSnackbar}
>
<Alert
onClose={handleCloseSnackbar}
severity={snackbarSeverity}
sx={{ width: "100%" }}>
{snackbarMessage} {snackbarMessage}
</Alert> </Alert>
</Snackbar> </Snackbar>

View file

@ -51,7 +51,7 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)', borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold', fontWeight: 'bold',
color: 'rgba(0, 0, 0)' color: 'rgba(0, 0, 0)',
}} }}
> >
{students.length > 0 {students.length > 0
@ -67,7 +67,7 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
borderColor: 'rgba(224, 224, 224, 1)', borderColor: 'rgba(224, 224, 224, 1)',
fontWeight: 'bold', fontWeight: 'bold',
fontSize: '1rem', fontSize: '1rem',
color: 'rgba(0, 0, 0)' color: 'rgba(0, 0, 0)',
}} }}
> >
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'} {students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
@ -76,4 +76,4 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
</TableFooter> </TableFooter>
); );
}; };
export default LiveResultsTableFooter; export default LiveResultsTableFooter;

View file

@ -15,76 +15,115 @@ interface Props {
const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => { const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props;
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || ''); console.log('MultipleChoiceQuestionDisplay: passedAnswer', JSON.stringify(passedAnswer));
const [answer, setAnswer] = useState<AnswerType>(() => {
if (passedAnswer && passedAnswer.length > 0) {
return passedAnswer;
}
return [];
});
let disableButton = false; let disableButton = false;
if(handleOnSubmitAnswer === undefined){ if (handleOnSubmitAnswer === undefined) {
disableButton = true; disableButton = true;
} }
useEffect(() => { useEffect(() => {
if (passedAnswer !== undefined) { console.log('MultipleChoiceQuestionDisplay: passedAnswer', JSON.stringify(passedAnswer));
setAnswer(passedAnswer); if (passedAnswer !== undefined) {
} setAnswer(passedAnswer);
}, [passedAnswer]); } else {
setAnswer([]);
}
}, [passedAnswer, question.id]);
const handleOnClickAnswer = (choice: string) => { const handleOnClickAnswer = (choice: string) => {
setAnswer(choice); setAnswer((prevAnswer) => {
console.log(`handleOnClickAnswer -- setAnswer(): prevAnswer: ${prevAnswer}, choice: ${choice}`);
const correctAnswersCount = question.choices.filter((c) => c.isCorrect).length;
if (correctAnswersCount === 1) {
// If only one correct answer, replace the current selection
return prevAnswer.includes(choice) ? [] : [choice];
} else {
// Allow multiple selections if there are multiple correct answers
if (prevAnswer.includes(choice)) {
// Remove the choice if it's already selected
return prevAnswer.filter((selected) => selected !== choice);
} else {
// Add the choice if it's not already selected
return [...prevAnswer, choice];
}
}
});
}; };
const alpha = Array.from(Array(26)).map((_e, i) => i + 65); const alpha = Array.from(Array(26)).map((_e, i) => i + 65);
const alphabet = alpha.map((x) => String.fromCharCode(x)); const alphabet = alpha.map((x) => String.fromCharCode(x));
return (
return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} /> <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
{question.choices.map((choice, i) => { {question.choices.map((choice, i) => {
const selected = answer === choice.formattedText.text ? 'selected' : ''; console.log(`answer: ${answer}, choice: ${choice.formattedText.text}`);
const selected = answer.includes(choice.formattedText.text) ? 'selected' : '';
return ( return (
<div key={choice.formattedText.text + i} className="choice-container"> <div key={choice.formattedText.text + i} className="choice-container">
<Button <Button
variant="text" variant="text"
className="button-wrapper" className="button-wrapper"
disabled={disableButton} disabled={disableButton}
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}> onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}
{showAnswer? (<div> {(choice.isCorrect ? '✅' : '❌')}</div>) >
:``} {showAnswer ? (
<div>{choice.isCorrect ? '✅' : '❌'}</div>
) : (
''
)}
<div className={`circle ${selected}`}>{alphabet[i]}</div> <div className={`circle ${selected}`}>{alphabet[i]}</div>
<div className={`answer-text ${selected}`}> <div className={`answer-text ${selected}`}>
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedText) }} /> <div
dangerouslySetInnerHTML={{
__html: FormattedTextTemplate(choice.formattedText),
}}
/>
</div> </div>
{choice.formattedFeedback && showAnswer && ( {choice.formattedFeedback && showAnswer && (
<div className="feedback-container mb-1 mt-1/2"> <div className="feedback-container mb-1 mt-1/2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedFeedback) }} /> <div
</div> dangerouslySetInnerHTML={{
)} __html: FormattedTextTemplate(choice.formattedFeedback),
}}
/>
</div>
)}
</Button> </Button>
</div> </div>
); );
})} })}
</div> </div>
{question.formattedGlobalFeedback && showAnswer && ( {question.formattedGlobalFeedback && showAnswer && (
<div className="global-feedback mb-2"> <div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} /> <div
</div> dangerouslySetInnerHTML={{
__html: FormattedTextTemplate(question.formattedGlobalFeedback),
}}
/>
</div>
)} )}
{!showAnswer && handleOnSubmitAnswer && ( {!showAnswer && handleOnSubmitAnswer && (
<Button <Button
variant="contained" variant="contained"
onClick={() => onClick={() =>
answer !== "" && handleOnSubmitAnswer && handleOnSubmitAnswer(answer) answer.length > 0 && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
} }
disabled={answer === '' || answer === null} disabled={answer.length === 0}
> >
Répondre Répondre
</Button> </Button>
)} )}
</div> </div>

View file

@ -17,7 +17,7 @@ interface Props {
const NumericalQuestionDisplay: React.FC<Props> = (props) => { const NumericalQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } =
props; props;
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || ''); const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
const correctAnswers = question.choices; const correctAnswers = question.choices;
let correctAnswer = ''; let correctAnswer = '';
@ -69,7 +69,7 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
id={question.formattedStem.text} id={question.formattedStem.text}
name={question.formattedStem.text} name={question.formattedStem.text}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAnswer(e.target.valueAsNumber); setAnswer([e.target.valueAsNumber]);
}} }}
inputProps={{ 'data-testid': 'number-input' }} inputProps={{ 'data-testid': 'number-input' }}
/> />
@ -87,7 +87,7 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
handleOnSubmitAnswer && handleOnSubmitAnswer &&
handleOnSubmitAnswer(answer) handleOnSubmitAnswer(answer)
} }
disabled={answer === "" || isNaN(answer as number)} disabled={answer === undefined || answer === null || isNaN(answer[0] as number)}
> >
Répondre Répondre
</Button> </Button>

View file

@ -16,7 +16,7 @@ interface Props {
const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => { const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props;
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || ''); const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
useEffect(() => { useEffect(() => {
if (passedAnswer !== undefined) { if (passedAnswer !== undefined) {
@ -58,7 +58,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
id={question.formattedStem.text} id={question.formattedStem.text}
name={question.formattedStem.text} name={question.formattedStem.text}
onChange={(e) => { onChange={(e) => {
setAnswer(e.target.value); setAnswer([e.target.value]);
}} }}
disabled={showAnswer} disabled={showAnswer}
aria-label="short-answer-input" aria-label="short-answer-input"
@ -72,7 +72,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
handleOnSubmitAnswer && handleOnSubmitAnswer &&
handleOnSubmitAnswer(answer) handleOnSubmitAnswer(answer)
} }
disabled={answer === null || answer === ''} disabled={answer === null || answer === undefined || answer.length === 0}
> >
Répondre Répondre
</Button> </Button>

View file

@ -1,5 +1,5 @@
// TrueFalseQuestion.tsx // TrueFalseQuestion.tsx
import React, { useState,useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import '../questionStyle.css'; import '../questionStyle.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { TrueFalseQuestion } from 'gift-pegjs'; import { TrueFalseQuestion } from 'gift-pegjs';
@ -8,37 +8,37 @@ import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
interface Props { interface Props {
question: TrueFalseQuestion; question: TrueFalseQuestion;
handleOnSubmitAnswer?: (answer: AnswerType) => void; handleOnSubmitAnswer?: (answer: AnswerType) => void;
showAnswer?: boolean; showAnswer?: boolean;
passedAnswer?: AnswerType; passedAnswer?: AnswerType;
} }
const TrueFalseQuestionDisplay: React.FC<Props> = (props) => { const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer} = const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } =
props; props;
const [answer, setAnswer] = useState<boolean | undefined>(() => {
if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
return passedAnswer[0];
}
return undefined;
});
let disableButton = false; let disableButton = false;
if(handleOnSubmitAnswer === undefined){ if (handleOnSubmitAnswer === undefined) {
disableButton = true; disableButton = true;
} }
useEffect(() => { useEffect(() => {
console.log("passedAnswer", answer); console.log("passedAnswer", passedAnswer);
if (passedAnswer === true || passedAnswer === false) { if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
setAnswer(passedAnswer); setAnswer(passedAnswer[0]);
} else { } else {
setAnswer(undefined); setAnswer(undefined);
}
}, [passedAnswer, question.id]);
const [answer, setAnswer] = useState<boolean | undefined>(() => {
if (passedAnswer === true || passedAnswer === false) {
return passedAnswer;
} }
}, [passedAnswer, question.id]);
return undefined;
});
const handleOnClickAnswer = (choice: boolean) => { const handleOnClickAnswer = (choice: boolean) => {
setAnswer(choice); setAnswer(choice);
@ -49,7 +49,7 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
return ( return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} /> <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
<Button <Button
@ -58,15 +58,15 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
fullWidth fullWidth
disabled={disableButton} disabled={disableButton}
> >
{showAnswer? (<div> {(question.isTrue ? '✅' : '❌')}</div>):``} {showAnswer ? (<div> {(question.isTrue ? '✅' : '❌')}</div>) : ``}
<div className={`circle ${selectedTrue}`}>V</div> <div className={`circle ${selectedTrue}`}>V</div>
<div className={`answer-text ${selectedTrue}`}>Vrai</div> <div className={`answer-text ${selectedTrue}`}>Vrai</div>
{showAnswer && answer && question.trueFormattedFeedback && ( {showAnswer && answer && question.trueFormattedFeedback && (
<div className="true-feedback mb-2"> <div className="true-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} /> <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} />
</div> </div>
)} )}
</Button> </Button>
<Button <Button
className="button-wrapper" className="button-wrapper"
@ -75,15 +75,15 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
disabled={disableButton} disabled={disableButton}
> >
{showAnswer? (<div> {(!question.isTrue ? '✅' : '❌')}</div>):``} {showAnswer ? (<div> {(!question.isTrue ? '✅' : '❌')}</div>) : ``}
<div className={`circle ${selectedFalse}`}>F</div> <div className={`circle ${selectedFalse}`}>F</div>
<div className={`answer-text ${selectedFalse}`}>Faux</div> <div className={`answer-text ${selectedFalse}`}>Faux</div>
{showAnswer && !answer && question.falseFormattedFeedback && ( {showAnswer && !answer && question.falseFormattedFeedback && (
<div className="false-feedback mb-2"> <div className="false-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.falseFormattedFeedback) }} /> <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.falseFormattedFeedback) }} />
</div> </div>
)} )}
</Button> </Button>
</div> </div>
{question.formattedGlobalFeedback && showAnswer && ( {question.formattedGlobalFeedback && showAnswer && (
@ -95,8 +95,7 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
<Button <Button
variant="contained" variant="contained"
onClick={() => onClick={() =>
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer(answer) answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer([answer])
} }
disabled={answer === undefined} disabled={answer === undefined}
> >

View file

@ -17,7 +17,7 @@ import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService' import ApiService from '../../../services/ApiService'
export type AnswerType = string | number | boolean; export type AnswerType = Array<string | number | boolean>;
const JoinRoom: React.FC = () => { const JoinRoom: React.FC = () => {
const [roomName, setRoomName] = useState(''); const [roomName, setRoomName] = useState('');
@ -39,9 +39,8 @@ const JoinRoom: React.FC = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
// init the answers array, one for each question
setAnswers(Array(questions.length).fill({} as AnswerSubmissionToBackendType));
console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`); console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`);
setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []);
}, [questions]); }, [questions]);
@ -64,6 +63,7 @@ const JoinRoom: React.FC = () => {
console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions); console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions);
setQuizMode('teacher'); setQuizMode('teacher');
setIsWaitingForTeacher(true); setIsWaitingForTeacher(true);
setQuestions([]); // clear out from last time (in case quiz is repeated)
setQuestions(questions); setQuestions(questions);
// wait for next-question // wait for next-question
}); });
@ -72,6 +72,7 @@ const JoinRoom: React.FC = () => {
setQuizMode('student'); setQuizMode('student');
setIsWaitingForTeacher(false); setIsWaitingForTeacher(false);
setQuestions([]); // clear out from last time (in case quiz is repeated)
setQuestions(questions); setQuestions(questions);
setQuestion(questions[0]); setQuestion(questions[0]);
}); });

View file

@ -1,12 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs'; import { BaseQuestion, parse, Question } from 'gift-pegjs';
import {
isSimpleNumericalAnswer,
isRangeNumericalAnswer,
isHighLowNumericalAnswer
} from 'gift-pegjs/typeGuards';
import LiveResultsComponent from 'src/components/LiveResults/LiveResults'; import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
import webSocketService, { import webSocketService, {
AnswerReceptionFromBackendType AnswerReceptionFromBackendType
@ -24,7 +19,7 @@ import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
import { QuestionType } from 'src/Types/QuestionType'; import { QuestionType } from 'src/Types/QuestionType';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; import { checkIfIsCorrect } from './useRooms';
const ManageRoom: React.FC = () => { const ManageRoom: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -36,8 +31,40 @@ const ManageRoom: React.FC = () => {
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher'); const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
const [connectingError, setConnectingError] = useState<string>(''); const [connectingError, setConnectingError] = useState<string>('');
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined); const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
const [quizStarted, setQuizStarted] = useState(false); const [quizStarted, setQuizStarted] = useState<boolean>(false);
const [formattedRoomName, setFormattedRoomName] = useState(""); const [formattedRoomName, setFormattedRoomName] = useState("");
const [newlyConnectedUser, setNewlyConnectedUser] = useState<StudentType | null>(null);
// Handle the newly connected user in useEffect, because it needs state info
// not available in the socket.on() callback
useEffect(() => {
if (newlyConnectedUser) {
console.log(`Handling newly connected user: ${newlyConnectedUser.name}`);
setStudents((prevStudents) => [...prevStudents, newlyConnectedUser]);
// only send nextQuestion if the quiz has started
if (!quizStarted) {
console.log(`!quizStarted: returning.... `);
return;
}
if (quizMode === 'teacher') {
webSocketService.nextQuestion({
roomName: formattedRoomName,
questions: quizQuestions,
questionIndex: Number(currentQuestion?.question.id) - 1,
isLaunch: true // started late
});
} else if (quizMode === 'student') {
webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
} else {
console.error('Invalid quiz mode:', quizMode);
}
// Reset the newly connected user state
setNewlyConnectedUser(null);
}
}, [newlyConnectedUser]);
useEffect(() => { useEffect(() => {
const verifyLogin = async () => { const verifyLogin = async () => {
@ -110,6 +137,17 @@ const ManageRoom: React.FC = () => {
const roomNameUpper = roomName.toUpperCase(); const roomNameUpper = roomName.toUpperCase();
setFormattedRoomName(roomNameUpper); setFormattedRoomName(roomNameUpper);
console.log(`Creating WebSocket room named ${roomNameUpper}`); console.log(`Creating WebSocket room named ${roomNameUpper}`);
/**
* ATTENTION: Lire les variables d'état dans
* les .on() n'est pas une bonne pratique.
* Les valeurs sont celles au moment de la création
* de la fonction et non au moment de l'exécution.
* Il faut utiliser des refs pour les valeurs qui
* changent fréquemment. Sinon, utiliser un trigger
* de useEffect pour mettre déclencher un traitement
* (voir user-joined plus bas).
*/
socket.on('connect', () => { socket.on('connect', () => {
webSocketService.createRoom(roomNameUpper); webSocketService.createRoom(roomNameUpper);
}); });
@ -124,23 +162,9 @@ const ManageRoom: React.FC = () => {
}); });
socket.on('user-joined', (student: StudentType) => { socket.on('user-joined', (student: StudentType) => {
console.log(`Student joined: name = ${student.name}, id = ${student.id}, quizMode = ${quizMode}, quizStarted = ${quizStarted}`); setNewlyConnectedUser(student);
setStudents((prevStudents) => [...prevStudents, student]);
// only send nextQuestion if the quiz has started
if (!quizStarted) return;
if (quizMode === 'teacher') {
webSocketService.nextQuestion(
{roomName: formattedRoomName,
questions: quizQuestions,
questionIndex: Number(currentQuestion?.question.id) - 1,
isLaunch: false});
} else if (quizMode === 'student') {
webSocketService.launchStudentModeQuiz(formattedRoomName, quizQuestions);
}
}); });
socket.on('join-failure', (message) => { socket.on('join-failure', (message) => {
setConnectingError(message); setConnectingError(message);
setSocket(null); setSocket(null);
@ -286,21 +310,19 @@ const ManageRoom: React.FC = () => {
}; };
const launchQuiz = () => { const launchQuiz = () => {
setQuizStarted(true);
if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) { if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) {
// TODO: This error happens when token expires! Need to handle it properly // TODO: This error happens when token expires! Need to handle it properly
console.log( console.log(
`Error launching quiz. socket: ${socket}, roomName: ${formattedRoomName}, quiz: ${quiz}` `Error launching quiz. socket: ${socket}, roomName: ${formattedRoomName}, quiz: ${quiz}`
); );
setQuizStarted(true);
return; return;
} }
console.log(`Launching quiz in ${quizMode} mode...`);
switch (quizMode) { switch (quizMode) {
case 'student': case 'student':
setQuizStarted(true);
return launchStudentMode(); return launchStudentMode();
case 'teacher': case 'teacher':
setQuizStarted(true);
return launchTeacherMode(); return launchTeacherMode();
} }
}; };
@ -319,63 +341,6 @@ const ManageRoom: React.FC = () => {
navigate('/teacher/dashboard'); navigate('/teacher/dashboard');
}; };
function checkIfIsCorrect(
answer: AnswerType,
idQuestion: number,
questions: QuestionType[]
): boolean {
const questionInfo = questions.find((q) =>
q.question.id ? q.question.id === idQuestion.toString() : false
) as QuestionType | undefined;
const answerText = answer.toString();
if (questionInfo) {
const question = questionInfo.question as ParsedGIFTQuestion;
if (question.type === 'TF') {
return (
(question.isTrue && answerText == 'true') ||
(!question.isTrue && answerText == 'false')
);
} else if (question.type === 'MC') {
return question.choices.some(
(choice) => choice.isCorrect && choice.formattedText.text === answerText
);
} else if (question.type === 'Numerical') {
if (isHighLowNumericalAnswer(question.choices[0])) {
const choice = question.choices[0];
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return (
answerNumber <= choice.numberHigh && answerNumber >= choice.numberLow
);
}
}
if (isRangeNumericalAnswer(question.choices[0])) {
const answerNumber = parseFloat(answerText);
const range = question.choices[0].range;
const correctAnswer = question.choices[0].number;
if (!isNaN(answerNumber)) {
return (
answerNumber <= correctAnswer + range &&
answerNumber >= correctAnswer - range
);
}
}
if (isSimpleNumericalAnswer(question.choices[0])) {
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return answerNumber === question.choices[0].number;
}
}
} else if (question.type === 'Short') {
return question.choices.some(
(choice) => choice.text.toUpperCase() === answerText.toUpperCase()
);
}
}
return false;
}
if (!formattedRoomName) { if (!formattedRoomName) {
return ( return (
<div className="center"> <div className="center">

View file

@ -1,8 +1,15 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { RoomType } from 'src/Types/RoomType'; import { RoomType } from 'src/Types/RoomType';
import { createContext } from 'react'; import { createContext } from 'react';
import { MultipleNumericalAnswer, NumericalAnswer, ParsedGIFTQuestion } from 'gift-pegjs';
//import { RoomContext } from './RoomContext'; import { QuestionType } from 'src/Types/QuestionType';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
import {
isSimpleNumericalAnswer,
isRangeNumericalAnswer,
isHighLowNumericalAnswer,
isMultipleNumericalAnswer
} from 'gift-pegjs/typeGuards';
type RoomContextType = { type RoomContextType = {
rooms: RoomType[]; rooms: RoomType[];
@ -18,3 +25,137 @@ export const useRooms = () => {
if (!context) throw new Error('useRooms must be used within a RoomProvider'); if (!context) throw new Error('useRooms must be used within a RoomProvider');
return context; return context;
}; };
/**
* Checks if the answer is correct - logic varies by type of question!
* True/False: answer must match the isTrue property
* Multiple Choice: answer must match the correct choice(s)
* Numerical: answer must be within the range or equal to the number (for each type of correct answer)
* Short Answer: answer must match the correct choice(s) (case-insensitive)
* @param answer
* @param idQuestion
* @param questions
* @returns
*/
export function checkIfIsCorrect(
answer: AnswerType,
idQuestion: number,
questions: QuestionType[]
): boolean {
const questionInfo = questions.find((q) =>
q.question.id ? q.question.id === idQuestion.toString() : false
) as QuestionType | undefined;
const simpleAnswerText = answer.toString();
if (questionInfo) {
const question = questionInfo.question as ParsedGIFTQuestion;
if (question.type === 'TF') {
return (
(question.isTrue && simpleAnswerText == 'true') ||
(!question.isTrue && simpleAnswerText == 'false')
);
} else if (question.type === 'MC') {
const correctChoices = question.choices.filter((choice) => choice.isCorrect
/* || (choice.weight && choice.weight > 0)*/ // handle weighted answers
);
const multipleAnswers = Array.isArray(answer) ? answer : [answer as string];
if (correctChoices.length === 0) {
return false;
}
// check if all (and only) correct choices are in the multipleAnswers array
return correctChoices.length === multipleAnswers.length && correctChoices.every(
(choice) => multipleAnswers.includes(choice.formattedText.text)
);
} else if (question.type === 'Numerical') {
if (isMultipleNumericalAnswer(question.choices[0])) { // Multiple numerical answers
// check to see if answer[0] is a match for any of the choices that isCorrect
const correctChoices = question.choices.filter((choice) => isMultipleNumericalAnswer(choice) && choice.isCorrect);
if (correctChoices.length === 0) { // weird case where there are multiple numerical answers but none are correct
return false;
}
return correctChoices.some((choice) => {
// narrow choice to MultipleNumericalAnswer type
const multipleNumericalChoice = choice as MultipleNumericalAnswer;
return isCorrectNumericalAnswer(multipleNumericalChoice.answer, simpleAnswerText);
});
}
if (isHighLowNumericalAnswer(question.choices[0])) {
// const choice = question.choices[0];
// const answerNumber = parseFloat(simpleAnswerText);
// if (!isNaN(answerNumber)) {
// return (
// answerNumber <= choice.numberHigh && answerNumber >= choice.numberLow
// );
// }
return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText);
}
if (isRangeNumericalAnswer(question.choices[0])) {
// const answerNumber = parseFloat(simpleAnswerText);
// const range = question.choices[0].range;
// const correctAnswer = question.choices[0].number;
// if (!isNaN(answerNumber)) {
// return (
// answerNumber <= correctAnswer + range &&
// answerNumber >= correctAnswer - range
// );
// }
return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText);
}
if (isSimpleNumericalAnswer(question.choices[0])) {
// const answerNumber = parseFloat(simpleAnswerText);
// if (!isNaN(answerNumber)) {
// return answerNumber === question.choices[0].number;
// }
return isCorrectNumericalAnswer(question.choices[0], simpleAnswerText);
}
} else if (question.type === 'Short') {
return question.choices.some(
(choice) => choice.text.toUpperCase() === simpleAnswerText.toUpperCase()
);
}
}
return false;
}
/**
* Determines if a numerical answer is correct based on the type of numerical answer.
* @param correctAnswer The correct answer (of type NumericalAnswer).
* @param userAnswer The user's answer (as a string or number).
* @returns True if the user's answer is correct, false otherwise.
*/
export function isCorrectNumericalAnswer(
correctAnswer: NumericalAnswer,
userAnswer: string | number
): boolean {
const answerNumber = typeof userAnswer === 'string' ? parseFloat(userAnswer) : userAnswer;
if (isNaN(answerNumber)) {
return false; // User's answer is not a valid number
}
if (isSimpleNumericalAnswer(correctAnswer)) {
// Exact match for simple numerical answers
return answerNumber === correctAnswer.number;
}
if (isRangeNumericalAnswer(correctAnswer)) {
// Check if the user's answer is within the range
const { number, range } = correctAnswer;
return answerNumber >= number - range && answerNumber <= number + range;
}
if (isHighLowNumericalAnswer(correctAnswer)) {
// Check if the user's answer is within the high-low range
const { numberLow, numberHigh } = correctAnswer;
return answerNumber >= numberLow && answerNumber <= numberHigh;
}
// if (isMultipleNumericalAnswer(correctAnswer)) {
// // Check if the user's answer matches any of the multiple numerical answers
// return correctAnswer.answer.some((choice) =>
// isCorrectNumericalAnswer(choice, answerNumber)
// );
// }
return false; // Default to false if the answer type is not recognized
}