first cut, with tests
Some checks failed
CI/CD Pipeline for Backend / build_and_push_backend (push) Failing after 1m1s
CI/CD Pipeline for Nginx Router / build_and_push_nginx (push) Failing after 59s
CI/CD Pipeline for Frontend / build_and_push_frontend (push) Failing after 18s
Tests / lint-and-tests (client) (push) Failing after 1m27s
Tests / lint-and-tests (server) (push) Failing after 1m3s

This commit is contained in:
C. Fuhrman 2025-03-21 00:25:25 -04:00
parent 112062c0b2
commit 42e3041830
16 changed files with 725 additions and 226 deletions

View file

@ -19,8 +19,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
}); });
const mockStudents: StudentType[] = [ const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
]; ];
const mockShowSelectedQuestion = jest.fn(); const mockShowSelectedQuestion = jest.fn();

View file

@ -20,8 +20,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
const mockStudents: StudentType[] = [ const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
]; ];
const mockShowSelectedQuestion = jest.fn(); const mockShowSelectedQuestion = jest.fn();

View file

@ -20,8 +20,8 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
}); });
const mockStudents: StudentType[] = [ const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
]; ];
const mockGetStudentGrade = jest.fn((student: StudentType) => { const mockGetStudentGrade = jest.fn((student: StudentType) => {

View file

@ -6,8 +6,8 @@ import LiveResultsTableFooter from 'src/components/LiveResults/LiveResultsTable/
const mockStudents: StudentType[] = [ const mockStudents: StudentType[] = [
{ id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Answer 1', isCorrect: true }] }, { id: "1", name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Answer 1'], isCorrect: true }] },
{ id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: 'Answer 2', isCorrect: false }] }, { id: "2", name: 'Student 2', answers: [{ idQuestion: 2, answer: ['Answer 2'], isCorrect: false }] },
]; ];
const mockGetStudentGrade = jest.fn((student: StudentType) => { const mockGetStudentGrade = jest.fn((student: StudentType) => {

View file

@ -19,7 +19,8 @@ const mockGiftQuestions = parse(
`::Sample Question 1:: Question stem `::Sample Question 1:: Question stem
{ {
=Choice 1 =Choice 1
~Choice 2 =Choice 2
~Choice 3
}`); }`);
const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => { const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) => {
@ -30,8 +31,9 @@ const mockQuestions: QuestionType[] = mockGiftQuestions.map((question, index) =>
}); });
const mockStudents: StudentType[] = [ const mockStudents: StudentType[] = [
{ id: '1', name: 'Student 1', answers: [{ idQuestion: 1, answer: 'Choice 1', isCorrect: true }] }, { id: '1', name: 'Student 1', answers: [{ idQuestion: 1, answer: ['Choice 1'], isCorrect: false }] },
{ id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: 'Choice 2', isCorrect: false }] }, { id: '2', name: 'Student 2', answers: [{ idQuestion: 1, answer: ['Choice 3'], isCorrect: false }] },
{ id: '3', name: 'Student 3', answers: [{ idQuestion: 1, answer: ['Choice 1', 'Choice 2'], isCorrect: true }] },
]; ];
describe('LiveResults', () => { describe('LiveResults', () => {
@ -82,7 +84,6 @@ describe('LiveResults', () => {
}); });
}); });
});
test('calculates and displays the correct student grades', () => { test('calculates and displays the correct student grades', () => {
render( render(
<LiveResults <LiveResults
@ -161,3 +162,5 @@ test('displays the correct answers per question', () => {
expect(correctAnswersElement).toBeInTheDocument(); expect(correctAnswersElement).toBeInTheDocument();
}); });
}); });
});

View file

@ -73,6 +73,30 @@ describe('MultipleChoiceQuestionDisplay', () => {
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith('Choice 1'); expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith('Choice 1');
}); });
test('submits multiple selected answers', () => {
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']);
});
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 () => {
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');

View file

@ -0,0 +1,353 @@
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 no answers are selected', () => {
const answer: AnswerType = [];
const result = checkIfIsCorrect(answer, 1, mockQuestions);
expect(result).toBe(false);
});
test('returns false when no correct answers are provided in the question', () => {
const mockQuestionsWithNoCorrectAnswers: QuestionType[] = [
{
question: {
id: '1',
type: 'MC',
choices: [
{ isCorrect: false, formattedText: { text: 'Answer1' } },
{ isCorrect: false, formattedText: { text: 'Answer2' } },
],
} as MultipleChoiceQuestion,
},
];
const answer: AnswerType = ['Answer1'];
const result = checkIfIsCorrect(answer, 1, mockQuestionsWithNoCorrectAnswers);
expect(result).toBe(false);
});
test('returns true for a correct true/false answer', () => {
const mockQuestionsTF: QuestionType[] = [
{
question: {
id: '2',
type: 'TF',
isTrue: true,
} as TrueFalseQuestion,
},
];
const answer: AnswerType = [true];
const result = checkIfIsCorrect(answer, 2, mockQuestionsTF);
expect(result).toBe(true);
});
test('returns false for an incorrect true/false answer', () => {
const mockQuestionsTF: QuestionType[] = [
{
question: {
id: '2',
type: 'TF',
isTrue: true,
} as TrueFalseQuestion,
},
];
const answer: AnswerType = [false];
const result = checkIfIsCorrect(answer, 2, mockQuestionsTF);
expect(result).toBe(false);
});
test('returns false for a true/false question with no answer', () => {
const mockQuestionsTF: QuestionType[] = [
{
question: {
id: '2',
type: 'TF',
isTrue: true,
} as TrueFalseQuestion,
},
];
const answer: AnswerType = [];
const result = checkIfIsCorrect(answer, 2, mockQuestionsTF);
expect(result).toBe(false);
});
test('returns true for a correct true/false answer when isTrue is false', () => {
const mockQuestionsTF: QuestionType[] = [
{
question: {
id: '3',
type: 'TF',
isTrue: false, // Correct answer is false
} as TrueFalseQuestion,
},
];
const answer: AnswerType = [false];
const result = checkIfIsCorrect(answer, 3, mockQuestionsTF);
expect(result).toBe(true);
});
test('returns false for an incorrect true/false answer when isTrue is false', () => {
const mockQuestionsTF: QuestionType[] = [
{
question: {
id: '3',
type: 'TF',
isTrue: false, // Correct answer is false
} as TrueFalseQuestion,
},
];
const answer: AnswerType = [true];
const result = checkIfIsCorrect(answer, 3, mockQuestionsTF);
expect(result).toBe(false);
});
test('returns true for a correct short answer', () => {
const mockQuestionsShort: QuestionType[] = [
{
question: {
id: '4',
type: 'Short',
choices: [
{ text: 'CorrectAnswer1' },
{ text: 'CorrectAnswer2' },
],
} as ShortAnswerQuestion,
},
];
const answer: AnswerType = ['CorrectAnswer1'];
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
expect(result).toBe(true);
});
test('returns false for an incorrect short answer', () => {
const mockQuestionsShort: QuestionType[] = [
{
question: {
id: '4',
type: 'Short',
choices: [
{ text: 'CorrectAnswer1' },
{ text: 'CorrectAnswer2' },
],
} as ShortAnswerQuestion,
},
];
const answer: AnswerType = ['WrongAnswer'];
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
expect(result).toBe(false);
});
test('returns true for a correct short answer with case insensitivity', () => {
const mockQuestionsShort: QuestionType[] = [
{
question: {
id: '4',
type: 'Short',
choices: [
{ text: 'CorrectAnswer1' },
{ text: 'CorrectAnswer2' },
],
} as ShortAnswerQuestion,
},
];
const answer: AnswerType = ['correctanswer1']; // Lowercase version of the correct answer
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
expect(result).toBe(true);
});
test('returns false for a short answer question with no answer', () => {
const mockQuestionsShort: QuestionType[] = [
{
question: {
id: '4',
type: 'Short',
choices: [
{ text: 'CorrectAnswer1' },
{ text: 'CorrectAnswer2' },
],
} as ShortAnswerQuestion,
},
];
const answer: AnswerType = [];
const result = checkIfIsCorrect(answer, 4, mockQuestionsShort);
expect(result).toBe(false);
});
test('returns true for a correct simple numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '5',
type: 'Numerical',
choices: [
{ type: 'simple', number: 42 } as SimpleNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [42]; // User's answer
const result = checkIfIsCorrect(answer, 5, mockQuestionsNumerical);
expect(result).toBe(true);
});
test('returns false for an incorrect simple numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '5',
type: 'Numerical',
choices: [
{ type: 'simple', number: 42 } as SimpleNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [43]; // User's answer
const result = checkIfIsCorrect(answer, 5, mockQuestionsNumerical);
expect(result).toBe(false);
});
test('returns true for a correct range numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '6',
type: 'Numerical',
choices: [
{ type: 'range', number: 50, range: 5 } as RangeNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [52]; // User's answer within the range (50 ± 5)
const result = checkIfIsCorrect(answer, 6, mockQuestionsNumerical);
expect(result).toBe(true);
});
test('returns false for an out-of-range numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '6',
type: 'Numerical',
choices: [
{ type: 'range', number: 50, range: 5 } as RangeNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [56]; // User's answer outside the range (50 ± 5)
const result = checkIfIsCorrect(answer, 6, mockQuestionsNumerical);
expect(result).toBe(false);
});
test('returns true for a correct high-low numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '7',
type: 'Numerical',
choices: [
{ type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [95]; // User's answer within the range (90 to 100)
const result = checkIfIsCorrect(answer, 7, mockQuestionsNumerical);
expect(result).toBe(true);
});
test('returns false for an out-of-range high-low numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '7',
type: 'Numerical',
choices: [
{ type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [105]; // User's answer outside the range (90 to 100)
const result = checkIfIsCorrect(answer, 7, mockQuestionsNumerical);
expect(result).toBe(false);
});
test('returns true for a correct multiple numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '8',
type: 'Numerical',
choices: [
{
isCorrect: true,
answer: { type: 'simple', number: 42 } as SimpleNumericalAnswer,
} as MultipleNumericalAnswer,
{
isCorrect: false,
answer: { type: 'high-low', numberHigh: 100, numberLow: 90 } as HighLowNumericalAnswer,
formattedFeedback: { text: 'You guessed way too high' },
}
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [42]; // User's answer matches the correct multiple numerical answer
const result = checkIfIsCorrect(answer, 8, mockQuestionsNumerical);
expect(result).toBe(true);
});
test('returns false for an incorrect multiple numerical answer', () => {
const mockQuestionsNumerical: QuestionType[] = [
{
question: {
id: '8',
type: 'Numerical',
choices: [
{
type: 'multiple',
isCorrect: true,
answer: { type: 'simple', number: 42 } as SimpleNumericalAnswer,
} as MultipleNumericalAnswer,
],
} as NumericalQuestion,
},
];
const answer: AnswerType = [43]; // User's answer does not match the correct multiple numerical answer
const result = checkIfIsCorrect(answer, 8, mockQuestionsNumerical);
expect(result).toBe(false);
});
});

View file

@ -44,7 +44,7 @@ const mockStudents: StudentType[] = [
]; ];
const mockAnswerData: AnswerReceptionFromBackendType = { const mockAnswerData: AnswerReceptionFromBackendType = {
answer: 'Answer1', answer: ['Answer1'],
idQuestion: 1, idQuestion: 1,
idUser: '1', idUser: '1',
username: 'Student 1', username: 'Student 1',
@ -233,7 +233,7 @@ describe('ManageRoom', () => {
await act(async () => { await act(async () => {
const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1]; const createSuccessCallback = (mockSocket.on as jest.Mock).mock.calls.find(call => call[0] === 'create-success')[1];
createSuccessCallback('test-room-name'); createSuccessCallback('Test Room');
}); });
const launchButton = screen.getByText('Lancer'); const launchButton = screen.getByText('Lancer');
@ -256,6 +256,7 @@ describe('ManageRoom', () => {
}); });
await waitFor(() => { await waitFor(() => {
// console.info(consoleSpy.mock.calls);
expect(consoleSpy).toHaveBeenCalledWith( expect(consoleSpy).toHaveBeenCalledWith(
'Received answer from Student 1 for question 1: Answer1' 'Received answer from Student 1 for question 1: Answer1'
); );
@ -294,3 +295,4 @@ describe('ManageRoom', () => {
}); });
}); });
}); });

View file

@ -8,7 +8,7 @@ import { QuestionType } from 'src/Types/QuestionType';
import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService'; import { AnswerSubmissionToBackendType } from 'src/services/WebsocketService';
const mockGiftQuestions = parse( const mockGiftQuestions = parse(
`::Sample Question 1:: Sample Question 1 {=Option A ~Option B} `::Sample Question 1:: Sample Question 1 {=Option A =Option B ~Option C}
::Sample Question 2:: Sample Question 2 {T}`); ::Sample Question 2:: Sample Question 2 {T}`);
@ -23,9 +23,6 @@ const mockSubmitAnswer = jest.fn();
const mockDisconnectWebSocket = jest.fn(); const mockDisconnectWebSocket = jest.fn();
beforeEach(() => { beforeEach(() => {
// Clear local storage before each test
// localStorage.clear();
render( render(
<MemoryRouter> <MemoryRouter>
<StudentModeQuiz <StudentModeQuiz
@ -54,7 +51,7 @@ describe('StudentModeQuiz', () => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1);
}); });
test('handles shows feedback for an already answered question', async () => { test('handles shows feedback for an already answered question', async () => {
@ -65,13 +62,13 @@ describe('StudentModeQuiz', () => {
act(() => { act(() => {
fireEvent.click(screen.getByText('Répondre')); fireEvent.click(screen.getByText('Répondre'));
}); });
expect(mockSubmitAnswer).toHaveBeenCalledWith('Option A', 1); expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A'], 1);
const firstButtonA = screen.getByRole("button", {name: '✅ A Option A'}); const firstButtonA = screen.getByRole("button", {name: '✅ A Option A'});
expect(firstButtonA).toBeInTheDocument(); expect(firstButtonA).toBeInTheDocument();
expect(firstButtonA.querySelector('.selected')).toBeInTheDocument(); expect(firstButtonA.querySelector('.selected')).toBeInTheDocument();
expect(screen.getByRole("button", {name: ' B Option B'})).toBeInTheDocument(); expect(screen.getByRole("button", {name: ' B Option B'})).toBeInTheDocument();
expect(screen.queryByText('Répondre')).not.toBeInTheDocument(); expect(screen.queryByText('Répondre')).not.toBeInTheDocument();
// Navigate to the next question // Navigate to the next question
@ -87,12 +84,12 @@ describe('StudentModeQuiz', () => {
}); });
expect(await screen.findByText('Sample Question 1')).toBeInTheDocument(); expect(await screen.findByText('Sample Question 1')).toBeInTheDocument();
// Since answers are mocked, the it doesn't recognize the question as already answered // Since answers are mocked, it doesn't recognize the question as already answered
// TODO these tests are partially faked, need to be fixed if we can mock the answers // TODO these tests are partially faked, need to be fixed if we can mock the answers
// const buttonA = screen.getByRole("button", {name: '✅ A Option A'}); // const buttonA = screen.getByRole("button", {name: '✅ A Option A'});
const buttonA = screen.getByRole("button", {name: 'A Option A'}); const buttonA = screen.getByRole("button", {name: 'A Option A'});
expect(buttonA).toBeInTheDocument(); expect(buttonA).toBeInTheDocument();
// const buttonB = screen.getByRole("button", {name: ' B Option B'}); // const buttonB = screen.getByRole("button", {name: ' B Option B'});
const buttonB = screen.getByRole("button", {name: 'B Option B'}); const buttonB = screen.getByRole("button", {name: 'B Option B'});
expect(buttonB).toBeInTheDocument(); expect(buttonB).toBeInTheDocument();
// // "Option A" div inside the name of button should have selected class // // "Option A" div inside the name of button should have selected class
@ -122,4 +119,28 @@ describe('StudentModeQuiz', () => {
expect(screen.getByText('Sample Question 2')).toBeInTheDocument(); expect(screen.getByText('Sample Question 2')).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument(); expect(screen.getByText('Répondre')).toBeInTheDocument();
}); });
test('allows multiple answers to be selected for a question', async () => {
// Simulate selecting multiple answers
act(() => {
fireEvent.click(screen.getByText('Option A'));
});
act(() => {
fireEvent.click(screen.getByText('Option B'));
});
// Simulate submitting the answers
act(() => {
fireEvent.click(screen.getByText('Répondre'));
});
// Verify that the mockSubmitAnswer function is called with both answers
expect(mockSubmitAnswer).toHaveBeenCalledWith(['Option A', 'Option B'], 1);
// Verify that the selected answers are displayed as selected
const buttonA = screen.getByRole('button', { name: '✅ A Option A' });
const buttonB = screen.getByRole('button', { name: '✅ B Option B' });
expect(buttonA).toBeInTheDocument();
expect(buttonB).toBeInTheDocument();
});
}); });

View file

@ -15,8 +15,7 @@ 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 || ''); const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
let disableButton = false; let disableButton = false;
if (handleOnSubmitAnswer === undefined) { if (handleOnSubmitAnswer === undefined) {
@ -30,61 +29,81 @@ const MultipleChoiceQuestionDisplay: React.FC<Props> = (props) => {
}, [passedAnswer]); }, [passedAnswer]);
const handleOnClickAnswer = (choice: string) => { const handleOnClickAnswer = (choice: string) => {
setAnswer(choice); setAnswer((prevAnswer) => {
if (prevAnswer.includes(choice)) {
// Remove the choice if it's already selected
return prevAnswer.filter((selected) => selected !== choice);
} else {
// Add the choice if it's not already selected
return [...prevAnswer, choice];
}
});
}; };
const alpha = Array.from(Array(26)).map((_e, i) => i + 65); const alpha = Array.from(Array(26)).map((_e, i) => i + 65);
const alphabet = alpha.map((x) => String.fromCharCode(x)); const alphabet = alpha.map((x) => String.fromCharCode(x));
return (
return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} /> <div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedStem) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
{question.choices.map((choice, i) => { {question.choices.map((choice, i) => {
const selected = answer === choice.formattedText.text ? 'selected' : ''; const selected = answer.includes(choice.formattedText.text) ? 'selected' : '';
return ( return (
<div key={choice.formattedText.text + i} className="choice-container"> <div key={choice.formattedText.text + i} className="choice-container">
<Button <Button
variant="text" variant="text"
className="button-wrapper" className="button-wrapper"
disabled={disableButton} disabled={disableButton}
onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}> onClick={() => !showAnswer && handleOnClickAnswer(choice.formattedText.text)}
{showAnswer? (<div> {(choice.isCorrect ? '✅' : '❌')}</div>) >
:``} {showAnswer ? (
<div>{choice.isCorrect ? '✅' : '❌'}</div>
) : (
''
)}
<div className={`circle ${selected}`}>{alphabet[i]}</div> <div className={`circle ${selected}`}>{alphabet[i]}</div>
<div className={`answer-text ${selected}`}> <div className={`answer-text ${selected}`}>
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedText) }} /> <div
dangerouslySetInnerHTML={{
__html: FormattedTextTemplate(choice.formattedText),
}}
/>
</div> </div>
{choice.formattedFeedback && showAnswer && ( {choice.formattedFeedback && showAnswer && (
<div className="feedback-container mb-1 mt-1/2"> <div className="feedback-container mb-1 mt-1/2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(choice.formattedFeedback) }} /> <div
dangerouslySetInnerHTML={{
__html: FormattedTextTemplate(choice.formattedFeedback),
}}
/>
</div> </div>
)} )}
</Button> </Button>
</div> </div>
); );
})} })}
</div> </div>
{question.formattedGlobalFeedback && showAnswer && ( {question.formattedGlobalFeedback && showAnswer && (
<div className="global-feedback mb-2"> <div className="global-feedback mb-2">
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate(question.formattedGlobalFeedback) }} /> <div
dangerouslySetInnerHTML={{
__html: FormattedTextTemplate(question.formattedGlobalFeedback),
}}
/>
</div> </div>
)} )}
{!showAnswer && handleOnSubmitAnswer && ( {!showAnswer && handleOnSubmitAnswer && (
<Button <Button
variant="contained" variant="contained"
onClick={() => onClick={() =>
answer !== "" && handleOnSubmitAnswer && handleOnSubmitAnswer(answer) answer.length > 0 && handleOnSubmitAnswer && handleOnSubmitAnswer(answer)
} }
disabled={answer === '' || answer === null} disabled={answer.length === 0}
> >
Répondre Répondre
</Button> </Button>
)} )}
</div> </div>

View file

@ -17,7 +17,7 @@ interface Props {
const NumericalQuestionDisplay: React.FC<Props> = (props) => { const NumericalQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } =
props; props;
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || ''); const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
const correctAnswers = question.choices; const correctAnswers = question.choices;
let correctAnswer = ''; let correctAnswer = '';
@ -69,7 +69,7 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
id={question.formattedStem.text} id={question.formattedStem.text}
name={question.formattedStem.text} name={question.formattedStem.text}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAnswer(e.target.valueAsNumber); setAnswer([e.target.valueAsNumber]);
}} }}
inputProps={{ 'data-testid': 'number-input' }} inputProps={{ 'data-testid': 'number-input' }}
/> />
@ -87,7 +87,7 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
handleOnSubmitAnswer && handleOnSubmitAnswer &&
handleOnSubmitAnswer(answer) handleOnSubmitAnswer(answer)
} }
disabled={answer === "" || isNaN(answer as number)} disabled={answer === undefined || answer === null || isNaN(answer[0] as number)}
> >
Répondre Répondre
</Button> </Button>

View file

@ -16,7 +16,7 @@ interface Props {
const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => { const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props; const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } = props;
const [answer, setAnswer] = useState<AnswerType>(passedAnswer || ''); const [answer, setAnswer] = useState<AnswerType>(passedAnswer || []);
useEffect(() => { useEffect(() => {
if (passedAnswer !== undefined) { if (passedAnswer !== undefined) {
@ -58,7 +58,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
id={question.formattedStem.text} id={question.formattedStem.text}
name={question.formattedStem.text} name={question.formattedStem.text}
onChange={(e) => { onChange={(e) => {
setAnswer(e.target.value); setAnswer([e.target.value]);
}} }}
disabled={showAnswer} disabled={showAnswer}
aria-label="short-answer-input" aria-label="short-answer-input"
@ -72,7 +72,7 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
handleOnSubmitAnswer && handleOnSubmitAnswer &&
handleOnSubmitAnswer(answer) handleOnSubmitAnswer(answer)
} }
disabled={answer === null || answer === ''} disabled={answer === null || answer === undefined || answer.length === 0}
> >
Répondre Répondre
</Button> </Button>

View file

@ -23,9 +23,9 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
} }
useEffect(() => { useEffect(() => {
console.log("passedAnswer", answer); console.log("passedAnswer", passedAnswer);
if (passedAnswer === true || passedAnswer === false) { if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
setAnswer(passedAnswer); setAnswer(passedAnswer[0]);
} else { } else {
setAnswer(undefined); setAnswer(undefined);
} }
@ -33,8 +33,8 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
const [answer, setAnswer] = useState<boolean | undefined>(() => { const [answer, setAnswer] = useState<boolean | undefined>(() => {
if (passedAnswer === true || passedAnswer === false) { if (passedAnswer && (passedAnswer[0] === true || passedAnswer[0] === false)) {
return passedAnswer; return passedAnswer[0];
} }
return undefined; return undefined;
@ -95,8 +95,7 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
<Button <Button
variant="contained" variant="contained"
onClick={() => onClick={() =>
answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer(answer) answer !== undefined && handleOnSubmitAnswer && handleOnSubmitAnswer([answer])
} }
disabled={answer === undefined} disabled={answer === undefined}
> >

View file

@ -17,7 +17,7 @@ import LoginContainer from 'src/components/LoginContainer/LoginContainer'
import ApiService from '../../../services/ApiService' import ApiService from '../../../services/ApiService'
export type AnswerType = string | number | boolean; export type AnswerType = Array<string | number | boolean>;
const JoinRoom: React.FC = () => { const JoinRoom: React.FC = () => {
const [roomName, setRoomName] = useState(''); const [roomName, setRoomName] = useState('');

View file

@ -1,12 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { ParsedGIFTQuestion, BaseQuestion, parse, Question } from 'gift-pegjs'; import { BaseQuestion, parse, Question } from 'gift-pegjs';
import {
isSimpleNumericalAnswer,
isRangeNumericalAnswer,
isHighLowNumericalAnswer
} from 'gift-pegjs/typeGuards';
import LiveResultsComponent from 'src/components/LiveResults/LiveResults'; import LiveResultsComponent from 'src/components/LiveResults/LiveResults';
import webSocketService, { import webSocketService, {
AnswerReceptionFromBackendType AnswerReceptionFromBackendType
@ -24,7 +19,7 @@ import QuestionDisplay from 'src/components/QuestionsDisplay/QuestionDisplay';
import ApiService from '../../../services/ApiService'; import ApiService from '../../../services/ApiService';
import { QuestionType } from 'src/Types/QuestionType'; import { QuestionType } from 'src/Types/QuestionType';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom'; import { checkIfIsCorrect } from './useRooms';
const ManageRoom: React.FC = () => { const ManageRoom: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -346,63 +341,6 @@ const ManageRoom: React.FC = () => {
navigate('/teacher/dashboard'); navigate('/teacher/dashboard');
}; };
function checkIfIsCorrect(
answer: AnswerType,
idQuestion: number,
questions: QuestionType[]
): boolean {
const questionInfo = questions.find((q) =>
q.question.id ? q.question.id === idQuestion.toString() : false
) as QuestionType | undefined;
const answerText = answer.toString();
if (questionInfo) {
const question = questionInfo.question as ParsedGIFTQuestion;
if (question.type === 'TF') {
return (
(question.isTrue && answerText == 'true') ||
(!question.isTrue && answerText == 'false')
);
} else if (question.type === 'MC') {
return question.choices.some(
(choice) => choice.isCorrect && choice.formattedText.text === answerText
);
} else if (question.type === 'Numerical') {
if (isHighLowNumericalAnswer(question.choices[0])) {
const choice = question.choices[0];
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return (
answerNumber <= choice.numberHigh && answerNumber >= choice.numberLow
);
}
}
if (isRangeNumericalAnswer(question.choices[0])) {
const answerNumber = parseFloat(answerText);
const range = question.choices[0].range;
const correctAnswer = question.choices[0].number;
if (!isNaN(answerNumber)) {
return (
answerNumber <= correctAnswer + range &&
answerNumber >= correctAnswer - range
);
}
}
if (isSimpleNumericalAnswer(question.choices[0])) {
const answerNumber = parseFloat(answerText);
if (!isNaN(answerNumber)) {
return answerNumber === question.choices[0].number;
}
}
} else if (question.type === 'Short') {
return question.choices.some(
(choice) => choice.text.toUpperCase() === answerText.toUpperCase()
);
}
}
return false;
}
if (!formattedRoomName) { if (!formattedRoomName) {
return ( return (
<div className="center"> <div className="center">

View file

@ -1,8 +1,15 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { RoomType } from 'src/Types/RoomType'; import { RoomType } from 'src/Types/RoomType';
import { createContext } from 'react'; import { createContext } from 'react';
import { MultipleNumericalAnswer, NumericalAnswer, ParsedGIFTQuestion } from 'gift-pegjs';
//import { RoomContext } from './RoomContext'; import { QuestionType } from 'src/Types/QuestionType';
import { AnswerType } from 'src/pages/Student/JoinRoom/JoinRoom';
import {
isSimpleNumericalAnswer,
isRangeNumericalAnswer,
isHighLowNumericalAnswer,
isMultipleNumericalAnswer
} from 'gift-pegjs/typeGuards';
type RoomContextType = { type RoomContextType = {
rooms: RoomType[]; rooms: RoomType[];
@ -18,3 +25,136 @@ export const useRooms = () => {
if (!context) throw new Error('useRooms must be used within a RoomProvider'); if (!context) throw new Error('useRooms must be used within a RoomProvider');
return context; return context;
}; };
/**
* Checks if the answer is correct - logic varies by type of question!
* True/False: answer must match the isTrue property
* Multiple Choice: answer must match the correct choice(s)
* Numerical: answer must be within the range or equal to the number (for each type of correct answer)
* Short Answer: answer must match the correct choice(s) (case-insensitive)
* @param answer
* @param idQuestion
* @param questions
* @returns
*/
export function checkIfIsCorrect(
answer: AnswerType,
idQuestion: number,
questions: QuestionType[]
): boolean {
const questionInfo = questions.find((q) =>
q.question.id ? q.question.id === idQuestion.toString() : false
) as QuestionType | undefined;
const simpleAnswerText = answer.toString();
if (questionInfo) {
const question = questionInfo.question as ParsedGIFTQuestion;
if (question.type === 'TF') {
return (
(question.isTrue && simpleAnswerText == 'true') ||
(!question.isTrue && simpleAnswerText == 'false')
);
} else if (question.type === 'MC') {
const correctAnswers = question.choices.filter((choice) => choice.isCorrect
/* || (choice.weight && choice.weight > 0)*/ // handle weighted answers
);
const multipleAnswers = Array.isArray(answer) ? answer : [answer as string];
if (correctAnswers.length === 0) {
return false;
}
return correctAnswers.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
}