Compare commits

..

No commits in common. "ffa22006066396dbc6b5deec93b80bfa286c0e60" and "d047c787b716e1780978bc3a5e6fa57a11c3a7f3" have entirely different histories.

11 changed files with 227 additions and 181 deletions

View file

@ -17,19 +17,15 @@ const question = questions[0];
describe('MultipleChoiceQuestionDisplay', () => { describe('MultipleChoiceQuestionDisplay', () => {
const mockHandleOnSubmitAnswer = jest.fn(); const mockHandleOnSubmitAnswer = jest.fn();
const sampleProps = {
question: question,
handleOnSubmitAnswer: mockHandleOnSubmitAnswer,
showAnswer: false
};
const choices = question.choices; const choices = question.choices;
beforeEach(() => { beforeEach(() => {
render( render(
<MemoryRouter> <MemoryRouter>
<MultipleChoiceQuestionDisplay <MultipleChoiceQuestionDisplay
{...sampleProps} question={question}
handleOnSubmitAnswer={mockHandleOnSubmitAnswer}
showAnswer={false}
/> />
</MemoryRouter>); </MemoryRouter>);
}); });

View file

@ -2,48 +2,29 @@
import React from 'react'; import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import NumericalQuestionDisplay from 'src/components/Questions/NumericalQuestionDisplay/NumericalQuestionDisplay'; import NumericalQuestion from 'src/components/Questions/NumericalQuestion/NumericalQuestion';
import { NumericalQuestion, parse, ParsedGIFTQuestion } from 'gift-pegjs';
import { MemoryRouter } from 'react-router-dom';
const questions = parse(
`
::Sample Question 1:: Question stem
{
#5..10
}`
) as ParsedGIFTQuestion[];
const question = questions[0] as NumericalQuestion;
describe('NumericalQuestion parse', () => {
const q = questions[0];
it('The question is Numerical', () => {
expect(q.type).toBe('Numerical');
});
});
describe('NumericalQuestion Component', () => { describe('NumericalQuestion Component', () => {
const mockHandleOnSubmitAnswer = jest.fn(); const mockHandleSubmitAnswer = jest.fn();
const sampleStem = 'Sample question stem';
const sampleProps = { const sampleProps = {
question: question, questionTitle: 'Sample Question',
handleOnSubmitAnswer: mockHandleOnSubmitAnswer, correctAnswers: {
numberHigh: 10,
numberLow: 5,
type: 'high-low'
},
handleOnSubmitAnswer: mockHandleSubmitAnswer,
showAnswer: false showAnswer: false
}; };
beforeEach(() => { beforeEach(() => {
render( render(<NumericalQuestion questionContent={{text: sampleStem, format: 'plain'}} {...sampleProps} />);
<MemoryRouter>
<NumericalQuestionDisplay
{...sampleProps}
/>
</MemoryRouter>);
}); });
it('renders correctly', () => { it('renders correctly', () => {
expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument(); expect(screen.getByText(sampleStem)).toBeInTheDocument();
expect(screen.getByTestId('number-input')).toBeInTheDocument(); expect(screen.getByTestId('number-input')).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument(); expect(screen.getByText('Répondre')).toBeInTheDocument();
}); });
@ -67,7 +48,7 @@ describe('NumericalQuestion Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleOnSubmitAnswer).not.toHaveBeenCalled(); expect(mockHandleSubmitAnswer).not.toHaveBeenCalled();
}); });
it('submits answer correctly', () => { it('submits answer correctly', () => {
@ -78,6 +59,6 @@ describe('NumericalQuestion Component', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(mockHandleOnSubmitAnswer).toHaveBeenCalledWith(7); expect(mockHandleSubmitAnswer).toHaveBeenCalledWith(7);
}); });
}); });

View file

@ -1,40 +1,71 @@
// Question.test.tsx // Question.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent, within } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import QuestionDisplay from 'src/components/Questions/QuestionDisplay'; import Questions from 'src/components/Questions/QuestionDisplay';
import { parse, Question } from 'gift-pegjs'; import { GIFTQuestion } from 'gift-pegjs';
//
describe('Questions Component', () => { describe('Questions Component', () => {
const mockHandleSubmitAnswer = jest.fn(); const mockHandleSubmitAnswer = jest.fn();
const sampleTrueFalseQuestion = const sampleTrueFalseQuestion: GIFTQuestion = {
parse('::Sample True/False Question:: Sample True/False Question {T}')[0]; type: 'TF',
stem: { format: 'plain', text: 'Sample True/False Question' },
const sampleMultipleChoiceQuestion = isTrue: true,
parse('::Sample Multiple Choice Question:: Sample Multiple Choice Question {=Choice 1 ~Choice 2}')[0]; falseFeedback: null,
trueFeedback: null,
const sampleNumericalQuestion = title: 'True/False Question',
parse('::Sample Numerical Question:: Sample Numerical Question {#5..10}')[0]; hasEmbeddedAnswers: false,
globalFeedback: null,
const sampleShortAnswerQuestion =
parse('::Sample Short Answer Question:: Sample Short Answer Question {=Correct Answer =Another Answer}')[0];
const sampleProps = {
handleOnSubmitAnswer: mockHandleSubmitAnswer,
showAnswer: false
}; };
const renderComponent = (question: Question) => { const sampleMultipleChoiceQuestion: GIFTQuestion = {
render(<QuestionDisplay question={question} {...sampleProps} />); type: 'MC',
stem: { format: 'plain', text: 'Sample Multiple Choice Question' },
title: 'Multiple Choice Question',
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: [
{ feedback: null, isCorrect: true, text: { format: 'plain', text: 'Choice 1' }, weight: 1 },
{ feedback: null, isCorrect: false, text: { format: 'plain', text: 'Choice 2' }, weight: 0 },
],
}; };
it('parsed questions correctly', () => { const sampleNumericalQuestion: GIFTQuestion = {
expect(sampleTrueFalseQuestion.type).toBe('TF'); type: 'Numerical',
expect(sampleMultipleChoiceQuestion.type).toBe('MC'); stem: { format: 'plain', text: 'Sample Numerical Question' },
expect(sampleNumericalQuestion.type).toBe('Numerical'); title: 'Numerical Question',
expect(sampleShortAnswerQuestion.type).toBe('Short'); hasEmbeddedAnswers: false,
}); globalFeedback: null,
choices: { numberHigh: 10, numberLow: 5, type: 'high-low' },
};
const sampleShortAnswerQuestion: GIFTQuestion = {
type: 'Short',
stem: { format: 'plain', text: 'Sample short answer question' },
title: 'Short Answer Question Title',
hasEmbeddedAnswers: false,
globalFeedback: null,
choices: [
{
feedback: { format: 'html', text: 'Correct answer feedback' },
isCorrect: true,
text: { format: 'html', text: 'Correct Answer' },
weight: 1,
},
{
feedback: { format: 'html', text: 'Incorrect answer feedback' },
isCorrect: false,
text: { format: 'html', text: 'Incorrect Answer' },
weight: 0,
},
],
};
const renderComponent = (question: GIFTQuestion) => {
render(<Questions question={question} handleOnSubmitAnswer={mockHandleSubmitAnswer} />);
};
it('renders correctly for True/False question', () => { it('renders correctly for True/False question', () => {
renderComponent(sampleTrueFalseQuestion); renderComponent(sampleTrueFalseQuestion);
@ -89,19 +120,15 @@ describe('Questions Component', () => {
it('renders correctly for Short Answer question', () => { it('renders correctly for Short Answer question', () => {
renderComponent(sampleShortAnswerQuestion); renderComponent(sampleShortAnswerQuestion);
expect(screen.getByText('Sample Short Answer Question')).toBeInTheDocument(); expect(screen.getByText('Sample short answer question')).toBeInTheDocument();
const container = screen.getByLabelText('short-answer-input'); expect(screen.getByTestId('text-input')).toBeInTheDocument();
const inputElement = within(container).getByRole('textbox') as HTMLInputElement;
expect(inputElement).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument(); expect(screen.getByText('Répondre')).toBeInTheDocument();
}); });
it('handles input and submission for Short Answer question', () => { it('handles input and submission for Short Answer question', () => {
renderComponent(sampleShortAnswerQuestion); renderComponent(sampleShortAnswerQuestion);
const container = screen.getByLabelText('short-answer-input'); const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
const inputElement = within(container).getByRole('textbox') as HTMLInputElement;
fireEvent.change(inputElement, { target: { value: 'User Input' } }); fireEvent.change(inputElement, { target: { value: 'User Input' } });
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');

View file

@ -1,35 +1,54 @@
// ShortAnswerQuestion.test.tsx // ShortAnswerQuestion.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent, within } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import ShortAnswerQuestionDisplay from 'src/components/Questions/ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay'; import ShortAnswerQuestion from 'src/components/Questions/ShortAnswerQuestion/ShortAnswerQuestion';
import { parse, ShortAnswerQuestion } from 'gift-pegjs';
describe('ShortAnswerQuestion Component', () => { describe('ShortAnswerQuestion Component', () => {
const mockHandleSubmitAnswer = jest.fn(); const mockHandleSubmitAnswer = jest.fn();
const question = const sampleStem = 'Sample question stem';
parse('::Sample Short Answer Question:: Sample Short Answer Question {=Correct Answer ~Incorrect Answer}')[0] as ShortAnswerQuestion;
const sampleProps = { const sampleProps = {
questionTitle: 'Sample Question',
choices: [
{
id: '1',
feedback: {
format: 'text',
text: 'Correct answer feedback'
},
isCorrect: true,
text: {
format: 'text',
text: 'Correct Answer'
}
},
{
id: '2',
feedback: null,
isCorrect: false,
text: {
format: 'text',
text: 'Incorrect Answer'
}
}
],
handleOnSubmitAnswer: mockHandleSubmitAnswer, handleOnSubmitAnswer: mockHandleSubmitAnswer,
showAnswer: false showAnswer: false
}; };
beforeEach(() => { beforeEach(() => {
render(<ShortAnswerQuestionDisplay question={question} {...sampleProps} />); render(<ShortAnswerQuestion questionContent={{text: sampleStem, format: 'plain'}} {...sampleProps} />);
}); });
it('renders correctly', () => { it('renders correctly', () => {
expect(screen.getByText(question.formattedStem.text)).toBeInTheDocument(); expect(screen.getByText(sampleStem)).toBeInTheDocument();
const container = screen.getByLabelText('short-answer-input'); expect(screen.getByTestId('text-input')).toBeInTheDocument();
const inputElement = within(container).getByRole('textbox') as HTMLInputElement;
expect(inputElement).toBeInTheDocument();
expect(screen.getByText('Répondre')).toBeInTheDocument(); expect(screen.getByText('Répondre')).toBeInTheDocument();
}); });
it('handles input change correctly', () => { it('handles input change correctly', () => {
const container = screen.getByLabelText('short-answer-input'); const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
const inputElement = within(container).getByRole('textbox') as HTMLInputElement;
fireEvent.change(inputElement, { target: { value: 'User Input' } }); fireEvent.change(inputElement, { target: { value: 'User Input' } });
@ -51,10 +70,7 @@ describe('ShortAnswerQuestion Component', () => {
}); });
it('submits answer correctly', () => { it('submits answer correctly', () => {
const container = screen.getByLabelText('short-answer-input'); const inputElement = screen.getByTestId('text-input') as HTMLInputElement;
const inputElement = within(container).getByRole('textbox') as HTMLInputElement;
// const inputElement = screen.getByRole('textbox', { name: 'short-answer-input'}) as HTMLInputElement;
const submitButton = screen.getByText('Répondre'); const submitButton = screen.getByText('Répondre');
fireEvent.change(inputElement, { target: { value: 'User Input' } }); fireEvent.change(inputElement, { target: { value: 'User Input' } });

View file

@ -1,30 +0,0 @@
import { parse, NumericalQuestion, SimpleNumericalAnswer, } from "gift-pegjs";
import { isSimpleNumericalAnswer } from "gift-pegjs/typeGuards";
describe('Numerical Question Tests', () => {
// ::Ulysses birthdate::When was Ulysses S. Grant born? {#1822}
it('should produce a valid Question object for a Numerical question with Title', () => {
const input = `
::Ulysses birthdate::When was Ulysses S. Grant born? {#1822}
`;
const result = parse(input);
// Type assertion to ensure result matches the Question interface
const question = result[0];
// Example assertions to check specific properties
expect(question).toHaveProperty('type', 'Numerical');
const numericalQuestion = question as NumericalQuestion;
expect(numericalQuestion.title).toBe('Ulysses birthdate');
expect(numericalQuestion.formattedStem.text).toBe('When was Ulysses S. Grant born?');
expect(numericalQuestion.choices).toBeDefined();
expect(numericalQuestion.choices).toHaveLength(1);
const choice = numericalQuestion.choices[0];
expect(isSimpleNumericalAnswer(choice)).toBe(true);
const c = choice as SimpleNumericalAnswer;
expect(c.type).toBe('simple');
expect(c.number).toBe(1822);
});
});

View file

@ -3,70 +3,71 @@ import React, { useState } from 'react';
import '../questionStyle.css'; import '../questionStyle.css';
import { Button, TextField } from '@mui/material'; import { Button, TextField } from '@mui/material';
import { textType } from '../../GiftTemplate/templates/TextType'; import { textType } from '../../GiftTemplate/templates/TextType';
import { NumericalQuestion, SimpleNumericalAnswer, RangeNumericalAnswer, HighLowNumericalAnswer } from 'gift-pegjs'; import { TextFormat, NumericalAnswer, isHighLowNumericalAnswer, isMultipleNumericalAnswer, isRangeNumericalAnswer, isSimpleNumericalAnswer, SimpleNumericalAnswer, RangeNumericalAnswer, HighLowNumericalAnswer } from 'gift-pegjs';
import { isSimpleNumericalAnswer, isRangeNumericalAnswer, isHighLowNumericalAnswer, isMultipleNumericalAnswer } from 'gift-pegjs/typeGuards';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
// type CorrectAnswer = {
// numberHigh?: number;
// numberLow?: number;
// number?: number;
// type: string;
// };
interface Props { interface Props {
question: NumericalQuestion; questionContent: TextFormat;
correctAnswers: NumericalAnswer;
globalFeedback?: string | undefined;
handleOnSubmitAnswer?: (answer: number) => void; handleOnSubmitAnswer?: (answer: number) => void;
showAnswer?: boolean; showAnswer?: boolean;
} }
const NumericalQuestionDisplay: React.FC<Props> = (props) => { const NumericalQuestion: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer } = const { questionContent, correctAnswers, showAnswer, handleOnSubmitAnswer, globalFeedback } =
props; props;
const [answer, setAnswer] = useState<number>(); const [answer, setAnswer] = useState<number>();
const correctAnswers = question.choices; let correctAnswer= '';
let correctAnswer = '';
//const isSingleAnswer = correctAnswers.length === 1; if (isSimpleNumericalAnswer(correctAnswers)) {
correctAnswer = `${(correctAnswers as SimpleNumericalAnswer).number}`;
if (isSimpleNumericalAnswer(correctAnswers[0])) { } else if (isRangeNumericalAnswer(correctAnswers)) {
correctAnswer = `${(correctAnswers[0] as SimpleNumericalAnswer).number}`; const choice = correctAnswers as RangeNumericalAnswer;
} else if (isRangeNumericalAnswer(correctAnswers[0])) {
const choice = correctAnswers[0] as RangeNumericalAnswer;
correctAnswer = `Entre ${choice.number - choice.range} et ${choice.number + choice.range}`; correctAnswer = `Entre ${choice.number - choice.range} et ${choice.number + choice.range}`;
} else if (isHighLowNumericalAnswer(correctAnswers[0])) { } else if (isHighLowNumericalAnswer(correctAnswers)) {
const choice = correctAnswers[0] as HighLowNumericalAnswer; const choice = correctAnswers as HighLowNumericalAnswer;
correctAnswer = `Entre ${choice.numberLow} et ${choice.numberHigh}`; correctAnswer = `Entre ${choice.numberLow} et ${choice.numberHigh}`;
} else if (isMultipleNumericalAnswer(correctAnswers[0])) { } else if (isMultipleNumericalAnswer(correctAnswers)) {
correctAnswer = `MultipleNumericalAnswer is not supported yet`; correctAnswer = `MultipleNumericalAnswer is not supported yet`;
} else { } else {
throw new Error('Unknown numerical answer type'); throw new Error('Unknown numerical answer type');
} }
return ( return (
<div className="question-wrapper"> <div className="question-wrapper">
<div> <div>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({ text: question.formattedStem })) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div> </div>
{showAnswer ? ( {showAnswer ? (
<> <>
<div className="correct-answer-text mb-2">{correctAnswer}</div> <div className="correct-answer-text mb-2">{correctAnswer}</div>
{question.formattedGlobalFeedback && <div className="global-feedback mb-2"> {globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({ text: question.formattedGlobalFeedback })) }} />
</div>}
</> </>
) : ( ) : (
<> <>
<div className="answer-wrapper mb-1"> <div className="answer-wrapper mb-1">
<TextField <TextField
type="number" type="number"
id={question.formattedStem.text} id={questionContent.text}
name={question.formattedStem.text} name={questionContent.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' }}
/> />
</div> </div>
{question.formattedGlobalFeedback && showAnswer && ( {globalFeedback && showAnswer && (
<div className="global-feedback mb-2"> <div className="global-feedback mb-2">{globalFeedback}</div>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({ text: question.formattedGlobalFeedback })) }} />
</div>
)} )}
{handleOnSubmitAnswer && ( {handleOnSubmitAnswer && (
<Button <Button
@ -87,4 +88,4 @@ const NumericalQuestionDisplay: React.FC<Props> = (props) => {
); );
}; };
export default NumericalQuestionDisplay; export default NumericalQuestion;

View file

@ -1,12 +1,12 @@
// Question;tsx // Question;tsx
import React from 'react'; import React, { useMemo } from 'react';
import { Question } from 'gift-pegjs'; import { Question } from 'gift-pegjs';
import TrueFalseQuestion from './TrueFalseQuestion/TrueFalseQuestion'; import TrueFalseQuestion from './TrueFalseQuestion/TrueFalseQuestion';
import MultipleChoiceQuestionDisplay from './MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay'; import MultipleChoiceQuestionDisplay from './MultipleChoiceQuestionDisplay/MultipleChoiceQuestionDisplay';
import NumericalQuestionDisplay from './NumericalQuestionDisplay/NumericalQuestionDisplay'; import NumericalQuestion from './NumericalQuestion/NumericalQuestion';
import ShortAnswerQuestionDisplay from './ShortAnswerQuestionDisplay/ShortAnswerQuestionDisplay'; import ShortAnswerQuestion from './ShortAnswerQuestion/ShortAnswerQuestion';
// import useCheckMobileScreen from '../../services/useCheckMobileScreen'; import useCheckMobileScreen from '../../services/useCheckMobileScreen';
interface QuestionProps { interface QuestionProps {
question: Question; question: Question;
@ -18,10 +18,10 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
handleOnSubmitAnswer, handleOnSubmitAnswer,
showAnswer, showAnswer,
}) => { }) => {
// const isMobile = useCheckMobileScreen(); const isMobile = useCheckMobileScreen();
// const imgWidth = useMemo(() => { const imgWidth = useMemo(() => {
// return isMobile ? '100%' : '20%'; return isMobile ? '100%' : '20%';
// }, [isMobile]); }, [isMobile]);
let questionTypeComponent = null; let questionTypeComponent = null;
switch (question?.type) { switch (question?.type) {
@ -49,18 +49,22 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
if (question.choices) { if (question.choices) {
if (!Array.isArray(question.choices)) { if (!Array.isArray(question.choices)) {
questionTypeComponent = ( questionTypeComponent = (
<NumericalQuestionDisplay <NumericalQuestion
question={question} questionContent={question.formattedStem}
correctAnswers={question.choices}
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer} showAnswer={showAnswer}
globalFeedback={question.formattedGlobalFeedback?.text}
/> />
); );
} else { } else {
questionTypeComponent = ( // TODO fix NumericalQuestion (correctAnswers is borked) questionTypeComponent = ( // TODO fix NumericalQuestion (correctAnswers is borked)
<NumericalQuestionDisplay <NumericalQuestion
question={question} questionContent={question.formattedStem}
correctAnswers={question.choices}
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer} showAnswer={showAnswer}
globalFeedback={question.formattedGlobalFeedback?.text}
/> />
); );
} }
@ -68,10 +72,12 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
break; break;
case 'Short': case 'Short':
questionTypeComponent = ( questionTypeComponent = (
<ShortAnswerQuestionDisplay <ShortAnswerQuestion
question={question} questionContent={question.formattedStem}
choices={question.choices.map((choice, index) => ({ ...choice, id: index.toString() }))}
handleOnSubmitAnswer={handleOnSubmitAnswer} handleOnSubmitAnswer={handleOnSubmitAnswer}
showAnswer={showAnswer} showAnswer={showAnswer}
globalFeedback={question.formattedGlobalFeedback?.text}
/> />
); );
break; break;
@ -80,6 +86,13 @@ const QuestionDisplay: React.FC<QuestionProps> = ({
<div className="question-container"> <div className="question-container">
{questionTypeComponent ? ( {questionTypeComponent ? (
<> <>
{imageUrl && (
<img
src={imageUrl}
alt="QuestionImage"
style={{ width: imgWidth, marginBottom: '2rem' }}
/>
)}
{questionTypeComponent} {questionTypeComponent}
</> </>
) : ( ) : (

View file

@ -1,50 +1,59 @@
// ShortAnswerQuestion.tsx
import React, { useState } from 'react'; import React, { useState } from 'react';
import '../questionStyle.css'; import '../questionStyle.css';
import { Button, TextField } from '@mui/material'; import { Button, TextField } from '@mui/material';
import { textType } from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { ShortAnswerQuestion } from 'gift-pegjs'; import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
type Choices = {
feedback: { format: string; text: string } | null;
isCorrect: boolean;
text: { format: string; text: string };
weigth?: number;
id: string;
};
interface Props { interface Props {
question: ShortAnswerQuestion; questionContent: TextFormat;
choices: Choices[];
globalFeedback?: string | undefined;
handleOnSubmitAnswer?: (answer: string) => void; handleOnSubmitAnswer?: (answer: string) => void;
showAnswer?: boolean; showAnswer?: boolean;
} }
const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => { const ShortAnswerQuestion: React.FC<Props> = (props) => {
const { question, showAnswer, handleOnSubmitAnswer } = props; const { questionContent, choices, showAnswer, handleOnSubmitAnswer, globalFeedback } = props;
const [answer, setAnswer] = useState<string>(); const [answer, setAnswer] = useState<string>();
return ( return (
<div className="question-wrapper"> <div className="question-wrapper">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: question.formattedStem})) }} /> <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: questionContent})) }} />
</div> </div>
{showAnswer ? ( {showAnswer ? (
<> <>
<div className="correct-answer-text mb-1"> <div className="correct-answer-text mb-1">
{question.choices.map((choice) => ( {choices.map((choice) => (
<div key={choice.text} className="mb-1"> <div key={choice.id} className="mb-1">
{choice.text} {choice.text.text}
</div> </div>
))} ))}
</div> </div>
{question.formattedGlobalFeedback && <div className="global-feedback mb-2"> {globalFeedback && <div className="global-feedback mb-2">{globalFeedback}</div>}
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(textType({text: question.formattedGlobalFeedback})) }} />
</div>}
</> </>
) : ( ) : (
<> <>
<div className="answer-wrapper mb-1"> <div className="answer-wrapper mb-1">
<TextField <TextField
type="text" type="text"
id={question.formattedStem.text} id={questionContent.text}
name={question.formattedStem.text} name={questionContent.text}
onChange={(e) => { onChange={(e) => {
setAnswer(e.target.value); setAnswer(e.target.value);
}} }}
disabled={showAnswer} disabled={showAnswer}
aria-label="short-answer-input" inputProps={{ 'data-testid': 'text-input' }}
/> />
</div> </div>
{handleOnSubmitAnswer && ( {handleOnSubmitAnswer && (
@ -66,4 +75,4 @@ const ShortAnswerQuestionDisplay: React.FC<Props> = (props) => {
); );
}; };
export default ShortAnswerQuestionDisplay; export default ShortAnswerQuestion;

View file

@ -2,8 +2,8 @@
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 { textType } from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from 'gift-pegjs'; import { TextFormat } from '../../GiftTemplate/templates/types';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
interface Props { interface Props {

28
package-lock.json generated Normal file
View file

@ -0,0 +1,28 @@
{
"name": "EvalueTonSavoir",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"dompurify": "^3.2.3"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/dompurify": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
"integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
}
}
}

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"dependencies": {
"dompurify": "^3.2.3"
}
}