Skip to content

Commit 23aa397

Browse files
committed
fix: answer range format validation and error message in Numerical input
1 parent 73490a5 commit 23aa397

File tree

6 files changed

+144
-13
lines changed

6 files changed

+144
-13
lines changed

src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { FeedbackBox } from './components/Feedback';
1717
import * as hooks from './hooks';
1818
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
1919
import ExpandableTextArea from '../../../../../sharedComponents/ExpandableTextArea';
20+
import { answerRangeFormatRegex } from '../../../data/OLXParser';
2021

2122
const AnswerOption = ({
2223
answer,
@@ -42,6 +43,11 @@ const AnswerOption = ({
4243
const setUnselectedFeedback = hooks.setUnselectedFeedback({ answer, hasSingleAnswer, dispatch });
4344
const { isFeedbackVisible, toggleFeedback } = hooks.useFeedback(answer);
4445

46+
const validateAnswerTitle = (value) => {
47+
const cleanedValue = value.replace(/^\s+|\s+$/g, '');
48+
return !cleanedValue.length || answerRangeFormatRegex.test(cleanedValue);
49+
};
50+
4551
const getInputArea = () => {
4652
if ([ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType)) {
4753
return (
@@ -72,8 +78,9 @@ const AnswerOption = ({
7278
);
7379
}
7480
// Return Answer Range View
81+
const isValidValue = validateAnswerTitle(answer.title);
7582
return (
76-
<div>
83+
<Form.Group isInvalid={!isValidValue}>
7784
<Form.Control
7885
as="textarea"
7986
className="answer-option-textarea text-gray-500 small"
@@ -83,11 +90,15 @@ const AnswerOption = ({
8390
onChange={setAnswerTitle}
8491
placeholder={intl.formatMessage(messages.answerRangeTextboxPlaceholder)}
8592
/>
93+
{!isValidValue && (
94+
<Form.Control.Feedback type="invalid">
95+
<FormattedMessage {...messages.answerRangeErrorText} />
96+
</Form.Control.Feedback>
97+
)}
8698
<div className="pgn__form-switch-helper-text">
8799
<FormattedMessage {...messages.answerRangeHelperText} />
88100
</div>
89-
</div>
90-
101+
</Form.Group>
91102
);
92103
};
93104

src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('AnswerOption', () => {
4141
};
4242
const answerRange = {
4343
id: 'A',
44-
title: 'Answer 1',
44+
title: '[2,5]',
4545
correct: true,
4646
selectedFeedback: 'selected feedback',
4747
unselectedFeedback: 'unselected feedback',
@@ -72,6 +72,9 @@ describe('AnswerOption', () => {
7272
test('snapshot: renders correct option with numeric input problem and answer range', () => {
7373
expect(shallow(<AnswerOption {...props} problemType="numericalresponse" answer={answerRange} />).snapshot).toMatchSnapshot();
7474
});
75+
test('snapshot: renders incorrect option with numeric input problem and answer range', () => {
76+
expect(shallow(<AnswerOption {...props} problemType="numericalresponse" answer={{ ...answerRange, title: '[2.5]' }} />).snapshot).toMatchSnapshot();
77+
});
7578
});
7679

7780
describe('mapStateToProps', () => {

src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswerOption.test.jsx.snap

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ exports[`AnswerOption render snapshot: renders correct option with numeric input
178178
"id": "A",
179179
"isAnswerRange": true,
180180
"selectedFeedback": "selected feedback",
181-
"title": "Answer 1",
181+
"title": "[2,5]",
182182
"unselectedFeedback": "unselected feedback",
183183
}
184184
}
@@ -190,15 +190,17 @@ exports[`AnswerOption render snapshot: renders correct option with numeric input
190190
<div
191191
className="ml-1 flex-grow-1"
192192
>
193-
<div>
193+
<Form.Group
194+
isInvalid={false}
195+
>
194196
<Form.Control
195197
as="textarea"
196198
autoResize={true}
197199
className="answer-option-textarea text-gray-500 small"
198200
onChange={[Function]}
199201
placeholder="Enter an answer range"
200202
rows={1}
201-
value="Answer 1"
203+
value="[2,5]"
202204
/>
203205
<div
204206
className="pgn__form-switch-helper-text"
@@ -209,7 +211,7 @@ exports[`AnswerOption render snapshot: renders correct option with numeric input
209211
id="authoring.answerwidget.answer.answerRangeHelperText"
210212
/>
211213
</div>
212-
</div>
214+
</Form.Group>
213215
<Body>
214216
<injectIntl(ShimmedIntlComponent)
215217
answer={
@@ -218,7 +220,7 @@ exports[`AnswerOption render snapshot: renders correct option with numeric input
218220
"id": "A",
219221
"isAnswerRange": true,
220222
"selectedFeedback": "selected feedback",
221-
"title": "Answer 1",
223+
"title": "[2,5]",
222224
"unselectedFeedback": "unselected feedback",
223225
}
224226
}
@@ -340,3 +342,109 @@ exports[`AnswerOption render snapshot: renders correct option with selected unse
340342
</div>
341343
</Advanced>
342344
`;
345+
346+
exports[`AnswerOption render snapshot: renders incorrect option with numeric input problem and answer range 1`] = `
347+
<Advanced
348+
className="answer-option d-flex flex-row justify-content-between flex-nowrap pb-2 pt-2"
349+
onToggle={[Function]}
350+
open={false}
351+
>
352+
<div
353+
className="mr-1 d-flex"
354+
>
355+
<Checker
356+
answer={
357+
{
358+
"correct": true,
359+
"id": "A",
360+
"isAnswerRange": true,
361+
"selectedFeedback": "selected feedback",
362+
"title": "[2.5]",
363+
"unselectedFeedback": "unselected feedback",
364+
}
365+
}
366+
disabled={true}
367+
hasSingleAnswer={false}
368+
setAnswer={[Function]}
369+
/>
370+
</div>
371+
<div
372+
className="ml-1 flex-grow-1"
373+
>
374+
<Form.Group
375+
isInvalid={true}
376+
>
377+
<Form.Control
378+
as="textarea"
379+
autoResize={true}
380+
className="answer-option-textarea text-gray-500 small"
381+
onChange={[Function]}
382+
placeholder="Enter an answer range"
383+
rows={1}
384+
value="[2.5]"
385+
/>
386+
<Form.Control.Feedback
387+
type="invalid"
388+
>
389+
<FormattedMessage
390+
defaultMessage="Error: Invalid range format. Use brackets or parentheses with values separated by a comma."
391+
description="Error text describing wrong format of answer ranges"
392+
id="authoring.answerwidget.answer.answerRangeErrorText"
393+
/>
394+
</Form.Control.Feedback>
395+
<div
396+
className="pgn__form-switch-helper-text"
397+
>
398+
<FormattedMessage
399+
defaultMessage="Enter min and max values separated by a comma. Use a bracket to include the number next to it in the range, or a parenthesis to exclude the number. For example, to identify the correct answers as 5, 6, or 7, but not 8, specify [5,8)."
400+
description="Helper text describing usage of answer ranges"
401+
id="authoring.answerwidget.answer.answerRangeHelperText"
402+
/>
403+
</div>
404+
</Form.Group>
405+
<Body>
406+
<injectIntl(ShimmedIntlComponent)
407+
answer={
408+
{
409+
"correct": true,
410+
"id": "A",
411+
"isAnswerRange": true,
412+
"selectedFeedback": "selected feedback",
413+
"title": "[2.5]",
414+
"unselectedFeedback": "unselected feedback",
415+
}
416+
}
417+
images={{}}
418+
intl={
419+
{
420+
"formatMessage": [Function],
421+
}
422+
}
423+
isLibrary={false}
424+
learningContextId="course+org+run"
425+
problemType="numericalresponse"
426+
setSelectedFeedback={[Function]}
427+
setUnselectedFeedback={[Function]}
428+
/>
429+
</Body>
430+
</div>
431+
<div
432+
className="d-flex flex-row flex-nowrap"
433+
>
434+
<Trigger
435+
aria-label="Toggle feedback"
436+
className="btn-icon btn-icon-primary btn-icon-md align-items-center"
437+
>
438+
<Icon
439+
alt="Toggle feedback"
440+
/>
441+
</Trigger>
442+
<IconButton
443+
alt="Delete answer"
444+
iconAs="Icon"
445+
onClick={[Function]}
446+
variant="primary"
447+
/>
448+
</div>
449+
</Advanced>
450+
`;

src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ const messages = defineMessages({
7272
defaultMessage: 'Enter min and max values separated by a comma. Use a bracket to include the number next to it in the range, or a parenthesis to exclude the number. For example, to identify the correct answers as 5, 6, or 7, but not 8, specify [5,8).',
7373
description: 'Helper text describing usage of answer ranges',
7474
},
75+
answerRangeErrorText: {
76+
id: 'authoring.answerwidget.answer.answerRangeErrorText',
77+
defaultMessage: 'Error: Invalid range format. Use brackets or parentheses with values separated by a comma.',
78+
description: 'Error text describing wrong format of answer ranges',
79+
},
7580
});
7681

7782
export default messages;

src/editors/containers/ProblemEditor/data/OLXParser.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export const responseKeys = [
5555
'choicetextresponse',
5656
];
5757

58+
export const answerRangeFormatRegex = /^[([]\s*\d+(\.\d+)?\s*,\s*\d+(\.\d+)?\s*[)\]]$/m;
59+
5860
export const stripNonTextTags = ({ input, tag }) => {
5961
const stripedTags = {};
6062
Object.entries(input).forEach(([key, value]) => {
@@ -423,7 +425,7 @@ export class OLXParser {
423425
[type]: defaultValue,
424426
};
425427
}
426-
const isAnswerRange = /[([]\s*\d*,\s*\d*\s*[)\]]/gm.test(numericalresponse['@_answer']);
428+
const isAnswerRange = answerRangeFormatRegex.test(numericalresponse['@_answer']);
427429
answers.push({
428430
id: indexToLetterMap[answers.length],
429431
title: numericalresponse['@_answer'],

src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -413,16 +413,18 @@ class ReactStateOLXParser {
413413
const lowerBoundFloat = Number(numerator) / Number(denominator);
414414
lowerBoundInt = lowerBoundFloat;
415415
} else {
416-
// these regex replaces remove everything that is not a decimal or positive/negative numer
416+
// these regex replaces remove everything that is not a decimal or positive/negative number
417417
lowerBoundInt = Number(rawLowerBound.replace(/[^0-9-.]/gm, ''));
418418
}
419-
if (rawUpperBound.includes('/')) {
419+
if (!rawUpperBound) {
420+
upperBoundInt = lowerBoundInt;
421+
} else if (rawUpperBound.includes('/')) {
420422
upperBoundFraction = rawUpperBound.replace(/[^0-9-/]/gm, '');
421423
const [numerator, denominator] = upperBoundFraction.split('/');
422424
const upperBoundFloat = Number(numerator) / Number(denominator);
423425
upperBoundInt = upperBoundFloat;
424426
} else {
425-
// these regex replaces remove everything that is not a decimal or positive/negative numer
427+
// these regex replaces remove everything that is not a decimal or positive/negative number
426428
upperBoundInt = Number(rawUpperBound.replace(/[^0-9-.]/gm, ''));
427429
}
428430
if (lowerBoundInt > upperBoundInt) {

0 commit comments

Comments
 (0)