mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
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
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:
parent
112062c0b2
commit
42e3041830
16 changed files with 725 additions and 226 deletions
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import LiveResults from 'src/components/LiveResults/LiveResults';
|
||||||
import { QuestionType } from 'src/Types/QuestionType';
|
import { QuestionType } from 'src/Types/QuestionType';
|
||||||
import { StudentType } from 'src/Types/StudentType';
|
import { StudentType } from 'src/Types/StudentType';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { BaseQuestion,parse } from 'gift-pegjs';
|
import { BaseQuestion, parse } from 'gift-pegjs';
|
||||||
|
|
||||||
const mockSocket: Socket = {
|
const mockSocket: Socket = {
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
|
|
@ -19,19 +19,21 @@ 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) => {
|
||||||
if (question.type !== "Category")
|
if (question.type !== "Category")
|
||||||
question.id = (index + 1).toString();
|
question.id = (index + 1).toString();
|
||||||
const newMockQuestion = question;
|
const newMockQuestion = question;
|
||||||
return {question : newMockQuestion as BaseQuestion};
|
return { question: newMockQuestion as BaseQuestion };
|
||||||
});
|
});
|
||||||
|
|
||||||
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,8 +84,7 @@ describe('LiveResults', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
test('calculates and displays the correct student grades', () => {
|
||||||
test('calculates and displays the correct student grades', () => {
|
|
||||||
render(
|
render(
|
||||||
<LiveResults
|
<LiveResults
|
||||||
socket={mockSocket}
|
socket={mockSocket}
|
||||||
|
|
@ -106,9 +107,9 @@ test('calculates and displays the correct student grades', () => {
|
||||||
const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100;
|
const grade = student.answers.filter(answer => answer.isCorrect).length / mockQuestions.length * 100;
|
||||||
expect(screen.getByText(`${grade.toFixed()} %`)).toBeInTheDocument();
|
expect(screen.getByText(`${grade.toFixed()} %`)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calculates and displays the class average', () => {
|
test('calculates and displays the class average', () => {
|
||||||
render(
|
render(
|
||||||
<LiveResults
|
<LiveResults
|
||||||
socket={mockSocket}
|
socket={mockSocket}
|
||||||
|
|
@ -137,9 +138,9 @@ test('calculates and displays the class average', () => {
|
||||||
return element.closest('td')?.classList.contains('MuiTableCell-footer');
|
return element.closest('td')?.classList.contains('MuiTableCell-footer');
|
||||||
});
|
});
|
||||||
expect(classAverageElement).toBeInTheDocument();
|
expect(classAverageElement).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('displays the correct answers per question', () => {
|
test('displays the correct answers per question', () => {
|
||||||
render(
|
render(
|
||||||
<LiveResults
|
<LiveResults
|
||||||
socket={mockSocket}
|
socket={mockSocket}
|
||||||
|
|
@ -160,4 +161,6 @@ test('displays the correct answers per question', () => {
|
||||||
});
|
});
|
||||||
expect(correctAnswersElement).toBeInTheDocument();
|
expect(correctAnswersElement).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
353
client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx
Normal file
353
client/src/__tests__/pages/ManageRoom/IsCorrect.test.tsx
Normal 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -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', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,10 @@ 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) {
|
||||||
disableButton = true;
|
disableButton = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// TrueFalseQuestion.tsx
|
// TrueFalseQuestion.tsx
|
||||||
import React, { useState,useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import '../questionStyle.css';
|
import '../questionStyle.css';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import { TrueFalseQuestion } from 'gift-pegjs';
|
import { TrueFalseQuestion } from 'gift-pegjs';
|
||||||
|
|
@ -14,18 +14,18 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer} =
|
const { question, showAnswer, handleOnSubmitAnswer, passedAnswer } =
|
||||||
props;
|
props;
|
||||||
|
|
||||||
let disableButton = false;
|
let disableButton = false;
|
||||||
if(handleOnSubmitAnswer === undefined){
|
if (handleOnSubmitAnswer === undefined) {
|
||||||
disableButton = true;
|
disableButton = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -58,7 +58,7 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={disableButton}
|
disabled={disableButton}
|
||||||
>
|
>
|
||||||
{showAnswer? (<div> {(question.isTrue ? '✅' : '❌')}</div>):``}
|
{showAnswer ? (<div> {(question.isTrue ? '✅' : '❌')}</div>) : ``}
|
||||||
<div className={`circle ${selectedTrue}`}>V</div>
|
<div className={`circle ${selectedTrue}`}>V</div>
|
||||||
<div className={`answer-text ${selectedTrue}`}>Vrai</div>
|
<div className={`answer-text ${selectedTrue}`}>Vrai</div>
|
||||||
|
|
||||||
|
|
@ -75,7 +75,7 @@ const TrueFalseQuestionDisplay: React.FC<Props> = (props) => {
|
||||||
disabled={disableButton}
|
disabled={disableButton}
|
||||||
|
|
||||||
>
|
>
|
||||||
{showAnswer? (<div> {(!question.isTrue ? '✅' : '❌')}</div>):``}
|
{showAnswer ? (<div> {(!question.isTrue ? '✅' : '❌')}</div>) : ``}
|
||||||
<div className={`circle ${selectedFalse}`}>F</div>
|
<div className={`circle ${selectedFalse}`}>F</div>
|
||||||
<div className={`answer-text ${selectedFalse}`}>Faux</div>
|
<div className={`answer-text ${selectedFalse}`}>Faux</div>
|
||||||
|
|
||||||
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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('');
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue