feat: add answer range (#291)
* feat: add answer range * feat: add margin for doropdown * fix: improve test converage
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
Form,
|
||||
} from '@edx/paragon';
|
||||
import { FeedbackOutline, DeleteOutline } from '@edx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import { selectors } from '../../../../../data/redux';
|
||||
import { answerOptionProps } from '../../../../../data/services/cms/types';
|
||||
@@ -38,6 +38,53 @@ export const AnswerOption = ({
|
||||
const setSelectedFeedback = hooks.setSelectedFeedback({ answer, hasSingleAnswer, dispatch });
|
||||
const setUnselectedFeedback = hooks.setUnselectedFeedback({ answer, hasSingleAnswer, dispatch });
|
||||
const { isFeedbackVisible, toggleFeedback } = hooks.useFeedback(answer);
|
||||
|
||||
const getInputArea = () => {
|
||||
if ([ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType)) {
|
||||
return (
|
||||
<ExpandableTextArea
|
||||
value={answer.title}
|
||||
setContent={setAnswerTitle}
|
||||
placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
|
||||
id={`answer-${answer.id}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (problemType !== ProblemTypeKeys.NUMERIC || !answer.isAnswerRange) {
|
||||
return (
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="answer-option-textarea text-gray-500 small"
|
||||
autoResize
|
||||
rows={1}
|
||||
value={answer.title}
|
||||
onChange={setAnswerTitle}
|
||||
placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// Return Answer Range View
|
||||
return (
|
||||
<div>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="answer-option-textarea text-gray-500 small"
|
||||
autoResize
|
||||
rows={1}
|
||||
value={answer.title}
|
||||
onChange={setAnswerTitle}
|
||||
placeholder={intl.formatMessage(messages.answerRangeTextboxPlaceholder)}
|
||||
/>
|
||||
<hr className="d-block" />
|
||||
<div className="pgn__form-switch-helper-text">
|
||||
<FormattedMessage {...messages.answerRangeHelperText} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Advanced
|
||||
open={isFeedbackVisible}
|
||||
@@ -53,24 +100,7 @@ export const AnswerOption = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-1 flex-grow-1">
|
||||
{[ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType) ? (
|
||||
<ExpandableTextArea
|
||||
value={answer.title}
|
||||
setContent={setAnswerTitle}
|
||||
placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
|
||||
id={`answer-${answer.id}`}
|
||||
/>
|
||||
) : (
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="answer-option-textarea text-gray-500 small"
|
||||
autoResize
|
||||
rows={1}
|
||||
value={answer.title}
|
||||
onChange={setAnswerTitle}
|
||||
placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
|
||||
/>
|
||||
)}
|
||||
{getInputArea()}
|
||||
<Collapsible.Body>
|
||||
<FeedbackBox
|
||||
problemType={problemType}
|
||||
|
||||
@@ -26,6 +26,14 @@ describe('AnswerOption', () => {
|
||||
selectedFeedback: 'selected feedback',
|
||||
unselectedFeedback: 'unselected feedback',
|
||||
};
|
||||
const answerRange = {
|
||||
id: 'A',
|
||||
title: 'Answer 1',
|
||||
correct: true,
|
||||
selectedFeedback: 'selected feedback',
|
||||
unselectedFeedback: 'unselected feedback',
|
||||
isAnswerRange: true,
|
||||
};
|
||||
|
||||
const props = {
|
||||
hasSingleAnswer: false,
|
||||
@@ -45,7 +53,11 @@ describe('AnswerOption', () => {
|
||||
test('snapshot: renders correct option with numeric input problem', () => {
|
||||
expect(shallow(<AnswerOption {...props} problemType="numericalresponse" />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: renders correct option with numeric input problem and answer range', () => {
|
||||
expect(shallow(<AnswerOption {...props} problemType="numericalresponse" answer={answerRange} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('problemType from problem.problemType', () => {
|
||||
|
||||
@@ -3,18 +3,22 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Dropdown, Icon } from '@edx/paragon';
|
||||
import { Add } from '@edx/paragon/icons';
|
||||
import messages from './messages';
|
||||
import { useAnswerContainer, isSingleAnswerProblem } from './hooks';
|
||||
import { actions, selectors } from '../../../../../data/redux';
|
||||
import { answerOptionProps } from '../../../../../data/services/cms/types';
|
||||
import AnswerOption from './AnswerOption';
|
||||
import Button from '../../../../../sharedComponents/Button';
|
||||
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
|
||||
|
||||
export const AnswersContainer = ({
|
||||
problemType,
|
||||
// Redux
|
||||
answers,
|
||||
addAnswer,
|
||||
addAnswerRange,
|
||||
updateField,
|
||||
}) => {
|
||||
const hasSingleAnswer = isSingleAnswerProblem(problemType);
|
||||
@@ -30,12 +34,45 @@ export const AnswersContainer = ({
|
||||
answer={answer}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
variant="add"
|
||||
onClick={addAnswer}
|
||||
>
|
||||
<FormattedMessage {...messages.addAnswerButtonText} />
|
||||
</Button>
|
||||
|
||||
{problemType !== ProblemTypeKeys.NUMERIC ? (
|
||||
|
||||
<Button
|
||||
variant="add"
|
||||
onClick={addAnswer}
|
||||
>
|
||||
<FormattedMessage {...messages.addAnswerButtonText} />
|
||||
</Button>
|
||||
|
||||
) : (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="Add-Answer-Or-Answer-Range"
|
||||
variant="tertiary"
|
||||
>
|
||||
<Icon
|
||||
src={Add}
|
||||
/>
|
||||
<FormattedMessage {...messages.addAnswerButtonText} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="add-answer"
|
||||
onClick={addAnswer}
|
||||
className={`AddAnswerRange ${answers.length === 1 && answers[0].isAnswerRange ? 'disabled' : ''}`}
|
||||
>
|
||||
<FormattedMessage {...messages.addAnswerButtonText} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="add-answer-range"
|
||||
onClick={addAnswerRange}
|
||||
className={`AddAnswerRange ${answers.length > 1 || (answers.length === 1 && answers[0].isAnswerRange) ? 'disabled' : ''}`}
|
||||
>
|
||||
<FormattedMessage {...messages.addAnswerRangeButtonText} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -44,6 +81,7 @@ AnswersContainer.propTypes = {
|
||||
problemType: PropTypes.string.isRequired,
|
||||
answers: PropTypes.arrayOf(answerOptionProps).isRequired,
|
||||
addAnswer: PropTypes.func.isRequired,
|
||||
addAnswerRange: PropTypes.func.isRequired,
|
||||
updateField: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -53,6 +91,7 @@ export const mapStateToProps = (state) => ({
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
addAnswer: actions.problem.addAnswer,
|
||||
addAnswerRange: actions.problem.addAnswerRange,
|
||||
updateField: actions.problem.updateField,
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { actions, selectors } from '../../../../../data/redux';
|
||||
import * as module from './AnswersContainer';
|
||||
|
||||
import { AnswersContainer as AnswersContainerWithoutHOC } from './AnswersContainer';
|
||||
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
FormattedMessage: ({ defaultMessage }) => (<p>{defaultMessage}</p>),
|
||||
@@ -54,6 +55,77 @@ describe('AnswersContainer', () => {
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
test('snapshot: numeric problems: answer range/answer select button: empty', () => {
|
||||
act(() => {
|
||||
const emptyAnswerProps = {
|
||||
problemType: ProblemTypeKeys.NUMERIC,
|
||||
answers: [],
|
||||
updateField: jest.fn(),
|
||||
addAnswer: jest.fn(),
|
||||
addAnswerRange: jest.fn(),
|
||||
};
|
||||
expect(shallow(
|
||||
<module.AnswersContainer
|
||||
{...emptyAnswerProps}
|
||||
/>,
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
test('snapshot: numeric problems: answer range/answer select button: Range disables the additon of more adds', () => {
|
||||
act(() => {
|
||||
const answerRangeProps = {
|
||||
problemType: ProblemTypeKeys.NUMERIC,
|
||||
answers: [{
|
||||
id: 'A',
|
||||
title: 'Answer 1',
|
||||
correct: true,
|
||||
selectedFeedback: 'selected feedback',
|
||||
unselectedFeedback: 'unselected feedback',
|
||||
isAnswerRange: true,
|
||||
}],
|
||||
updateField: jest.fn(),
|
||||
addAnswer: jest.fn(),
|
||||
addAnswerRange: jest.fn(),
|
||||
};
|
||||
expect(shallow(
|
||||
<module.AnswersContainer
|
||||
{...answerRangeProps}
|
||||
/>,
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
test('snapshot: numeric problems: answer range/answer select button: multiple answers disables range.', () => {
|
||||
act(() => {
|
||||
const answersProps = {
|
||||
problemType: ProblemTypeKeys.NUMERIC,
|
||||
answers: [{
|
||||
id: 'A',
|
||||
title: 'Answer 1',
|
||||
correct: true,
|
||||
selectedFeedback: 'selected feedback',
|
||||
unselectedFeedback: 'unselected feedback',
|
||||
isAnswerRange: false,
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
title: 'Answer 1',
|
||||
correct: true,
|
||||
selectedFeedback: 'selected feedback',
|
||||
unselectedFeedback: 'unselected feedback',
|
||||
isAnswerRange: false,
|
||||
},
|
||||
],
|
||||
updateField: jest.fn(),
|
||||
addAnswer: jest.fn(),
|
||||
addAnswerRange: jest.fn(),
|
||||
};
|
||||
expect(shallow(
|
||||
<module.AnswersContainer
|
||||
{...answersProps}
|
||||
/>,
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('useAnswerContainer', async () => {
|
||||
let container = null;
|
||||
|
||||
@@ -151,6 +151,100 @@ exports[`AnswerOption render snapshot: renders correct option with numeric input
|
||||
</Advanced>
|
||||
`;
|
||||
|
||||
exports[`AnswerOption render snapshot: renders correct option with numeric input problem and answer range 1`] = `
|
||||
<Advanced
|
||||
className="answer-option d-flex flex-row justify-content-between flex-nowrap pb-2 pt-2"
|
||||
onToggle={[Function]}
|
||||
open={false}
|
||||
>
|
||||
<div
|
||||
className="mr-1 d-flex"
|
||||
>
|
||||
<Checker
|
||||
answer={
|
||||
Object {
|
||||
"correct": true,
|
||||
"id": "A",
|
||||
"isAnswerRange": true,
|
||||
"selectedFeedback": "selected feedback",
|
||||
"title": "Answer 1",
|
||||
"unselectedFeedback": "unselected feedback",
|
||||
}
|
||||
}
|
||||
disabled={true}
|
||||
hasSingleAnswer={false}
|
||||
setAnswer={[Function]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ml-1 flex-grow-1"
|
||||
>
|
||||
<div>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
autoResize={true}
|
||||
className="answer-option-textarea text-gray-500 small"
|
||||
onChange={[Function]}
|
||||
placeholder="Enter an answer range"
|
||||
rows={1}
|
||||
value="Answer 1"
|
||||
/>
|
||||
<hr
|
||||
className="d-block"
|
||||
/>
|
||||
<div
|
||||
className="pgn__form-switch-helper-text"
|
||||
>
|
||||
<FormattedMessage
|
||||
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"
|
||||
id="authoring.answerwidget.answer.answerRangeHelperText"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Body>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
answer={
|
||||
Object {
|
||||
"correct": true,
|
||||
"id": "A",
|
||||
"isAnswerRange": true,
|
||||
"selectedFeedback": "selected feedback",
|
||||
"title": "Answer 1",
|
||||
"unselectedFeedback": "unselected feedback",
|
||||
}
|
||||
}
|
||||
intl={
|
||||
Object {
|
||||
"formatMessage": [Function],
|
||||
}
|
||||
}
|
||||
problemType="numericalresponse"
|
||||
setSelectedFeedback={[Function]}
|
||||
setUnselectedFeedback={[Function]}
|
||||
/>
|
||||
</Body>
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-row flex-nowrap"
|
||||
>
|
||||
<Trigger>
|
||||
<IconButton
|
||||
alt="Toggle feedback"
|
||||
iconAs="Icon"
|
||||
variant="primary"
|
||||
/>
|
||||
</Trigger>
|
||||
<IconButton
|
||||
alt="Delete answer"
|
||||
iconAs="Icon"
|
||||
onClick={[Function]}
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</Advanced>
|
||||
`;
|
||||
|
||||
exports[`AnswerOption render snapshot: renders correct option with selected unselected feedback 1`] = `
|
||||
<Advanced
|
||||
className="answer-option d-flex flex-row justify-content-between flex-nowrap pb-2 pt-2"
|
||||
|
||||
@@ -1,5 +1,179 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AnswersContainer render snapshot: numeric problems: answer range/answer select button: Range disables the additon of more adds 1`] = `
|
||||
<div
|
||||
className="answers-container border border-light-700 rounded py-4 pl-4 pr-3"
|
||||
>
|
||||
<Component
|
||||
answer={
|
||||
Object {
|
||||
"correct": true,
|
||||
"id": "A",
|
||||
"isAnswerRange": true,
|
||||
"selectedFeedback": "selected feedback",
|
||||
"title": "Answer 1",
|
||||
"unselectedFeedback": "unselected feedback",
|
||||
}
|
||||
}
|
||||
hasSingleAnswer={false}
|
||||
key="A"
|
||||
/>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="Add-Answer-Or-Answer-Range"
|
||||
variant="tertiary"
|
||||
>
|
||||
<Icon />
|
||||
<FormattedMessage
|
||||
defaultMessage="Add answer"
|
||||
description="Button text to add answer"
|
||||
id="authoring.answerwidget.answer.addAnswerButton"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
className="AddAnswerRange disabled"
|
||||
key="add-answer"
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add answer"
|
||||
description="Button text to add answer"
|
||||
id="authoring.answerwidget.answer.addAnswerButton"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
className="AddAnswerRange disabled"
|
||||
key="add-answer-range"
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add answer range"
|
||||
description="Button text to add a range of answers"
|
||||
id="authoring.answerwidget.answer.addAnswerRangeButton"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AnswersContainer render snapshot: numeric problems: answer range/answer select button: empty 1`] = `
|
||||
<div
|
||||
className="answers-container border border-light-700 rounded py-4 pl-4 pr-3"
|
||||
>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="Add-Answer-Or-Answer-Range"
|
||||
variant="tertiary"
|
||||
>
|
||||
<Icon />
|
||||
<FormattedMessage
|
||||
defaultMessage="Add answer"
|
||||
description="Button text to add answer"
|
||||
id="authoring.answerwidget.answer.addAnswerButton"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
className="AddAnswerRange "
|
||||
key="add-answer"
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add answer"
|
||||
description="Button text to add answer"
|
||||
id="authoring.answerwidget.answer.addAnswerButton"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
className="AddAnswerRange "
|
||||
key="add-answer-range"
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add answer range"
|
||||
description="Button text to add a range of answers"
|
||||
id="authoring.answerwidget.answer.addAnswerRangeButton"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AnswersContainer render snapshot: numeric problems: answer range/answer select button: multiple answers disables range. 1`] = `
|
||||
<div
|
||||
className="answers-container border border-light-700 rounded py-4 pl-4 pr-3"
|
||||
>
|
||||
<Component
|
||||
answer={
|
||||
Object {
|
||||
"correct": true,
|
||||
"id": "A",
|
||||
"isAnswerRange": false,
|
||||
"selectedFeedback": "selected feedback",
|
||||
"title": "Answer 1",
|
||||
"unselectedFeedback": "unselected feedback",
|
||||
}
|
||||
}
|
||||
hasSingleAnswer={false}
|
||||
key="A"
|
||||
/>
|
||||
<Component
|
||||
answer={
|
||||
Object {
|
||||
"correct": true,
|
||||
"id": "B",
|
||||
"isAnswerRange": false,
|
||||
"selectedFeedback": "selected feedback",
|
||||
"title": "Answer 1",
|
||||
"unselectedFeedback": "unselected feedback",
|
||||
}
|
||||
}
|
||||
hasSingleAnswer={false}
|
||||
key="B"
|
||||
/>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="Add-Answer-Or-Answer-Range"
|
||||
variant="tertiary"
|
||||
>
|
||||
<Icon />
|
||||
<FormattedMessage
|
||||
defaultMessage="Add answer"
|
||||
description="Button text to add answer"
|
||||
id="authoring.answerwidget.answer.addAnswerButton"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
className="AddAnswerRange "
|
||||
key="add-answer"
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add answer"
|
||||
description="Button text to add answer"
|
||||
id="authoring.answerwidget.answer.addAnswerButton"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
className="AddAnswerRange disabled"
|
||||
key="add-answer-range"
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add answer range"
|
||||
description="Button text to add a range of answers"
|
||||
id="authoring.answerwidget.answer.addAnswerRangeButton"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AnswersContainer render snapshot: renders correct default 1`] = `
|
||||
<div
|
||||
className="answers-container border border-light-700 rounded py-4 pl-4 pr-3"
|
||||
|
||||
@@ -16,7 +16,7 @@ const AnswerWidget = ({
|
||||
const problemStaticData = ProblemTypes[problemType];
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-4 mb-3 text-primary-500">
|
||||
<div className="mt-4 text-primary-500">
|
||||
<div className="h4">
|
||||
<FormattedMessage {...messages.answerWidgetTitle} />
|
||||
</div>
|
||||
|
||||
@@ -56,5 +56,22 @@ const messages = defineMessages({
|
||||
defaultMessage: 'is not selected',
|
||||
description: 'Bold & underlined text for feedback if option is not selected',
|
||||
},
|
||||
|
||||
addAnswerRangeButtonText: {
|
||||
id: 'authoring.answerwidget.answer.addAnswerRangeButton',
|
||||
defaultMessage: 'Add answer range',
|
||||
description: 'Button text to add a range of answers',
|
||||
},
|
||||
answerRangeTextboxPlaceholder: {
|
||||
id: 'authoring.answerwidget.answer.answerRangeTextboxPlaceholder',
|
||||
defaultMessage: 'Enter an answer range',
|
||||
description: 'Text to prompt the user to add an answer range to the textbox.',
|
||||
},
|
||||
answerRangeHelperText: {
|
||||
id: 'authoring.answerwidget.answer.answerRangeHelperText',
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7,13 +7,10 @@ import messages from './messages';
|
||||
import { ToleranceTypes } from './constants';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export const isAnswerRangeSet = ({ answers }) =>
|
||||
// TODO: for TNL 10258
|
||||
// eslint-disable-next-line implicit-arrow-linebreak
|
||||
false;
|
||||
export const isAnswerRangeSet = ({ answers }) => !!answers[0].isAnswerRange;
|
||||
|
||||
export const handleToleranceTypeChange = ({ updateSettings, tolerance, answers }) => (event) => {
|
||||
if (!isAnswerRangeSet(answers)) {
|
||||
if (!isAnswerRangeSet({ answers })) {
|
||||
let value;
|
||||
if (event.target.value === ToleranceTypes.none.type) {
|
||||
value = null;
|
||||
@@ -26,7 +23,7 @@ export const handleToleranceTypeChange = ({ updateSettings, tolerance, answers }
|
||||
};
|
||||
|
||||
export const handleToleranceValueChange = ({ updateSettings, tolerance, answers }) => (event) => {
|
||||
if (!isAnswerRangeSet(answers)) {
|
||||
if (!isAnswerRangeSet({ answers })) {
|
||||
const newTolerance = { value: event.target.value, type: tolerance.type };
|
||||
updateSettings({ tolerance: newTolerance });
|
||||
}
|
||||
@@ -52,7 +49,7 @@ export const ToleranceCard = ({
|
||||
// inject
|
||||
intl,
|
||||
}) => {
|
||||
const canEdit = isAnswerRangeSet({ answers });
|
||||
const isAnswerRange = isAnswerRangeSet({ answers });
|
||||
let summary = getSummary({ tolerance, intl });
|
||||
useEffect(() => { summary = getSummary({ tolerance, intl }); }, [tolerance]);
|
||||
return (
|
||||
@@ -61,7 +58,7 @@ export const ToleranceCard = ({
|
||||
summary={summary}
|
||||
none={tolerance.type === ToleranceTypes.none.type}
|
||||
>
|
||||
{ canEdit
|
||||
{ isAnswerRange
|
||||
&& (
|
||||
<Alert
|
||||
varaint="info"
|
||||
@@ -78,7 +75,7 @@ export const ToleranceCard = ({
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={handleToleranceTypeChange({ updateSettings, tolerance, answers })}
|
||||
disabled={canEdit}
|
||||
disabled={isAnswerRange}
|
||||
value={tolerance.type}
|
||||
>
|
||||
{Object.keys(ToleranceTypes).map((toleranceType) => (
|
||||
@@ -90,7 +87,7 @@ export const ToleranceCard = ({
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
{ tolerance?.type !== ToleranceTypes.none.type && !canEdit
|
||||
{ tolerance?.type !== ToleranceTypes.none.type && !isAnswerRange
|
||||
&& (
|
||||
<Form.Control
|
||||
className="mt-4"
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
render, screen, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import messages from './messages';
|
||||
import { ToleranceTypes } from './constants';
|
||||
import { ToleranceCard } from './index';
|
||||
import { formatMessage } from '../../../../../../../../testUtils';
|
||||
@@ -23,10 +24,10 @@ jest.mock('@edx/paragon', () => ({
|
||||
<div className="PGN-Alert">{children}</div>)),
|
||||
Form: {
|
||||
Control: jest.fn(({
|
||||
children, onChange, as, value,
|
||||
children, onChange, as, value, disabled,
|
||||
}) => {
|
||||
if (as === 'select') {
|
||||
return (<select className="PGN-Form-Control" data-testid="select" onChange={onChange}>{children}</select>);
|
||||
return (<select className="PGN-Form-Control" data-testid="select" onChange={onChange} disabled={disabled}>{children}</select>);
|
||||
}
|
||||
return (<input type="number" data-testid="input" onChange={onChange} value={value} />);
|
||||
}),
|
||||
@@ -49,7 +50,15 @@ describe('ToleranceCard', () => {
|
||||
};
|
||||
|
||||
const props = {
|
||||
answers: [], // TODO: for TNL 10258
|
||||
answers: [{
|
||||
id: 'A',
|
||||
correct: true,
|
||||
selectedFeedback: '',
|
||||
title: 'An Answer',
|
||||
isAnswerRange: false,
|
||||
unselectedFeedback: '',
|
||||
},
|
||||
],
|
||||
updateSettings: jest.fn(),
|
||||
intl: {
|
||||
formatMessage,
|
||||
@@ -74,6 +83,32 @@ describe('ToleranceCard', () => {
|
||||
const NumberText = screen.getByText(`± ${mockToleranceNumber.value}`);
|
||||
expect(NumberText).toBeDefined();
|
||||
});
|
||||
|
||||
it('If there is an answer range, show message and disable dropdown.', () => {
|
||||
const rangeprops = {
|
||||
answers: [{
|
||||
id: 'A',
|
||||
correct: true,
|
||||
selectedFeedback: '',
|
||||
title: 'An Answer',
|
||||
isAnswerRange: true,
|
||||
unselectedFeedback: '',
|
||||
},
|
||||
],
|
||||
updateSettings: jest.fn(),
|
||||
intl: {
|
||||
formatMessage,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ToleranceCard
|
||||
tolerance={mockToleranceNumber}
|
||||
{...rangeprops}
|
||||
/>);
|
||||
const NumberText = screen.getByText(messages.toleranceAnswerRangeWarning.defaultMessage);
|
||||
expect(NumberText).toBeDefined();
|
||||
expect(screen.getByTestId('select').getAttributeNames().includes('disabled')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('Type Select', () => {
|
||||
it('Renders the types for selection', async () => {
|
||||
|
||||
@@ -40,7 +40,7 @@ exports[`EditorProblemView component renders simple view 1`] = `
|
||||
className="editProblemView d-flex flex-row flex-nowrap justify-content-end"
|
||||
>
|
||||
<span
|
||||
className="flex-grow-1"
|
||||
className="flex-grow-1 mb-5"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
<injectIntl(ShimmedIntlComponent) />
|
||||
|
||||
@@ -40,7 +40,7 @@ export const EditProblemView = ({
|
||||
<RawEditor editorRef={editorRef} lang="xml" content={problemState.rawOLX} />
|
||||
</Container>
|
||||
) : (
|
||||
<span className="flex-grow-1">
|
||||
<span className="flex-grow-1 mb-5">
|
||||
<QuestionWidget />
|
||||
<ExplanationWidget />
|
||||
<AnswerWidget problemType={problemType} />
|
||||
|
||||
@@ -263,7 +263,6 @@ export class OLXParser {
|
||||
let answerFeedback = '';
|
||||
const answers = [];
|
||||
let responseParam = {};
|
||||
// TODO: UI needs to be added to support adding tolerence in numeric response.
|
||||
const feedback = this.getFeedback(numericalresponse);
|
||||
if (_.has(numericalresponse, 'responseparam')) {
|
||||
const type = _.get(numericalresponse, 'responseparam.@_type');
|
||||
@@ -272,11 +271,13 @@ export class OLXParser {
|
||||
[type]: defaultValue,
|
||||
};
|
||||
}
|
||||
const isAnswerRange = /[([]\d*,\d*[)\]]/gm.test(numericalresponse['@_answer']);
|
||||
answers.push({
|
||||
id: indexToLetterMap[answers.length],
|
||||
title: numericalresponse['@_answer'],
|
||||
correct: true,
|
||||
selectedFeedback: feedback,
|
||||
isAnswerRange,
|
||||
...responseParam,
|
||||
});
|
||||
|
||||
@@ -299,6 +300,7 @@ export class OLXParser {
|
||||
title: additionalAnswer['@_answer'],
|
||||
correct: true,
|
||||
selectedFeedback: answerFeedback,
|
||||
isAnswerRange: false,
|
||||
});
|
||||
}
|
||||
return { answers };
|
||||
|
||||
@@ -351,12 +351,14 @@ export const numericInputWithFeedbackAndHintsOLX = {
|
||||
title: '100',
|
||||
correct: true,
|
||||
selectedFeedback: '<p>You can specify optional feedback like this, which appears after this answer is submitted.</p>',
|
||||
isAnswerRange: false,
|
||||
tolerance: '5',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
title: '200',
|
||||
correct: true,
|
||||
isAnswerRange: false,
|
||||
selectedFeedback: '<p>You can specify optional feedback like this, which appears after this answer is submitted.</p>',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -85,8 +85,24 @@ const problem = createSlice({
|
||||
deleteAnswer: (state, { payload }) => {
|
||||
const { id, correct } = payload;
|
||||
if (state.answers.length <= 1) {
|
||||
if (state.answers.length > 0 && state.answers[0].isAnswerRange) {
|
||||
return {
|
||||
...state,
|
||||
correctAnswerCount: 1,
|
||||
answers: [{
|
||||
id: 'A',
|
||||
title: '',
|
||||
selectedFeedback: '',
|
||||
unselectedFeedback: '',
|
||||
correct: state.problemType === ProblemTypeKeys.NUMERIC,
|
||||
isAnswerRange: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
let { correctAnswerCount } = state;
|
||||
if (correct) {
|
||||
correctAnswerCount -= 1;
|
||||
@@ -115,6 +131,7 @@ const problem = createSlice({
|
||||
selectedFeedback: '',
|
||||
unselectedFeedback: '',
|
||||
correct: state.problemType === ProblemTypeKeys.NUMERIC,
|
||||
isAnswerRange: false,
|
||||
};
|
||||
let { correctAnswerCount } = state;
|
||||
if (state.problemType === ProblemTypeKeys.NUMERIC) {
|
||||
@@ -131,6 +148,24 @@ const problem = createSlice({
|
||||
answers,
|
||||
};
|
||||
},
|
||||
addAnswerRange: (state) => {
|
||||
// As you may only have one answer range at a time, overwrite the answer object.
|
||||
const newOption = {
|
||||
id: 'A',
|
||||
title: '',
|
||||
selectedFeedback: '',
|
||||
unselectedFeedback: '',
|
||||
correct: state.problemType === ProblemTypeKeys.NUMERIC,
|
||||
isAnswerRange: true,
|
||||
};
|
||||
const correctAnswerCount = 1;
|
||||
return {
|
||||
...state,
|
||||
correctAnswerCount,
|
||||
answers: [newOption],
|
||||
};
|
||||
},
|
||||
|
||||
updateSettings: (state, { payload }) => ({
|
||||
...state,
|
||||
settings: {
|
||||
|
||||
@@ -56,6 +56,7 @@ describe('problem reducer', () => {
|
||||
correct: false,
|
||||
selectedFeedback: '',
|
||||
title: '',
|
||||
isAnswerRange: false,
|
||||
unselectedFeedback: '',
|
||||
};
|
||||
expect(reducer(testingState, actions.addAnswer(answer))).toEqual({
|
||||
@@ -91,6 +92,7 @@ describe('problem reducer', () => {
|
||||
correct: false,
|
||||
selectedFeedback: '',
|
||||
title: '',
|
||||
isAnswerRange: false,
|
||||
unselectedFeedback: '',
|
||||
};
|
||||
it('sets answers', () => {
|
||||
@@ -116,6 +118,26 @@ describe('problem reducer', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addAnswerRange', () => {
|
||||
const answerRange = {
|
||||
id: 'A',
|
||||
correct: true,
|
||||
selectedFeedback: '',
|
||||
title: '',
|
||||
isAnswerRange: true,
|
||||
unselectedFeedback: '',
|
||||
};
|
||||
it('sets answerRange', () => {
|
||||
expect(reducer({ ...testingState, problemType: ProblemTypeKeys.NUMERIC }, actions.addAnswerRange())).toEqual({
|
||||
...testingState,
|
||||
correctAnswerCount: 1,
|
||||
problemType: ProblemTypeKeys.NUMERIC,
|
||||
answers: [answerRange],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAnswer', () => {
|
||||
it('sets answers, as well as setting the correctAnswerCount ', () => {
|
||||
const answer = { id: 'A', correct: true };
|
||||
@@ -162,6 +184,39 @@ describe('problem reducer', () => {
|
||||
}],
|
||||
});
|
||||
});
|
||||
it('if you delete an answer range, it will be replaced with a blank answer', () => {
|
||||
const answer = {
|
||||
id: 'A',
|
||||
correct: true,
|
||||
selectedFeedback: '',
|
||||
title: '',
|
||||
isAnswerRange: false,
|
||||
unselectedFeedback: '',
|
||||
};
|
||||
const answerRange = {
|
||||
id: 'A',
|
||||
correct: false,
|
||||
selectedFeedback: '',
|
||||
title: '',
|
||||
isAnswerRange: true,
|
||||
unselectedFeedback: '',
|
||||
};
|
||||
|
||||
expect(reducer(
|
||||
{
|
||||
...testingState,
|
||||
problemType: ProblemTypeKeys.NUMERIC,
|
||||
correctAnswerCount: 1,
|
||||
answers: [{ ...answerRange }],
|
||||
},
|
||||
actions.deleteAnswer(answer),
|
||||
)).toEqual({
|
||||
...testingState,
|
||||
problemType: ProblemTypeKeys.NUMERIC,
|
||||
correctAnswerCount: 1,
|
||||
answers: [{ ...answer }],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user