diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx index a0ee440b62..07d7130045 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx @@ -18,6 +18,7 @@ import { FeedbackBox } from './components/Feedback'; import * as hooks from './hooks'; import { ProblemTypeKeys } from '../../../../../data/constants/problem'; import ExpandableTextArea from '../../../../../sharedComponents/ExpandableTextArea'; +import { answerRangeFormatRegex } from '../../../data/OLXParser'; const AnswerOption = ({ answer, @@ -47,6 +48,11 @@ const AnswerOption = ({ staticRootUrl = `${getConfig().STUDIO_BASE_URL }/library_assets/blocks/${ blockId }/`; } + const validateAnswerTitle = (value) => { + const cleanedValue = value.replace(/^\s+|\s+$/g, ''); + return !cleanedValue.length || answerRangeFormatRegex.test(cleanedValue); + }; + const getInputArea = () => { if ([ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType)) { return ( @@ -78,8 +84,9 @@ const AnswerOption = ({ ); } // Return Answer Range View + const isValidValue = validateAnswerTitle(answer.title); return ( -
+ + {!isValidValue && ( + + + + )}
-
- + ); }; diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js index 5f58109137..d69bc876d7 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js @@ -77,6 +77,11 @@ const messages = defineMessages({ 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).', description: 'Helper text describing usage of answer ranges', }, + answerRangeErrorText: { + id: 'authoring.answerwidget.answer.answerRangeErrorText', + defaultMessage: 'Error: Invalid range format. Use brackets or parentheses with values separated by a comma.', + description: 'Error text describing wrong format of answer ranges', + }, }); export default messages; diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.js b/src/editors/containers/ProblemEditor/data/OLXParser.js index 4a390e68cc..b59f57efde 100644 --- a/src/editors/containers/ProblemEditor/data/OLXParser.js +++ b/src/editors/containers/ProblemEditor/data/OLXParser.js @@ -57,6 +57,8 @@ export const responseKeys = [ 'choicetextresponse', ]; +export const answerRangeFormatRegex = /^[([]\s*\d+(\.\d+)?\s*,\s*\d+(\.\d+)?\s*[)\]]$/m; + export const stripNonTextTags = ({ input, tag }) => { const stripedTags = {}; Object.entries(input).forEach(([key, value]) => { @@ -432,7 +434,7 @@ export class OLXParser { [type]: defaultValue, }; } - const isAnswerRange = /[([]\s*\d*,\s*\d*\s*[)\]]/gm.test(numericalresponse['@_answer']); + const isAnswerRange = answerRangeFormatRegex.test(numericalresponse['@_answer']); answers.push({ id: indexToLetterMap[answers.length], title: numericalresponse['@_answer'], diff --git a/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js b/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js index 5f2d8ca9f7..279294750a 100644 --- a/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js +++ b/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js @@ -413,16 +413,18 @@ class ReactStateOLXParser { const lowerBoundFloat = Number(numerator) / Number(denominator); lowerBoundInt = lowerBoundFloat; } else { - // these regex replaces remove everything that is not a decimal or positive/negative numer + // these regex replaces remove everything that is not a decimal or positive/negative number lowerBoundInt = Number(rawLowerBound.replace(/[^0-9-.]/gm, '')); } - if (rawUpperBound.includes('/')) { + if (!rawUpperBound) { + upperBoundInt = lowerBoundInt; + } else if (rawUpperBound.includes('/')) { upperBoundFraction = rawUpperBound.replace(/[^0-9-/]/gm, ''); const [numerator, denominator] = upperBoundFraction.split('/'); const upperBoundFloat = Number(numerator) / Number(denominator); upperBoundInt = upperBoundFloat; } else { - // these regex replaces remove everything that is not a decimal or positive/negative numer + // these regex replaces remove everything that is not a decimal or positive/negative number upperBoundInt = Number(rawUpperBound.replace(/[^0-9-.]/gm, '')); } if (lowerBoundInt > upperBoundInt) { diff --git a/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.test.js b/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.test.js index 416fafdcd8..7b852ca64b 100644 --- a/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.test.js +++ b/src/editors/containers/ProblemEditor/data/ReactStateOLXParser.test.js @@ -18,6 +18,10 @@ import { numericInputWithAnswerRange, textInputWithFeedbackAndHintsWithMultipleAnswers, numberParseTest, + numericInputWithFractionBounds, + numericInputWithEmptyUpperBound, + numericInputWithSwappedBounds, + numericInputWithMissingUpperBound, } from './mockData/editorTestData'; import ReactStateOLXParser from './ReactStateOLXParser'; @@ -147,4 +151,41 @@ describe('Check React State OLXParser problem', () => { ); }); }); + describe('ReactStateOLXParser numerical response range parsing', () => { + test('handles empty upper bound as same as lower', () => { + const parser = new ReactStateOLXParser({ + problem: numericInputWithEmptyUpperBound, + editorObject: numericInputWithEmptyUpperBound, + }); + const result = parser.buildNumericalResponse(); + expect(result[':@']['@_answer']).toBe('[0,1.5]'); + }); + + test('handles swapped bounds and corrects order', () => { + const parser = new ReactStateOLXParser({ + problem: numericInputWithSwappedBounds, + editorObject: numericInputWithSwappedBounds, + }); + const result = parser.buildNumericalResponse(); + expect(result[':@']['@_answer']).toBe('[2,5]'); + }); + + test('fixes swapped fraction bounds and preserves brackets', () => { + const parser = new ReactStateOLXParser({ + problem: numericInputWithFractionBounds, + editorObject: numericInputWithFractionBounds, + }); + const result = parser.buildNumericalResponse(); + expect(result[':@']['@_answer']).toBe('(1/2,3/2)'); + }); + + test('sets upper bound = lower bound if upper bound missing', () => { + const parser = new ReactStateOLXParser({ + problem: numericInputWithMissingUpperBound, + editorObject: numericInputWithMissingUpperBound, + }); + const result = parser.buildNumericalResponse(); + expect(result[':@']['@_answer']).toBe('[,2.5]'); + }); + }); }); diff --git a/src/editors/containers/ProblemEditor/data/mockData/editorTestData.js b/src/editors/containers/ProblemEditor/data/mockData/editorTestData.js index a96230c46e..a33d947e41 100644 --- a/src/editors/containers/ProblemEditor/data/mockData/editorTestData.js +++ b/src/editors/containers/ProblemEditor/data/mockData/editorTestData.js @@ -130,3 +130,51 @@ export const numberParseTest = { hints: [], question: '

What is the content of the register x2 after executing the following three lines of instructions?

', }; + +export const numericInputWithEmptyUpperBound = { + answers: [ + { + id: 'a1', + title: '[1.5,]', + correct: true, + }, + ], + problemType: 'numericalresponse', + settings: {}, +}; + +export const numericInputWithSwappedBounds = { + answers: [ + { + id: 'a1', + title: '[5,2]', + correct: true, + }, + ], + problemType: 'numericalresponse', + settings: {}, +}; + +export const numericInputWithFractionBounds = { + answers: [ + { + id: 'a1', + title: '(3/2,1/2)', + correct: true, + }, + ], + problemType: 'numericalresponse', + settings: {}, +}; + +export const numericInputWithMissingUpperBound = { + answers: [ + { + id: 'a1', + title: '[,2.5]', + correct: true, + }, + ], + problemType: 'numericalresponse', + settings: {}, +};