fix bugs that showed in dev

This commit is contained in:
C. Fuhrman 2025-03-21 11:05:16 -04:00
parent b92f81cc0e
commit 13136b9e91
6 changed files with 148 additions and 55 deletions

View file

@ -12,14 +12,23 @@ const questions = parse(
{ {
=Choice 1 =Choice 1
~Choice 2 ~Choice 2
}`) as MultipleChoiceQuestion[]; }
::Sample Question 2:: Question stem
{
=Choice 1
=Choice 2
~Choice 3
}
`) as MultipleChoiceQuestion[];
const question = questions[0]; const questionWithOneCorrectChoice = questions[0];
const questionWithMultipleCorrectChoices = questions[1];
describe('MultipleChoiceQuestionDisplay', () => { describe('MultipleChoiceQuestionDisplay', () => {
const mockHandleOnSubmitAnswer = jest.fn(); const mockHandleOnSubmitAnswer = jest.fn();
const TestWrapper = ({ showAnswer }: { showAnswer: boolean }) => { const TestWrapper = ({ showAnswer, question }: { showAnswer: boolean; question: MultipleChoiceQuestion }) => {
const [showAnswerState, setShowAnswerState] = useState(showAnswer); const [showAnswerState, setShowAnswerState] = useState(showAnswer);
const handleOnSubmitAnswer = (answer: AnswerType) => { const handleOnSubmitAnswer = (answer: AnswerType) => {
@ -38,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('❌');
}); });
}); });

View file

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

View file

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

View file

@ -17,7 +17,7 @@ interface AnswerWeightOptions extends TemplateOptions {
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 isMultipleAnswer = choices.filter(({ isCorrect }) => isCorrect === true).length != 0;
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` : `` isMultipleAnswer ? ` ou plusieurs` : ``
@ -32,7 +32,7 @@ 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 = isMultipleAnswer ? isPositiveWeight || isCorrect : isCorrect;
return ` return `
<div class='multiple-choice-answers-container'> <div class='multiple-choice-answers-container'>

View file

@ -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">

View file

@ -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') {