mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Compare commits
7 commits
7afc81942a
...
b02da32083
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b02da32083 | ||
|
|
ee7a7a0544 | ||
|
|
8c57a8759f | ||
|
|
22a2754e31 | ||
|
|
3d9015febd | ||
|
|
fc15d2c3bd | ||
|
|
13136b9e91 |
11 changed files with 208 additions and 83 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/backend-deploy.yml)
|
||||||
[](https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir/actions/workflows/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.
|
## 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
|
||||||
|
|
|
||||||
|
|
@ -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' }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -733,7 +733,7 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
|
||||||
|
|
||||||
<div class='multiple-choice-answers-container'>
|
<div class='multiple-choice-answers-container'>
|
||||||
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
<input class="gift-input" type="radio" id="idmocked-id" name="idmocked-id">
|
||||||
<span class="answer-weight-container answer-positive-weight">1%</span>
|
|
||||||
<label style="
|
<label style="
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.2em 0 0.2em 0;
|
padding: 0.2em 0 0.2em 0;
|
||||||
|
|
@ -742,15 +742,15 @@ exports[`MultipleChoice snapshot test with katex 1`] = `
|
||||||
" for="idmocked-id">
|
" for="idmocked-id">
|
||||||
Choice 2
|
Choice 2
|
||||||
</label>
|
</label>
|
||||||
<svg data-testid="correct-icon" style="
|
<svg data-testid="incorrect-icon" style="
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 0.1rem;
|
margin-left: 0.1rem;
|
||||||
margin-right: 0.2rem;
|
margin-right: 0.2rem;
|
||||||
|
|
||||||
width: 1em;
|
width: 0.75em;
|
||||||
color: hsl(120, 39%, 54%);
|
color: hsl(2, 64%, 58%);
|
||||||
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"></path></svg>
|
" role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>
|
||||||
<span class="feedback-container">Correct!</span>
|
<span class="feedback-container">Correct!</span>
|
||||||
</input>
|
</input>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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,20 +47,41 @@ 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);
|
||||||
|
|
@ -61,6 +91,7 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
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(() => {
|
||||||
|
|
@ -75,12 +106,43 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
||||||
mockHandleOnSubmitAnswer.mockClear();
|
mockHandleOnSubmitAnswer.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('submits multiple selected answers', () => {
|
|
||||||
|
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 choiceButton1 = screen.getByText('Choice 1').closest('button');
|
||||||
const choiceButton2 = screen.getByText('Choice 2').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');
|
if (!choiceButton1 || !choiceButton2) throw new Error('Choice buttons not found');
|
||||||
|
|
||||||
// Simulate selecting multiple answers
|
// Simulate selecting multiple answers
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.click(choiceButton1);
|
fireEvent.click(choiceButton1);
|
||||||
|
|
@ -88,19 +150,20 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.click(choiceButton2);
|
fireEvent.click(choiceButton2);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate submitting the answers
|
// Simulate submitting the answers
|
||||||
const submitButton = screen.getByText('Répondre');
|
const submitButton = screen.getByText('Répondre');
|
||||||
act(() => {
|
act(() => {
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify that the mockHandleOnSubmitAnswer function is called with both answers
|
// Verify that the mockHandleOnSubmitAnswer function is called with both answers
|
||||||
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(['Choice 1', 'Choice 2']);
|
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(['Choice 1', 'Choice 2']);
|
||||||
mockHandleOnSubmitAnswer.mockClear();
|
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');
|
||||||
|
|
||||||
|
|
@ -116,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');
|
||||||
|
|
||||||
|
|
@ -145,5 +209,5 @@ describe('MultipleChoiceQuestionDisplay', () => {
|
||||||
expect(wrongAnswer1?.textContent).not.toContain('❌');
|
expect(wrongAnswer1?.textContent).not.toContain('❌');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ describe('checkIfIsCorrect', () => {
|
||||||
expect(result).toBe(false);
|
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', () => {
|
test('returns false when no answers are selected', () => {
|
||||||
const answer: AnswerType = [];
|
const answer: AnswerType = [];
|
||||||
const result = checkIfIsCorrect(answer, 1, mockQuestions);
|
const result = checkIfIsCorrect(answer, 1, mockQuestions);
|
||||||
|
|
|
||||||
|
|
@ -120,27 +120,29 @@ describe('StudentModeQuiz', () => {
|
||||||
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
expect(screen.getByText('Répondre')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allows multiple answers to be selected for a question', async () => {
|
// le test suivant est fait dans MultipleChoiceQuestionDisplay.test.tsx
|
||||||
// Simulate selecting multiple answers
|
// test('allows multiple answers to be selected for a question', async () => {
|
||||||
act(() => {
|
// // Simulate selecting multiple answers
|
||||||
fireEvent.click(screen.getByText('Option A'));
|
// act(() => {
|
||||||
});
|
// fireEvent.click(screen.getByText('Option A'));
|
||||||
act(() => {
|
// });
|
||||||
fireEvent.click(screen.getByText('Option B'));
|
// act(() => {
|
||||||
});
|
// fireEvent.click(screen.getByText('Option B'));
|
||||||
|
// });
|
||||||
|
|
||||||
// Simulate submitting the answers
|
// // Simulate submitting the answers
|
||||||
act(() => {
|
// act(() => {
|
||||||
fireEvent.click(screen.getByText('Répondre'));
|
// fireEvent.click(screen.getByText('Répondre'));
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Verify that the mockSubmitAnswer function is called with both answers
|
// // Verify that the mockSubmitAnswer function is called with both answers
|
||||||
expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A', 'Option B'], 1);
|
// 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();
|
||||||
|
// });
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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}">
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,14 @@ 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) {
|
||||||
|
|
@ -23,19 +30,31 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('MultipleChoiceQuestionDisplay: passedAnswer', JSON.stringify(passedAnswer));
|
||||||
if (passedAnswer !== undefined) {
|
if (passedAnswer !== undefined) {
|
||||||
setAnswer(passedAnswer);
|
setAnswer(passedAnswer);
|
||||||
|
} else {
|
||||||
|
setAnswer([]);
|
||||||
}
|
}
|
||||||
}, [passedAnswer]);
|
}, [passedAnswer, question.id]);
|
||||||
|
|
||||||
const handleOnClickAnswer = (choice: string) => {
|
const handleOnClickAnswer = (choice: string) => {
|
||||||
setAnswer((prevAnswer) => {
|
setAnswer((prevAnswer) => {
|
||||||
if (prevAnswer.includes(choice)) {
|
console.log(`handleOnClickAnswer -- setAnswer(): prevAnswer: ${prevAnswer}, choice: ${choice}`);
|
||||||
// Remove the choice if it's already selected
|
const correctAnswersCount = question.choices.filter((c) => c.isCorrect).length;
|
||||||
return prevAnswer.filter((selected) => selected !== choice);
|
|
||||||
|
if (correctAnswersCount === 1) {
|
||||||
|
// If only one correct answer, replace the current selection
|
||||||
|
return prevAnswer.includes(choice) ? [] : [choice];
|
||||||
} else {
|
} else {
|
||||||
// Add the choice if it's not already selected
|
// Allow multiple selections if there are multiple correct answers
|
||||||
return [...prevAnswer, choice];
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -50,6 +69,7 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="choices-wrapper mb-1">
|
<div className="choices-wrapper mb-1">
|
||||||
{question.choices.map((choice, i) => {
|
{question.choices.map((choice, i) => {
|
||||||
|
console.log(`answer: ${answer}, choice: ${choice.formattedText.text}`);
|
||||||
const selected = answer.includes(choice.formattedText.text) ? 'selected' : '';
|
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">
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,15 @@ 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;
|
||||||
|
|
@ -31,15 +40,6 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
}
|
}
|
||||||
}, [passedAnswer, question.id]);
|
}, [passedAnswer, question.id]);
|
||||||
|
|
||||||
const [answer, setAnswer] = useState<boolean | undefined>(() => {
|
|
||||||
|
|
||||||
if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
|
|
||||||
return passedAnswer[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleOnClickAnswer = (choice: boolean) => {
|
const handleOnClickAnswer = (choice: boolean) => {
|
||||||
setAnswer(choice);
|
setAnswer(choice);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -55,14 +55,15 @@ export function checkIfIsCorrect(
|
||||||
(!question.isTrue && simpleAnswerText == 'false')
|
(!question.isTrue && simpleAnswerText == 'false')
|
||||||
);
|
);
|
||||||
} else if (question.type === 'MC') {
|
} else if (question.type === 'MC') {
|
||||||
const correctAnswers = question.choices.filter((choice) => choice.isCorrect
|
const correctChoices = question.choices.filter((choice) => choice.isCorrect
|
||||||
/* || (choice.weight && choice.weight > 0)*/ // handle weighted answers
|
/* || (choice.weight && choice.weight > 0)*/ // handle weighted answers
|
||||||
);
|
);
|
||||||
const multipleAnswers = Array.isArray(answer) ? answer : [answer as string];
|
const multipleAnswers = Array.isArray(answer) ? answer : [answer as string];
|
||||||
if (correctAnswers.length === 0) {
|
if (correctChoices.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return correctAnswers.every(
|
// 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)
|
(choice) => multipleAnswers.includes(choice.formattedText.text)
|
||||||
);
|
);
|
||||||
} else if (question.type === 'Numerical') {
|
} else if (question.type === 'Numerical') {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue