diff --git a/client/src/__tests__/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.test.tsx index 639537a..1ebc451 100644 --- a/client/src/__tests__/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.test.tsx @@ -1,44 +1,21 @@ -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { NumericalQuestion, parse, ParsedGIFTQuestion } from 'gift-pegjs'; -import { MemoryRouter } from 'react-router-dom'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { parse, NumericalQuestion } from 'gift-pegjs'; +import React from 'react'; import NumericalQuestionDisplay from 'src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay'; -const questions = parse( - ` - ::Sample Question 1:: Question stem - { - #5..10 - }` -) as ParsedGIFTQuestion[]; - -const question = questions[0] as NumericalQuestion; - -describe('NumericalQuestion parse', () => { - const q = questions[0]; - - it('The question is Numerical', () => { - expect(q.type).toBe('Numerical'); - }); -}); - -describe('NumericalQuestion Component', () => { +describe('NumericalQuestionDisplay Component', () => { const mockHandleOnSubmitAnswer = jest.fn(); + const question = + parse('::Sample Numerical Question:: What is 2+2? {#4}')[0] as NumericalQuestion; const sampleProps = { - question: question, handleOnSubmitAnswer: mockHandleOnSubmitAnswer, showAnswer: false }; beforeEach(() => { - render( - - - ); + render(); }); it('renders correctly', () => { @@ -55,13 +32,13 @@ describe('NumericalQuestion Component', () => { expect(inputElement.value).toBe('7'); }); - it('Submit button should be disable if nothing is entered', () => { + it('Submit button should be disabled if nothing is entered', () => { const submitButton = screen.getByText('Répondre'); expect(submitButton).toBeDisabled(); }); - it('not submited answer if nothing is entered', () => { + it('does not submit answer if nothing is entered', () => { const submitButton = screen.getByText('Répondre'); fireEvent.click(submitButton); @@ -79,4 +56,52 @@ describe('NumericalQuestion Component', () => { expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(7); }); -}); + + it('renders correctly with the correct answer shown', () => { + render(); + expect(screen.getByText('Réponse(s) accepté(es):')).toBeInTheDocument(); + expect(screen.getAllByText('4')).toHaveLength(2); + }); + + it('handles input change and checks if the answer is correct', () => { + const inputElement = screen.getByTestId('number-input') as HTMLInputElement; + + fireEvent.change(inputElement, { target: { value: '4' } }); + + expect(inputElement.value).toBe('4'); + + const submitButton = screen.getByText('Répondre'); + fireEvent.click(submitButton); + + expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(4); + }); + + it('submits the correct answer', () => { + const inputElement = screen.getByTestId('number-input') as HTMLInputElement; + + fireEvent.change(inputElement, { target: { value: '4' } }); + + const submitButton = screen.getByText('Répondre'); + fireEvent.click(submitButton); + + expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(4); + }); + + it('submits an incorrect answer', () => { + const inputElement = screen.getByTestId('number-input') as HTMLInputElement; + + fireEvent.change(inputElement, { target: { value: '5' } }); + + const submitButton = screen.getByText('Répondre'); + fireEvent.click(submitButton); + + expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(5); + }); + + it('displays feedback when the answer is shown', () => { + render(); + expect(screen.getByText('❌ Incorrect!')).toBeInTheDocument(); + expect(screen.getByText('Réponse(s) accepté(es):')).toBeInTheDocument(); + expect(screen.getByText('5')).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/client/src/__tests__/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.test.tsx b/client/src/__tests__/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.test.tsx index c4326eb..ec3fd84 100644 --- a/client/src/__tests__/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.test.tsx +++ b/client/src/__tests__/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.test.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { render, screen, fireEvent, within } from '@testing-library/react'; import '@testing-library/jest-dom'; +import { render, screen, fireEvent } from '@testing-library/react'; import { parse, ShortAnswerQuestion } from 'gift-pegjs'; +import React from 'react'; import ShortAnswerQuestionDisplay from 'src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay'; describe('ShortAnswerQuestion Component', () => { @@ -20,28 +20,26 @@ describe('ShortAnswerQuestion Component', () => { it('renders correctly', () => { expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument(); - const container = screen.getByLabelText('short-answer-input'); - const inputElement = within(container).getByRole('textbox') as HTMLInputElement; + const inputElement = screen.getByRole('textbox') as HTMLInputElement; expect(inputElement).toBeInTheDocument(); expect(screen.getByText('Répondre')).toBeInTheDocument(); }); it('handles input change correctly', () => { - const container = screen.getByLabelText('short-answer-input'); - const inputElement = within(container).getByRole('textbox') as HTMLInputElement; + const inputElement = screen.getByRole('textbox') as HTMLInputElement; fireEvent.change(inputElement, { target: { value: 'User Input' } }); expect(inputElement.value).toBe('User Input'); }); - it('Submit button should be disable if nothing is entered', () => { + it('Submit button should be disabled if nothing is entered', () => { const submitButton = screen.getByText('Répondre'); expect(submitButton).toBeDisabled(); }); - it('not submitted answer if nothing is entered', () => { + it('does not submit answer if nothing is entered', () => { const submitButton = screen.getByText('Répondre'); fireEvent.click(submitButton); @@ -49,17 +47,51 @@ describe('ShortAnswerQuestion Component', () => { expect(mockHandleSubmitAnswer).not.toHaveBeenCalled(); }); - it('submits answer correctly', () => { - const container = screen.getByLabelText('short-answer-input'); - const inputElement = within(container).getByRole('textbox') as HTMLInputElement; + it('renders correctly with the correct answer shown', () => { + render(); + expect(screen.getByText('Réponse(s) accepté(es):')).toBeInTheDocument(); + expect(screen.getByText('Correct Answer')).toBeInTheDocument(); + }); + + it('handles input change and checks if the answer is correct', () => { + const inputElement = screen.getByRole('textbox') as HTMLInputElement; + + fireEvent.change(inputElement, { target: { value: 'Correct Answer' } }); + + expect(inputElement.value).toBe('Correct Answer'); - // const inputElement = screen.getByRole('textbox', { name: 'short-answer-input'}) as HTMLInputElement; const submitButton = screen.getByText('Répondre'); - - fireEvent.change(inputElement, { target: { value: 'User Input' } }); - fireEvent.click(submitButton); - expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('User Input'); + expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('Correct Answer'); }); -}); + + it('submits the correct answer', () => { + const inputElement = screen.getByRole('textbox') as HTMLInputElement; + + fireEvent.change(inputElement, { target: { value: 'Correct Answer' } }); + + const submitButton = screen.getByText('Répondre'); + fireEvent.click(submitButton); + + expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('Correct Answer'); + }); + + it('submits an incorrect answer', () => { + const inputElement = screen.getByRole('textbox') as HTMLInputElement; + + fireEvent.change(inputElement, { target: { value: 'Incorrect Answer' } }); + + const submitButton = screen.getByText('Répondre'); + fireEvent.click(submitButton); + + expect(mockHandleSubmitAnswer).toHaveBeenCalledWith('Incorrect Answer'); + }); + + it('displays feedback when the answer is shown', () => { + render(); + expect(screen.getByText('❌ Incorrect!')).toBeInTheDocument(); + expect(screen.getByText('Réponse(s) accepté(es):')).toBeInTheDocument(); + expect(screen.getByText('Incorrect Answer')).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx index 6a4ec59..90c963a 100644 --- a/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx +++ b/client/src/__tests__/pages/Student/TeacherModeQuiz/TeacherModeQuiz.test.tsx @@ -66,6 +66,18 @@ describe('TeacherModeQuiz', () => { expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); }); + + test('handles shows feedback for answered question', () => { + act(() => { + fireEvent.click(screen.getByText('Option B')); + }); + act(() => { + fireEvent.click(screen.getByText('Répondre')); + }); + expect(mockSubmitAnswer).toHaveBeenCalledWith('Option B', 1) + expect(screen.getByText('❌ Incorrect!')).toBeInTheDocument(); + }); + test('handles shows feedback for an already answered question', () => { // Answer the first question act(() => { @@ -106,7 +118,7 @@ describe('TeacherModeQuiz', () => { }); // Check if the feedback dialog is shown again - expect(screen.getByText('Rétroaction')).toBeInTheDocument(); + expect(screen.getByText('❌ Incorrect!')).toBeInTheDocument(); }); test('handles disconnect button click', () => { diff --git a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx index df46193..fffae6c 100644 --- a/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay.tsx @@ -16,19 +16,29 @@ interface Props { const MultipleChoiceQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; const [answer, setAnswer] = useState(passedAnswer || ''); + const [isGoodAnswer, setisGoodAnswer] = useState(false); let disableButton = false; - if(handleOnSubmitAnswer === undefined){ + if (handleOnSubmitAnswer === undefined) { disableButton = true; } useEffect(() => { - if (passedAnswer !== undefined) { - setAnswer(passedAnswer); - } + if (passedAnswer !== undefined) { + setAnswer(passedAnswer); + } }, [passedAnswer]); + useEffect(() => { + checkAnswer(); + }, [answer]); + + const checkAnswer = () => { + const isCorrect = question.choices.some((choice) => choice.isCorrect && choice.formattedText.text === answer as string); + setisGoodAnswer(isCorrect); + }; + const handleOnClickAnswer = (choice: string) => { setAnswer(choice); }; @@ -36,12 +46,24 @@ const MultipleChoiceQuestionDisplay: React.FC = (props) => { const alphabet = alpha.map((x) => String.fromCharCode(x)); return ( -
+ +
+ {showAnswer && ( +
+
+ {isGoodAnswer ? '✅ Correct! ' : '❌ Incorrect!'} +
+
+ Question : +
+ +
+ )}
- + {question.choices.map((choice, i) => { const selected = answer === choice.formattedText.text ? 'selected' : ''; return ( @@ -51,17 +73,17 @@ const MultipleChoiceQuestionDisplay: React.FC = (props) => { className="button-wrapper" disabled={disableButton} onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}> - {showAnswer? (
{(choice.isCorrect ? '✅' : '❌')}
) - :``} + {showAnswer ? (
{(choice.isCorrect ? '✅' : '❌')}
) + : ``}
{alphabet[i]}
{choice.formattedFeedback && showAnswer && ( -
-
-
- )} +
+
+
+ )}
@@ -70,22 +92,24 @@ const MultipleChoiceQuestionDisplay: React.FC = (props) => {
{question.formattedGlobalFeedback && showAnswer && (
-
-
+
+
)} - + {!showAnswer && handleOnSubmitAnswer && ( - +
+
)}
); diff --git a/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx index 525501d..e415c14 100644 --- a/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/NumericalQuestionDisplay/NumericalQuestionDisplay.tsx @@ -18,7 +18,13 @@ const NumericalQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; const [answer, setAnswer] = useState(passedAnswer || ''); + const [isGoodAnswer, setisGoodAnswer] = useState(false); + const [isMultpleAnswer, setIsMultpleAnswer] = useState(false); + const correctAnswers = question.choices; + const correctAnswersList: number[] = []; + const correctAnswersPhrases: string[] = []; + let correctAnswer = ''; useEffect(() => { @@ -27,6 +33,34 @@ const NumericalQuestionDisplay: React.FC = (props) => { } }, [passedAnswer]); + useEffect(() => { + checkAnswer(); + }, [answer]); + + const checkAnswer = () => { + if(isMultpleAnswer) { + correctAnswers.forEach((answers) => { + if(isSimpleNumericalAnswer(answers) && answer === answers.number) { + setisGoodAnswer(true); + } else if(isRangeNumericalAnswer(answers) && answer as number >= answers.number - answers.range && answer as number <= answers.number + answers.range) { + setisGoodAnswer(true); + } else if(isHighLowNumericalAnswer(answers) && answer as number >= answers.numberLow && answer as number <= answers.numberHigh) { + setisGoodAnswer(true); + } + } + ) + ; + } else { + if(isSimpleNumericalAnswer(answers) && answer === answers.number) { + setisGoodAnswer(true); + } else if(isRangeNumericalAnswer(answers) && answer as number >= answers.number - answers.range && answer as number <= answers.number + answers.range) { + setisGoodAnswer(true); + } else if(isHighLowNumericalAnswer(answers) && answer as number >= answers.numberLow && answer as number <= answers.numberHigh) { + setisGoodAnswer(true); + } + }; + }; + //const isSingleAnswer = correctAnswers.length === 1; if (isSimpleNumericalAnswer(correctAnswers[0])) { @@ -38,28 +72,57 @@ const NumericalQuestionDisplay: React.FC = (props) => { const choice = correctAnswers[0] as HighLowNumericalAnswer; correctAnswer = `Entre ${choice.numberLow} et ${choice.numberHigh}`; } else if (isMultipleNumericalAnswer(correctAnswers[0])) { - correctAnswer = `MultipleNumericalAnswer is not supported yet`; - } else { + setIsMultpleAnswer(true); + correctAnswers.forEach((answers) => { + if(isSimpleNumericalAnswer(answers)) { + correctAnswersPhrases.push(`${(answers as SimpleNumericalAnswer).number}`); + } else if(isRangeNumericalAnswer(answers)) { + correctAnswersPhrases.push(`Entre ${answers.number - answers.range} et ${answers.number + answers.range}`); + } else if(isHighLowNumericalAnswer(answers)) { + correctAnswersPhrases.push(`Entre ${answers.numberLow} et ${answers.numberHigh}`); + } + }); + } + else { throw new Error('Unknown numerical answer type'); } return (
+ {showAnswer && ( +
+
+ {isGoodAnswer ? '✅ Correct! ' : '❌ Incorrect!'} +
+
+ Question : +
+ +
+ )}
{showAnswer ? ( - <> -
- La bonne réponse est: - {correctAnswer}
- - Votre réponse est: {answer.toString()} - + <> +
+
+
+ Réponse(s) accepté(es): +
+
+ {correctAnswer} +
+
+
+
+ Votre réponse est:
+
{answer}
+
+
{question.formattedGlobalFeedback &&
} - ) : ( <> @@ -80,7 +143,9 @@ const NumericalQuestionDisplay: React.FC = (props) => {
)} {handleOnSubmitAnswer && ( +
+
)} )} diff --git a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx index 28876f9..d03720c 100644 --- a/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay.tsx @@ -17,34 +17,59 @@ const ShortAnswerQuestionDisplay: React.FC = (props) => { const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; const [answer, setAnswer] = useState(passedAnswer || ''); - + const [isGoodAnswer, setisGoodAnswer] = useState(false); + + useEffect(() => { - if (passedAnswer !== undefined) { - setAnswer(passedAnswer); - } + if (passedAnswer !== undefined) { + setAnswer(passedAnswer); + } }, [passedAnswer]); - console.log("Answer" , answer); + + useEffect(() => { + checkAnswer(); + }, [answer]); + + const checkAnswer = () => { + const isCorrect = question.choices.some((choice) => String(choice.text).toLowerCase() === String(answer).toLowerCase()); + setisGoodAnswer(isCorrect); + }; return ( +
-
+ {showAnswer && ( +
+
+ {isGoodAnswer ? '✅ Correct! ' : '❌ Incorrect!'} +
+
+ Question : +
+ +
+ )} +
{showAnswer ? ( <>
- - La bonne réponse est: - - {question.choices.map((choice) => ( -
- {choice.text} +
+
+ Réponse(s) accepté(es):
- ))} - - - Votre réponse est: {answer} - + {question.choices.map((choice) => ( +
+ {choice.text} +
+ ))} +
+
+
+ Votre réponse est:
+
{answer}
+
{question.formattedGlobalFeedback &&
@@ -65,7 +90,9 @@ const ShortAnswerQuestionDisplay: React.FC = (props) => { />
{handleOnSubmitAnswer && ( +
+
)} )} diff --git a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx index 8908338..808185b 100644 --- a/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx +++ b/client/src/components/QuestionsDisplay/TrueFalseQuestionDisplay/TrueFalseQuestionDisplay.tsx @@ -47,7 +47,19 @@ const TrueFalseQuestionDisplay: React.FC = (props) => { const selectedTrue = answer ? 'selected' : ''; const selectedFalse = answer !== undefined && !answer ? 'selected' : ''; return ( -
+
+ {showAnswer && ( +
+
+ {answer === question.isTrue ? '✅ Correct! ' : '❌ Incorrect!'} +
+
+ Question : +
+ +
+ )} +
@@ -92,7 +104,9 @@ const TrueFalseQuestionDisplay: React.FC = (props) => {
)} {!showAnswer && handleOnSubmitAnswer && ( +
+
)}
); diff --git a/client/src/components/QuestionsDisplay/questionStyle.css b/client/src/components/QuestionsDisplay/questionStyle.css index f300ba2..328c1ed 100644 --- a/client/src/components/QuestionsDisplay/questionStyle.css +++ b/client/src/components/QuestionsDisplay/questionStyle.css @@ -2,7 +2,6 @@ display: flex; flex-direction: column; align-items: center; - justify-content: center; width: 100%; } @@ -22,8 +21,6 @@ .question-wrapper { display: flex; flex-direction: column; - align-items: center; - justify-content: center; } .question-wrapper .katex { @@ -31,7 +28,8 @@ } .katex * { - font-family: 'KaTeX_Main' /* to display characters like \neq properly */ + font-family: 'KaTeX_Main' + /* to display characters like \neq properly */ } .circle { @@ -97,15 +95,11 @@ } .answer-positive-weight { - background-color: hsl( - 120, 100%, 90% - ); + background-color: hsl(120, 100%, 90%); } .answer-zero-or-less-weight { - background-color: hsl( - 0, 100%, 90% - ); + background-color: hsl(0, 100%, 90%); } .answer-text.selected { @@ -119,7 +113,8 @@ } .feedback-container { - display: inline-block !important; /* override the parent */ + display: inline-block !important; + /* override the parent */ align-items: center; margin-left: 1.1rem; position: relative; @@ -136,12 +131,12 @@ /* height: 1em; */ } - .global-feedback { position: relative; padding: 0 1rem; background-color: hsl(43, 100%, 94%); color: hsl(43, 95%, 9%); + margin-top: 2rem; border: hsl(36, 84%, 93%) 1px solid; border-radius: 6px; box-shadow: 0px 2px 5px hsl(0, 0%, 74%); @@ -156,6 +151,7 @@ border-radius: 6px; box-shadow: 0px 2px 5px hsl(0, 0%, 74%); } + .false-feedback { position: relative; padding: 0 1rem; @@ -169,3 +165,57 @@ .choices-wrapper { width: 90%; } + +.correct-answer-text { + text-align: left; + /* Align text to the left */ + width: 100%; + /* Ensure it takes the full width of its container */ +} + +.accepted-answers { + display: inline-block; + /* Display elements side by side */ + border-radius: 6px; + /* Add rounded corners */ + padding: 0.5rem; + /* Add padding for spacing */ + margin: 0.5rem; + /* Add margin for spacing between elements */ + background-color: hsl(0, 0%, 87%); + /* Optional: Add a background color for emphasis */ +} + +.question-feedback-validation { + font-size: 1.8rem; + /* Increase the font size */ + font-weight: bold; + /* Make the text bold */ + width: 100%; + height: 100%; + margin: 0 auto; + +} + +.question-title { + margin-bottom: 0.5rem; + margin-top: 1.5rem; + width: 100%; + + font-weight: bold; +} + +.question-content { + margin-bottom: 1rem; + +} + +.submit-button { + width: min-content; + margin-left: 19rem;} + +.submit-button-container{ + display: flex; + justify-content: center; + margin-top: 1rem; +} \ No newline at end of file diff --git a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx index 192c0b2..e2445fb 100644 --- a/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx +++ b/client/src/components/StudentModeQuiz/StudentModeQuiz.tsx @@ -60,7 +60,7 @@ const StudentModeQuiz: React.FC = ({
Question {questionInfos.question.id}/{questions.length}
-
+
{/* = ({ open={isFeedbackDialogOpen} onClose={handleFeedbackDialogClose} > - Rétroaction
= ({ maxHeight: '400px', overflowY: 'auto', }}> -
Question :