mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Merge branch 'feature/gestion-images' of https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir into feature/gestion-images
This commit is contained in:
commit
8e999dc10f
27 changed files with 996 additions and 317 deletions
30
README.fr-ca.md
Normal file
30
README.fr-ca.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/frontend-deploy.yml)
|
||||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml)
|
||||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml)
|
||||
|
||||
|
||||
[](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).
|
||||
22
README.md
22
README.md
|
|
@ -2,24 +2,26 @@
|
|||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/backend-deploy.yml)
|
||||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/deploy.yml)
|
||||
|
||||
# EvalueTonSavoir
|
||||
[](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.
|
||||
* 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.
|
||||
## Key Features
|
||||
|
||||
* **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
|
||||
|
||||
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)
|
||||
* [Dépôt d'origine Backend](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Backend)
|
||||
* [Original Frontend Repository](https://github.com/ETS-PFE004-Plateforme-sondage-minitest/ETS-PFE004-EvalueTonSavoir-Frontend)
|
||||
* [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)
|
||||
|
||||
## License
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
|
|||
});
|
||||
|
||||
const mockStudents: StudentType[] = [
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] },
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
|
||||
];
|
||||
|
||||
const mockShowSelectedQuestion = jest.fn();
|
||||
|
|
@ -92,4 +92,4 @@ describe('LiveResults', () => {
|
|||
|
||||
expect(mockShowSelectedQuestion).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
|
|||
|
||||
|
||||
const mockStudents: StudentType[] = [
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] },
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
|
||||
];
|
||||
|
||||
const mockShowSelectedQuestion = jest.fn();
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
|
|||
});
|
||||
|
||||
const mockStudents: StudentType[] = [
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] },
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
|
||||
];
|
||||
|
||||
const mockGetStudentGrade = jest.fn((student: StudentType) => {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import LiveResultsTableFooter from 'src/components/LiveResults/LiveResultsTable/
|
|||
|
||||
|
||||
const mockStudents: StudentType[] = [
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] },
|
||||
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
|
||||
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
|
||||
];
|
||||
|
||||
const mockGetStudentGrade = jest.fn((student: StudentType) => {
|
||||
|
|
@ -52,4 +52,4 @@ describe('LiveResultsTableFooter', () => {
|
|||
|
||||
expect(screen.getByText('50 %')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const katekMock: TemplateOptions & MultipleChoiceQuestion = {
|
|||
formattedStem: { format: 'plain' , text: '$$\\frac{zzz}{yyy}$$'},
|
||||
choices: [
|
||||
{ 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' }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -733,7 +733,7 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
|
|||
|
||||
<div class='multiple-choice-answers-container'>
|
||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
||||
<span class="answer-weight-container answer-positive-weight">1%</span>
|
||||
|
||||
<label style="
|
||||
display: inline-block;
|
||||
padding: 0.2em 0 0.2em 0;
|
||||
|
|
@ -742,15 +742,15 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
|
|||
" for="idmocked-id">
|
||||
Choice 2
|
||||
</label>
|
||||
<svg data-testid="correct-icon" style="
|
||||
<svg data-testid="incorrect-icon" style="
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin-left: 0.1rem;
|
||||
margin-right: 0.2rem;
|
||||
|
||||
width: 1em;
|
||||
color: hsl(120, 39%, 54%);
|
||||
" 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>
|
||||
width: 0.75em;
|
||||
color: hsl(2, 64%, 58%);
|
||||
" 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>
|
||||
</input>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import LiveResults from 'src/components/LiveResults/LiveResults';
|
|||
import { QuestionType } from 'src/Types/QuestionType';
|
||||
import { StudentType } from 'src/Types/StudentType';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { BaseQuestion,parse } from 'gift-pegjs';
|
||||
import { BaseQuestion, parse } from 'gift-pegjs';
|
||||
|
||||
const mockSocket: Socket = {
|
||||
on: jest.fn(),
|
||||
|
|
@ -19,19 +19,28 @@ const mockGiftQuestions = parse(
|
|||
`::Sample Question 1:: Question stem
|
||||
{
|
||||
=Choice 1
|
||||
~Choice 2
|
||||
}`);
|
||||
=Choice 2
|
||||
~Choice 3
|
||||
~Choice 4
|
||||
}
|
||||
|
||||
::Sample Question 2:: Question stem {TRUE}
|
||||
`);
|
||||
|
||||
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
|
||||
if (question.type !== "Category")
|
||||
question.id = (index + 1).toString();
|
||||
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[] = [
|
||||
{ id: '1', name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Choice 1', isCorrect: true }] },
|
||||
{ id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: 'Choice 2', isCorrect: false }] },
|
||||
{ id: '1', name: 'Student 1', answers: [] },
|
||||
{ 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', () => {
|
||||
|
|
@ -52,7 +61,7 @@ describe('LiveResults', () => {
|
|||
|
||||
// Toggle the display of usernames back
|
||||
fireEvent.click(toggleUsernamesSwitch);
|
||||
|
||||
|
||||
// Check if the component renders the students
|
||||
mockStudents.forEach((student) => {
|
||||
expect(screen.getByText(student.name)).toBeInTheDocument();
|
||||
|
|
@ -82,82 +91,88 @@ describe('LiveResults', () => {
|
|||
});
|
||||
});
|
||||
|
||||
});
|
||||
test('calculates and displays the correct student grades', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={mockSocket}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={jest.fn()}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
test('calculates and displays the correct student grades', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={mockSocket}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={jest.fn()}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
// Toggle the display of usernames
|
||||
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
|
||||
// Toggle the display of usernames
|
||||
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
|
||||
|
||||
// Toggle the display of usernames back
|
||||
fireEvent.click(toggleUsernamesSwitch);
|
||||
|
||||
// Check if the student grades are calculated and displayed correctly
|
||||
mockStudents.forEach((student) => {
|
||||
const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100;
|
||||
expect(screen.getByText(`${grade.toFixed()} %`)).toBeInTheDocument();
|
||||
// Toggle the display of usernames back
|
||||
fireEvent.click(toggleUsernamesSwitch);
|
||||
|
||||
// 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) => {
|
||||
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', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={mockSocket}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={jest.fn()}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
test('calculates and displays the class average', () => {
|
||||
render(
|
||||
<LiveResults
|
||||
socket={mockSocket}
|
||||
questions={mockQuestions}
|
||||
showSelectedQuestion={jest.fn()}
|
||||
quizMode="teacher"
|
||||
students={mockStudents}
|
||||
/>
|
||||
);
|
||||
|
||||
// Toggle the display of usernames
|
||||
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
|
||||
// Toggle the display of usernames
|
||||
const toggleUsernamesSwitch = screen.getByLabelText('Afficher les noms');
|
||||
|
||||
// Toggle the display of usernames back
|
||||
fireEvent.click(toggleUsernamesSwitch);
|
||||
|
||||
// Calculate the class average
|
||||
const totalGrades = mockStudents.reduce((total, student) => {
|
||||
return total + (student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100);
|
||||
}, 0);
|
||||
const classAverage = totalGrades / mockStudents.length;
|
||||
// Toggle the display of usernames back
|
||||
fireEvent.click(toggleUsernamesSwitch);
|
||||
|
||||
// Check if the class average is displayed correctly
|
||||
const classAverageElements = screen.getAllByText(`${classAverage.toFixed()} %`);
|
||||
const classAverageElement = classAverageElements.find((element) => {
|
||||
return element.closest('td')?.classList.contains('MuiTableCell-footer');
|
||||
});
|
||||
expect(classAverageElement).toBeInTheDocument();
|
||||
});
|
||||
// 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;
|
||||
|
||||
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');
|
||||
// Check if the class average is displayed correctly
|
||||
const classAverageElements = screen.getAllByText(`${classAverage.toFixed()} %`);
|
||||
const classAverageElement = classAverageElements.find((element) => {
|
||||
return element.closest('td')?.classList.contains('MuiTableCell-footer');
|
||||
});
|
||||
expect(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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,14 +12,23 @@ const questions = parse(
|
|||
{
|
||||
=Choice 1
|
||||
~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', () => {
|
||||
const mockHandleOnSubmitAnswer = jest.fn();
|
||||
|
||||
const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => {
|
||||
const TestWrapper = ({ showAnswer, question }: { showAnswer: boolean; question: MultipleChoiceQuestion }) => {
|
||||
const [showAnswerState, setShowAnswerState] = useState(showAnswer);
|
||||
|
||||
const handleOnSubmitAnswer = (answer: AnswerType) => {
|
||||
|
|
@ -38,28 +47,51 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
|||
);
|
||||
};
|
||||
|
||||
const choices = question.choices;
|
||||
const twoChoices = questionWithOneCorrectChoice.choices;
|
||||
const threeChoices = questionWithMultipleCorrectChoices.choices;
|
||||
|
||||
beforeEach(() => {
|
||||
render(<TestWrapper showAnswer={false} />);
|
||||
});
|
||||
test('renders a question (that has only one correct choice) and its choices', () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
|
||||
|
||||
test('renders the question and choices', () => {
|
||||
expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument();
|
||||
choices.forEach((choice) => {
|
||||
expect(screen.getByText(questionWithOneCorrectChoice.formattedStem.text)).toBeInTheDocument();
|
||||
twoChoices.forEach((choice) => {
|
||||
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', () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
|
||||
const submitButton = screen.getByText('Répondre');
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
|
||||
mockHandleOnSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
test('submits the selected answer', () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
|
||||
const choiceButton = screen.getByText('Choice 1').closest('button');
|
||||
if (!choiceButton) throw new Error('Choice button not found');
|
||||
act(() => {
|
||||
|
|
@ -70,10 +102,68 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
|||
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 () => {
|
||||
render(<TestWrapper showAnswer={false} question={questionWithOneCorrectChoice} />);
|
||||
const choiceButton = screen.getByText('Choice 1').closest('button');
|
||||
if (!choiceButton) throw new Error('Choice button not found');
|
||||
|
||||
|
|
@ -89,16 +179,17 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
|||
});
|
||||
|
||||
// Wait for the DOM to update
|
||||
const correctAnswer = screen.getByText("Choice 1").closest('button');
|
||||
expect(correctAnswer).toBeInTheDocument();
|
||||
expect(correctAnswer?.textContent).toContain('✅');
|
||||
const correctAnswer = screen.getByText("Choice 1").closest('button');
|
||||
expect(correctAnswer).toBeInTheDocument();
|
||||
expect(correctAnswer?.textContent).toContain('✅');
|
||||
|
||||
const wrongAnswer1 = screen.getByText("Choice 2").closest('button');
|
||||
expect(wrongAnswer1).toBeInTheDocument();
|
||||
expect(wrongAnswer1?.textContent).toContain('❌');
|
||||
const wrongAnswer1 = screen.getByText("Choice 2").closest('button');
|
||||
expect(wrongAnswer1).toBeInTheDocument();
|
||||
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');
|
||||
if (!choiceButton) throw new Error('Choice button not found');
|
||||
|
||||
|
|
@ -118,5 +209,5 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
|||
expect(wrongAnswer1?.textContent).not.toContain('❌');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ describe('NumericalQuestion Component', () => {
|
|||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled();
|
||||
mockHandleOnSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
it('submits answer correctly', () => {
|
||||
|
|
@ -77,6 +78,7 @@ describe('NumericalQuestion Component', () => {
|
|||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(7);
|
||||
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith([7]);
|
||||
mockHandleOnSubmitAnswer.mockClear();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,23 +29,24 @@ describe('Questions Component', () => {
|
|||
render(<QuestionDisplay question={question} {...sampleProps} />);
|
||||
};
|
||||
|
||||
describe('question type parsing', () => {
|
||||
it('parses true/false question type correctly', () => {
|
||||
expect(sampleTrueFalseQuestion.type).toBe('TF');
|
||||
});
|
||||
// describe('question type parsing', () => {
|
||||
// it('parses true/false question type correctly', () => {
|
||||
// expect(sampleTrueFalseQuestion.type).toBe('TF');
|
||||
// });
|
||||
|
||||
it('parses multiple choice question type correctly', () => {
|
||||
expect(sampleMultipleChoiceQuestion.type).toBe('MC');
|
||||
});
|
||||
// it('parses multiple choice question type correctly', () => {
|
||||
// expect(sampleMultipleChoiceQuestion.type).toBe('MC');
|
||||
// });
|
||||
|
||||
it('parses numerical question type correctly', () => {
|
||||
expect(sampleNumericalQuestion.type).toBe('Numerical');
|
||||
});
|
||||
// it('parses numerical question type correctly', () => {
|
||||
// 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', () => {
|
||||
renderComponent(sampleTrueFalseQuestion);
|
||||
|
||||
|
|
@ -73,7 +74,8 @@ describe('Questions Component', () => {
|
|||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('Choice 1');
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['Choice 1']);
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
it('renders correctly for Numerical question', () => {
|
||||
|
|
@ -93,7 +95,8 @@ describe('Questions Component', () => {
|
|||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7);
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([7]);
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
it('renders correctly for Short Answer question', () => {
|
||||
|
|
@ -117,7 +120,7 @@ describe('Questions Component', () => {
|
|||
const submitButton = screen.getByText('Répondre');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input');
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ describe('ShortAnswerQuestion Component', () => {
|
|||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
it('submits answer correctly', () => {
|
||||
|
|
@ -60,6 +61,7 @@ describe('ShortAnswerQuestion Component', () => {
|
|||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input');
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(['User Input']);
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ describe('TrueFalseQuestion Component', () => {
|
|||
});
|
||||
|
||||
expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
it('submits answer correctly for True', () => {
|
||||
|
|
@ -70,7 +71,8 @@ describe('TrueFalseQuestion Component', () => {
|
|||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(true);
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([true]);
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
it('submits answer correctly for False', () => {
|
||||
|
|
@ -83,7 +85,8 @@ describe('TrueFalseQuestion Component', () => {
|
|||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(false);
|
||||
expect(mockHandleSubmitAnswer).toHaveBeenCalledWith([false]);
|
||||
mockHandleSubmitAnswer.mockClear();
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -112,7 +115,7 @@ describe('TrueFalseQuestion Component', () => {
|
|||
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 () => {
|
||||
const choiceButton = screen.getByText('Vrai').closest('button');
|
||||
if (!choiceButton) throw new Error('Choice button not found');
|
||||
|
||||
|
|
|
|||
359
client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx
Normal file
359
client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -44,7 +44,7 @@ const mockStudents: StudentType[] = [
|
|||
];
|
||||
|
||||
const mockAnswerData: AnswerReceptionFromBackendType = {
|
||||
answer: 'Answer1',
|
||||
answer: ['Answer1'],
|
||||
idQuestion: 1,
|
||||
idUser: '1',
|
||||
username: 'Student 1',
|
||||
|
|
@ -233,7 +233,7 @@ describe('ManageRoom', () => {
|
|||
|
||||
await act(async () => {
|
||||
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');
|
||||
|
|
@ -256,6 +256,7 @@ describe('ManageRoom', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// console.info(consoleSpy.mock.calls);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Received answer from Student 1 for question 1: Answer1'
|
||||
);
|
||||
|
|
@ -294,3 +295,4 @@ describe('ManageRoom', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { QuestionType } from 'src/Types/QuestionType';
|
|||
import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService';
|
||||
|
||||
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}`);
|
||||
|
||||
|
|
@ -23,9 +23,6 @@ const mockSubmitAnswer = jest.fn();
|
|||
const mockDisconnectWebSocket = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear local storage before each test
|
||||
// localStorage.clear();
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<StudentModeQuiz
|
||||
|
|
@ -54,7 +51,7 @@ describe('StudentModeQuiz', () => {
|
|||
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 () => {
|
||||
|
|
@ -65,13 +62,13 @@ describe('StudentModeQuiz', () => {
|
|||
act(() => {
|
||||
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'});
|
||||
expect(firstButtonA).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();
|
||||
|
||||
// Navigate to the next question
|
||||
|
|
@ -87,12 +84,12 @@ describe('StudentModeQuiz', () => {
|
|||
});
|
||||
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
|
||||
// const buttonA = screen.getByRole("button", {name: '✅ A Option A'});
|
||||
const buttonA = screen.getByRole("button", {name: 'A Option A'});
|
||||
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'});
|
||||
expect(buttonB).toBeInTheDocument();
|
||||
// // "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('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();
|
||||
// });
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ describe('TeacherModeQuiz', () => {
|
|||
act(() => {
|
||||
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', () => {
|
||||
|
|
@ -74,7 +75,8 @@ describe('TeacherModeQuiz', () => {
|
|||
act(() => {
|
||||
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;
|
||||
// Navigate to the next question by re-rendering with new props
|
||||
act(() => {
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ type AnswerFeedbackOptions = TemplateOptions & Pick<TextChoice, 'formattedFeedba
|
|||
interface AnswerWeightOptions extends TemplateOptions {
|
||||
weight: TextChoice['weight'];
|
||||
}
|
||||
|
||||
// careful -- this template is re-used by True/False questions!
|
||||
export default function MultipleChoiceAnswersTemplate({ choices }: MultipleChoiceAnswerOptions) {
|
||||
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${
|
||||
isMultipleAnswer ? ` ou plusieurs` : ``
|
||||
hasManyCorrectChoices ? ` ou plusieurs` : ``
|
||||
}:</span>`;
|
||||
const result = choices
|
||||
.map(({ weight, isCorrect, formattedText, formattedFeedback }) => {
|
||||
|
|
@ -32,12 +32,12 @@ export default function MultipleChoiceAnswersTemplate({ choices }: MultipleChoic
|
|||
const inputId = `id${nanoid(6)}`;
|
||||
|
||||
const isPositiveWeight = (weight != undefined) && (weight > 0);
|
||||
const isCorrectOption = isMultipleAnswer ? isPositiveWeight : isCorrect;
|
||||
const isCorrectOption = hasManyCorrectChoices ? isPositiveWeight || isCorrect : isCorrect;
|
||||
|
||||
return `
|
||||
<div class='multiple-choice-answers-container'>
|
||||
<input class="gift-input" type="${
|
||||
isMultipleAnswer ? 'checkbox' : 'radio'
|
||||
hasManyCorrectChoices ? 'checkbox' : 'radio'
|
||||
}" id="${inputId}" name="${id}">
|
||||
${AnswerWeight({ weight: weight })}
|
||||
<label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}">
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
|
|||
borderWidth: 1,
|
||||
borderColor: 'rgba(224, 224, 224, 1)',
|
||||
fontWeight: 'bold',
|
||||
color: 'rgba(0, 0, 0)'
|
||||
color: 'rgba(0, 0, 0)',
|
||||
}}
|
||||
>
|
||||
{students.length > 0
|
||||
|
|
@ -67,7 +67,7 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
|
|||
borderColor: 'rgba(224, 224, 224, 1)',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1rem',
|
||||
color: 'rgba(0, 0, 0)'
|
||||
color: 'rgba(0, 0, 0)',
|
||||
}}
|
||||
>
|
||||
{students.length > 0 ? `${classAverage.toFixed()} %` : '-'}
|
||||
|
|
@ -76,4 +76,4 @@ const LiveResultsTableFooter: React.FC<LiveResultsFooterProps> = ({
|
|||
</TableFooter>
|
||||
);
|
||||
};
|
||||
export default LiveResultsTableFooter;
|
||||
export default LiveResultsTableFooter;
|
||||
|
|
|
|||
|
|
@ -15,76 +15,115 @@ interface Props {
|
|||
|
||||
const MultipleChoiceQuestionDisplay: React.FC<Props> = (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;
|
||||
if(handleOnSubmitAnswer === undefined){
|
||||
if (handleOnSubmitAnswer === undefined) {
|
||||
disableButton = true;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (passedAnswer !== undefined) {
|
||||
setAnswer(passedAnswer);
|
||||
}
|
||||
}, [passedAnswer]);
|
||||
console.log('MultipleChoiceQuestionDisplay: passedAnswer', JSON.stringify(passedAnswer));
|
||||
if (passedAnswer !== undefined) {
|
||||
setAnswer(passedAnswer);
|
||||
} else {
|
||||
setAnswer([]);
|
||||
}
|
||||
}, [passedAnswer, question.id]);
|
||||
|
||||
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 alphabet = alpha.map((x) => String.fromCharCode(x));
|
||||
return (
|
||||
|
||||
return (
|
||||
<div className="question-container">
|
||||
<div className="question content">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
||||
</div>
|
||||
<div className="choices-wrapper mb-1">
|
||||
|
||||
{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 (
|
||||
<div key={choice.formattedText.text + i} className="choice-container">
|
||||
<Button
|
||||
variant="text"
|
||||
className="button-wrapper"
|
||||
disabled={disableButton}
|
||||
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}>
|
||||
{showAnswer? (<div> {(choice.isCorrect ? '✅' : '❌')}</div>)
|
||||
:``}
|
||||
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}
|
||||
>
|
||||
{showAnswer ? (
|
||||
<div>{choice.isCorrect ? '✅' : '❌'}</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<div className={`circle ${selected}`}>{alphabet[i]}</div>
|
||||
<div className={`answer-text ${selected}`}>
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedText) }} />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: FormattedTextTemplate(choice.formattedText),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{choice.formattedFeedback && showAnswer && (
|
||||
<div className="feedback-container mb-1 mt-1/2">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedFeedback) }} />
|
||||
</div>
|
||||
)}
|
||||
<div className="feedback-container mb-1 mt-1/2">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: FormattedTextTemplate(choice.formattedFeedback),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{question.formattedGlobalFeedback && showAnswer && (
|
||||
<div className="global-feedback mb-2">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} />
|
||||
</div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: FormattedTextTemplate(question.formattedGlobalFeedback),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showAnswer && handleOnSubmitAnswer && (
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
answer !== "" && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
|
||||
answer.length > 0 && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
|
||||
}
|
||||
disabled={answer === '' || answer === null}
|
||||
disabled={answer.length === 0}
|
||||
>
|
||||
Répondre
|
||||
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ interface Props {
|
|||
const NumericalQuestionDisplay: React.FC<Props> = (props) => {
|
||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } =
|
||||
props;
|
||||
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || '');
|
||||
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
|
||||
const correctAnswers = question.choices;
|
||||
let correctAnswer = '';
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
|
|||
id={question.formattedStem.text}
|
||||
name={question.formattedStem.text}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAnswer(e.target.valueAsNumber);
|
||||
setAnswer([e.target.valueAsNumber]);
|
||||
}}
|
||||
inputProps={{ 'data-testid': 'number-input' }}
|
||||
/>
|
||||
|
|
@ -87,7 +87,7 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
|
|||
handleOnSubmitAnswer &&
|
||||
handleOnSubmitAnswer(answer)
|
||||
}
|
||||
disabled={answer === "" || isNaN(answer as number)}
|
||||
disabled={answer === undefined || answer === null || isNaN(answer[0] as number)}
|
||||
>
|
||||
Répondre
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ interface Props {
|
|||
const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
|
||||
|
||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props;
|
||||
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || '');
|
||||
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
|
||||
|
||||
useEffect(() => {
|
||||
if (passedAnswer !== undefined) {
|
||||
|
|
@ -58,7 +58,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
|
|||
id={question.formattedStem.text}
|
||||
name={question.formattedStem.text}
|
||||
onChange={(e) => {
|
||||
setAnswer(e.target.value);
|
||||
setAnswer([e.target.value]);
|
||||
}}
|
||||
disabled={showAnswer}
|
||||
aria-label="short-answer-input"
|
||||
|
|
@ -72,7 +72,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
|
|||
handleOnSubmitAnswer &&
|
||||
handleOnSubmitAnswer(answer)
|
||||
}
|
||||
disabled={answer === null || answer === ''}
|
||||
disabled={answer === null || answer === undefined || answer.length === 0}
|
||||
>
|
||||
Répondre
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// TrueFalseQuestion.tsx
|
||||
import React, { useState,useEffect } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import '../questionStyle.css';
|
||||
import { Button } from '@mui/material';
|
||||
import { TrueFalseQuestion } from 'gift-pegjs';
|
||||
|
|
@ -8,37 +8,37 @@ import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
|||
|
||||
interface Props {
|
||||
question: TrueFalseQuestion;
|
||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||
handleOnSubmitAnswer?: (answer: AnswerType) => void;
|
||||
showAnswer?: boolean;
|
||||
passedAnswer?: AnswerType;
|
||||
}
|
||||
|
||||
const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer} =
|
||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } =
|
||||
props;
|
||||
|
||||
const [answer, setAnswer] = useState<boolean | undefined>(() => {
|
||||
|
||||
if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
|
||||
return passedAnswer[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
let disableButton = false;
|
||||
if(handleOnSubmitAnswer === undefined){
|
||||
if (handleOnSubmitAnswer === undefined) {
|
||||
disableButton = true;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log("passedAnswer", answer);
|
||||
if (passedAnswer === true || passedAnswer === false) {
|
||||
setAnswer(passedAnswer);
|
||||
} else {
|
||||
setAnswer(undefined);
|
||||
}
|
||||
}, [passedAnswer, question.id]);
|
||||
|
||||
const [answer, setAnswer] = useState<boolean | undefined>(() => {
|
||||
|
||||
if (passedAnswer === true || passedAnswer === false) {
|
||||
return passedAnswer;
|
||||
console.log("passedAnswer", passedAnswer);
|
||||
if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
|
||||
setAnswer(passedAnswer[0]);
|
||||
} else {
|
||||
setAnswer(undefined);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}, [passedAnswer, question.id]);
|
||||
|
||||
const handleOnClickAnswer = (choice: boolean) => {
|
||||
setAnswer(choice);
|
||||
|
|
@ -49,7 +49,7 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
|||
return (
|
||||
<div className="question-container">
|
||||
<div className="question content">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
|
||||
</div>
|
||||
<div className="choices-wrapper mb-1">
|
||||
<Button
|
||||
|
|
@ -58,15 +58,15 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
|||
fullWidth
|
||||
disabled={disableButton}
|
||||
>
|
||||
{showAnswer? (<div> {(question.isTrue ? '✅' : '❌')}</div>):``}
|
||||
{showAnswer ? (<div> {(question.isTrue ? '✅' : '❌')}</div>) : ``}
|
||||
<div className={`circle ${selectedTrue}`}>V</div>
|
||||
<div className={`answer-text ${selectedTrue}`}>Vrai</div>
|
||||
|
||||
{showAnswer && answer && question.trueFormattedFeedback && (
|
||||
<div className="true-feedback mb-2">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} />
|
||||
</div>
|
||||
)}
|
||||
<div className="true-feedback mb-2">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.trueFormattedFeedback) }} />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="button-wrapper"
|
||||
|
|
@ -75,15 +75,15 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
|||
disabled={disableButton}
|
||||
|
||||
>
|
||||
{showAnswer? (<div> {(!question.isTrue ? '✅' : '❌')}</div>):``}
|
||||
{showAnswer ? (<div> {(!question.isTrue ? '✅' : '❌')}</div>) : ``}
|
||||
<div className={`circle ${selectedFalse}`}>F</div>
|
||||
<div className={`answer-text ${selectedFalse}`}>Faux</div>
|
||||
|
||||
{showAnswer && !answer && question.falseFormattedFeedback && (
|
||||
<div className="false-feedback mb-2">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.falseFormattedFeedback) }} />
|
||||
</div>
|
||||
)}
|
||||
<div className="false-feedback mb-2">
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.falseFormattedFeedback) }} />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{question.formattedGlobalFeedback && showAnswer && (
|
||||
|
|
@ -95,8 +95,7 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
|||
<Button
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
|
||||
|
||||
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer([answer])
|
||||
}
|
||||
disabled={answer === undefined}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import LoginContainer from 'src/components/LoginContainer/LoginContainer'
|
|||
|
||||
import ApiService from '../../../services/ApiService'
|
||||
|
||||
export type AnswerType = string | number | boolean;
|
||||
export type AnswerType = Array<string | number | boolean>;
|
||||
|
||||
const JoinRoom: React.FC = () => {
|
||||
const [roomName, setRoomName] = useState('');
|
||||
|
|
@ -39,9 +39,8 @@ const JoinRoom: React.FC = () => {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// init the answers array, one for each question
|
||||
setAnswers(Array(questions.length).fill({} as AnswerSubmissionToBackendType));
|
||||
console.log(`JoinRoom: useEffect: questions: ${JSON.stringify(questions)}`);
|
||||
setAnswers(questions ? Array(questions.length).fill({} as AnswerSubmissionToBackendType) : []);
|
||||
}, [questions]);
|
||||
|
||||
|
||||
|
|
@ -64,6 +63,7 @@ const JoinRoom: React.FC = () => {
|
|||
console.log('on(launch-teacher-mode): Received launch-teacher-mode:', questions);
|
||||
setQuizMode('teacher');
|
||||
setIsWaitingForTeacher(true);
|
||||
setQuestions([]); // clear out from last time (in case quiz is repeated)
|
||||
setQuestions(questions);
|
||||
// wait for next-question
|
||||
});
|
||||
|
|
@ -72,6 +72,7 @@ const JoinRoom: React.FC = () => {
|
|||
|
||||
setQuizMode('student');
|
||||
setIsWaitingForTeacher(false);
|
||||
setQuestions([]); // clear out from last time (in case quiz is repeated)
|
||||
setQuestions(questions);
|
||||
setQuestion(questions[0]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs';
|
||||
import {
|
||||
isSimpleNumericalAnswer,
|
||||
isRangeNumericalAnswer,
|
||||
isHighLowNumericalAnswer
|
||||
} from 'gift-pegjs/typeGuards';
|
||||
import { BaseQuestion, parse, Question } from 'gift-pegjs';
|
||||
import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
|
||||
import webSocketService, {
|
||||
AnswerReceptionFromBackendType
|
||||
|
|
@ -24,7 +19,7 @@ import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
|
|||
import ApiService from '../../../services/ApiService';
|
||||
import { QuestionType } from 'src/Types/QuestionType';
|
||||
import { Button } from '@mui/material';
|
||||
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
|
||||
import { checkIfIsCorrect } from './useRooms';
|
||||
|
||||
const ManageRoom: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -36,8 +31,40 @@ const ManageRoom: React.FC = () => {
|
|||
const [quizMode, setQuizMode] = useState<'teacher' | 'student'>('teacher');
|
||||
const [connectingError, setConnectingError] = useState<string>('');
|
||||
const [currentQuestion, setCurrentQuestion] = useState<QuestionType | undefined>(undefined);
|
||||
const [quizStarted, setQuizStarted] = useState(false);
|
||||
const [quizStarted, setQuizStarted] = useState<boolean>(false);
|
||||
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(() => {
|
||||
const verifyLogin = async () => {
|
||||
|
|
@ -110,6 +137,17 @@ const ManageRoom: React.FC = () => {
|
|||
const roomNameUpper = roomName.toUpperCase();
|
||||
setFormattedRoomName(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', () => {
|
||||
webSocketService.createRoom(roomNameUpper);
|
||||
});
|
||||
|
|
@ -124,23 +162,9 @@ const ManageRoom: React.FC = () => {
|
|||
});
|
||||
|
||||
socket.on('user-joined', (student: StudentType) => {
|
||||
console.log(`Student joined: name = ${student.name}, id = ${student.id}, quizMode = ${quizMode}, quizStarted = ${quizStarted}`);
|
||||
|
||||
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);
|
||||
}
|
||||
setNewlyConnectedUser(student);
|
||||
});
|
||||
|
||||
socket.on('join-failure', (message) => {
|
||||
setConnectingError(message);
|
||||
setSocket(null);
|
||||
|
|
@ -286,21 +310,19 @@ const ManageRoom: React.FC = () => {
|
|||
};
|
||||
|
||||
const launchQuiz = () => {
|
||||
setQuizStarted(true);
|
||||
if (!socket || !formattedRoomName || !quiz?.content || quiz?.content.length === 0) {
|
||||
// TODO: This error happens when token expires! Need to handle it properly
|
||||
console.log(
|
||||
`Error launching quiz. socket: ${socket}, roomName: ${formattedRoomName}, quiz: ${quiz}`
|
||||
);
|
||||
setQuizStarted(true);
|
||||
|
||||
return;
|
||||
}
|
||||
console.log(`Launching quiz in ${quizMode} mode...`);
|
||||
switch (quizMode) {
|
||||
case 'student':
|
||||
setQuizStarted(true);
|
||||
return launchStudentMode();
|
||||
case 'teacher':
|
||||
setQuizStarted(true);
|
||||
return launchTeacherMode();
|
||||
}
|
||||
};
|
||||
|
|
@ -319,63 +341,6 @@ const ManageRoom: React.FC = () => {
|
|||
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) {
|
||||
return (
|
||||
<div className="center">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
import { useContext } from 'react';
|
||||
import { RoomType } from 'src/Types/RoomType';
|
||||
import { createContext } from 'react';
|
||||
|
||||
//import { RoomContext } from './RoomContext';
|
||||
import { MultipleNumericalAnswer, NumericalAnswer, ParsedGIFTQuestion } from 'gift-pegjs';
|
||||
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 = {
|
||||
rooms: RoomType[];
|
||||
|
|
@ -18,3 +25,137 @@ export const useRooms = () => {
|
|||
if (!context) throw new Error('useRooms must be used within a RoomProvider');
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue