[FEATURE] Être possible de cacher le tableau du LiveResult

Fixes #294
This commit is contained in:
JubaAzul 2025-03-24 16:45:53 -04:00
commit 7b83a93c5b
26 changed files with 878 additions and 222 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

@ -8,9 +8,17 @@ import { StudentType } from 'src/Types/StudentType';
import { BaseQuestion, parse } from 'gift-pegjs'; import { BaseQuestion, parse } from 'gift-pegjs';
const mockGiftQuestions = parse( const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Answer 1 ~Answer 2} `::Sample Question 1:: Question stem
{
=Choice 1
=Choice 2
~Choice 3
~Choice 4
}
::Sample Question 2:: Sample Question 2 {T}`); ::Sample Question 2:: Question stem {TRUE}
`);
const mockSocket: Socket = { const mockSocket: Socket = {
on: jest.fn(), on: jest.fn(),
@ -27,9 +35,13 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
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: 'Answer 1', isCorrect: true }] }, { id: '1', name: 'Student 1', answers: [] },
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 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}] },
]; ];
const mockShowSelectedQuestion = jest.fn(); const mockShowSelectedQuestion = jest.fn();
@ -94,6 +106,7 @@ test('calculates and displays the correct student grades', () => {
/> />
); );
// Toggle the display of usernames // Toggle the display of usernames
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms'); const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
@ -101,10 +114,15 @@ test('calculates and displays the correct student grades', () => {
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
const getByTextInTableCellBody = (text: string) => {
const elements = screen.getAllByText(text); // Get all elements with the specified text
return elements.find((element) => element.closest('.MuiTableCell-body')); // don't get the footer element(s)
};
mockStudents.forEach((student) => { mockStudents.forEach((student) => {
const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100; const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100;
const gradeElements = screen.getAllByText(`${grade.toFixed()} %`); const element = getByTextInTableCellBody(`${grade.toFixed()} %`);
expect(gradeElements.length).toBeGreaterThan(0);}); expect(element).toBeInTheDocument();
});
}); });
test('calculates and displays the class average', () => { test('calculates and displays the class average', () => {
@ -160,6 +178,8 @@ test('displays the correct answers per question', () => {
expect(correctAnswersElement).toBeInTheDocument(); expect(correctAnswersElement).toBeInTheDocument();
}); });
}); });
test('renders LiveResults component', () => { test('renders LiveResults component', () => {
render( render(
<LiveResults <LiveResults

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`] = `
&lt;div class='multiple-choice-answers-container'&gt; &lt;div class='multiple-choice-answers-container'&gt;
&lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt; &lt;input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id"&gt;
&lt;span class="answer-weight-container answer-positive-weight"&gt;1%&lt;/span&gt;
&lt;label style=" &lt;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"&gt; " for="idmocked-id"&gt;
Choice 2 Choice 2
&lt;/label&gt; &lt;/label&gt;
&lt;svg data-testid="correct-icon" style=" &lt;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"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt; " role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"&gt;&lt;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"&gt;&lt;/path&gt;&lt;/svg&gt;
&lt;span class="feedback-container"&gt;Correct!&lt;/span&gt; &lt;span class="feedback-container"&gt;Correct!&lt;/span&gt;
&lt;/input&gt; &lt;/input&gt;
&lt;/div&gt; &lt;/div&gt;

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

@ -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('');

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();
@ -348,63 +343,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
}