Skip to content

fix: answer range format validation and error message in Numerical input #1557

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -47,6 +48,11 @@ const AnswerOption = ({
staticRootUrl = `${getConfig().STUDIO_BASE_URL }/library_assets/blocks/${ blockId }/`;
}

const validateAnswerTitle = (value) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this function called validateAnswerTitle ; shouldn't it be validateAnswerRange ?

const cleanedValue = value.replace(/^\s+|\s+$/g, '');
return !cleanedValue.length || answerRangeFormatRegex.test(cleanedValue);
};

const getInputArea = () => {
if ([ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType)) {
return (
Expand Down Expand Up @@ -78,8 +84,9 @@ const AnswerOption = ({
);
}
// Return Answer Range View
const isValidValue = validateAnswerTitle(answer.title);
return (
<div>
<Form.Group isInvalid={!isValidValue}>
<Form.Control
as="textarea"
className="answer-option-textarea text-gray-500 small"
Expand All @@ -89,11 +96,15 @@ const AnswerOption = ({
onChange={setAnswerTitle}
placeholder={intl.formatMessage(messages.answerRangeTextboxPlaceholder)}
/>
{!isValidValue && (
<Form.Control.Feedback type="invalid">
<FormattedMessage {...messages.answerRangeErrorText} />
</Form.Control.Feedback>
)}
<div className="pgn__form-switch-helper-text">
<FormattedMessage {...messages.answerRangeHelperText} />
</div>
</div>

</Form.Group>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 3 additions & 1 deletion src/editors/containers/ProblemEditor/data/OLXParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export const responseKeys = [
'choicetextresponse',
];

export const answerRangeFormatRegex = /^[([]\s*\d+(\.\d+)?\s*,\s*\d+(\.\d+)?\s*[)\]]$/m;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a JSDoc comment explaining what this is in detail, including examples of valid values.


export const stripNonTextTags = ({ input, tag }) => {
const stripedTags = {};
Object.entries(input).forEach(([key, value]) => {
Expand Down Expand Up @@ -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'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,16 +413,18 @@
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;

Check warning on line 420 in src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L420 was not covered by tests
} 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
numericInputWithAnswerRange,
textInputWithFeedbackAndHintsWithMultipleAnswers,
numberParseTest,
numericInputWithFractionBounds,
numericInputWithEmptyUpperBound,
numericInputWithSwappedBounds,
numericInputWithMissingUpperBound,
} from './mockData/editorTestData';
import ReactStateOLXParser from './ReactStateOLXParser';

Expand Down Expand Up @@ -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]');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,51 @@ export const numberParseTest = {
hints: [],
question: '<p>What is the content of the register x2 after executing the following three lines of instructions?</p>',
};

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: {},
};