feat: numeric input UI to allow only correct (#256)
This commit is contained in:
@@ -16,6 +16,7 @@ import { answerOptionProps } from '../../../../../data/services/cms/types';
|
||||
import Checker from './components/Checker';
|
||||
import { FeedbackBox } from './components/Feedback';
|
||||
import * as hooks from './hooks';
|
||||
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
|
||||
|
||||
export const AnswerOption = ({
|
||||
answer,
|
||||
@@ -43,6 +44,7 @@ export const AnswerOption = ({
|
||||
hasSingleAnswer={hasSingleAnswer}
|
||||
answer={answer}
|
||||
setAnswer={setAnswer}
|
||||
disabled={problemType === ProblemTypeKeys.NUMERIC}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-1 flex-grow-1">
|
||||
|
||||
@@ -42,6 +42,9 @@ describe('AnswerOption', () => {
|
||||
test('snapshot: renders correct option with selected unselected feedback', () => {
|
||||
expect(shallow(<AnswerOption {...props} problemType="choiceresponse" answer={answerWithSelectedUnselectedFeedback} />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: renders correct option with numeric input problem', () => {
|
||||
expect(shallow(<AnswerOption {...props} problemType="numericalresponse" />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
|
||||
@@ -18,6 +18,7 @@ exports[`AnswerOption render snapshot: renders correct option with feedback 1`]
|
||||
"title": "Answer 1",
|
||||
}
|
||||
}
|
||||
disabled={false}
|
||||
hasSingleAnswer={false}
|
||||
setAnswer={[Function]}
|
||||
/>
|
||||
@@ -75,6 +76,82 @@ exports[`AnswerOption render snapshot: renders correct option with feedback 1`]
|
||||
</Advanced>
|
||||
`;
|
||||
|
||||
exports[`AnswerOption render snapshot: renders correct option with numeric input problem 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",
|
||||
"selectedFeedback": "some feedback",
|
||||
"title": "Answer 1",
|
||||
}
|
||||
}
|
||||
disabled={true}
|
||||
hasSingleAnswer={false}
|
||||
setAnswer={[Function]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="ml-1 flex-grow-1"
|
||||
>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
autoResize={true}
|
||||
className="answer-option-textarea text-gray-500 small"
|
||||
onChange={[Function]}
|
||||
placeholder="Enter an answer"
|
||||
rows={1}
|
||||
value="Answer 1"
|
||||
/>
|
||||
<Body>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
answer={
|
||||
Object {
|
||||
"correct": true,
|
||||
"id": "A",
|
||||
"selectedFeedback": "some feedback",
|
||||
"title": "Answer 1",
|
||||
}
|
||||
}
|
||||
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"
|
||||
@@ -94,6 +171,7 @@ exports[`AnswerOption render snapshot: renders correct option with selected unse
|
||||
"unselectedFeedback": "unselected feedback",
|
||||
}
|
||||
}
|
||||
disabled={false}
|
||||
hasSingleAnswer={false}
|
||||
setAnswer={[Function]}
|
||||
/>
|
||||
|
||||
@@ -1,25 +1,55 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Checker component with disabled 1`] = `
|
||||
<Fragment>
|
||||
<Radio
|
||||
checked={true}
|
||||
className="pt-2.5"
|
||||
disabled={true}
|
||||
isValid={true}
|
||||
onChange={[Function]}
|
||||
value="A"
|
||||
/>
|
||||
<Form.Label
|
||||
className="pt-2"
|
||||
>
|
||||
A
|
||||
</Form.Label>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`Checker component with multiple answers 1`] = `
|
||||
<Form.Checkbox
|
||||
checked={true}
|
||||
className="pt-2.5"
|
||||
isValid={true}
|
||||
onChange={[Function]}
|
||||
value="A"
|
||||
>
|
||||
A
|
||||
</Form.Checkbox>
|
||||
<Fragment>
|
||||
<Form.Checkbox
|
||||
checked={true}
|
||||
className="pt-2.5"
|
||||
disabled={false}
|
||||
isValid={true}
|
||||
onChange={[Function]}
|
||||
value="A"
|
||||
/>
|
||||
<Form.Label
|
||||
className="pt-2"
|
||||
>
|
||||
A
|
||||
</Form.Label>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`Checker component with single answer 1`] = `
|
||||
<Radio
|
||||
checked={true}
|
||||
className="pt-2.5"
|
||||
isValid={true}
|
||||
onChange={[Function]}
|
||||
value="A"
|
||||
>
|
||||
A
|
||||
</Radio>
|
||||
<Fragment>
|
||||
<Radio
|
||||
checked={true}
|
||||
className="pt-2.5"
|
||||
disabled={false}
|
||||
isValid={true}
|
||||
onChange={[Function]}
|
||||
value="A"
|
||||
/>
|
||||
<Form.Label
|
||||
className="pt-2"
|
||||
>
|
||||
A
|
||||
</Form.Label>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -3,24 +3,36 @@ import PropTypes from 'prop-types';
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
const Checker = ({
|
||||
hasSingleAnswer, answer, setAnswer,
|
||||
hasSingleAnswer,
|
||||
answer,
|
||||
setAnswer,
|
||||
disabled,
|
||||
}) => {
|
||||
let CheckerType = Form.Checkbox;
|
||||
if (hasSingleAnswer) {
|
||||
CheckerType = Form.Radio;
|
||||
}
|
||||
return (
|
||||
<CheckerType
|
||||
className="pt-2.5"
|
||||
value={answer.id}
|
||||
onChange={(e) => setAnswer({ correct: e.target.checked })}
|
||||
checked={answer.correct}
|
||||
isValid={answer.correct}
|
||||
>
|
||||
{answer.id}
|
||||
</CheckerType>
|
||||
<>
|
||||
<CheckerType
|
||||
className="pt-2.5"
|
||||
value={answer.id}
|
||||
onChange={(e) => setAnswer({ correct: e.target.checked })}
|
||||
checked={answer.correct}
|
||||
isValid={answer.correct}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Form.Label
|
||||
className="pt-2"
|
||||
>
|
||||
{answer.id}
|
||||
</Form.Label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Checker.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
Checker.propTypes = {
|
||||
hasSingleAnswer: PropTypes.bool.isRequired,
|
||||
answer: PropTypes.shape({
|
||||
@@ -28,6 +40,7 @@ Checker.propTypes = {
|
||||
id: PropTypes.number,
|
||||
}).isRequired,
|
||||
setAnswer: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Checker;
|
||||
|
||||
@@ -19,4 +19,8 @@ describe('Checker component', () => {
|
||||
test('with multiple answers', () => {
|
||||
expect(shallow(<Checker {...props} hasSingleAnswer={false} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('with disabled', () => {
|
||||
expect(shallow(<Checker {...props} disabled />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -213,6 +213,11 @@ export const typeRowHooks = ({
|
||||
}
|
||||
});
|
||||
};
|
||||
const updateAnswersToCorrect = () => {
|
||||
answers.forEach(answer => {
|
||||
updateAnswer({ ...answer, correct: true });
|
||||
});
|
||||
};
|
||||
const onClick = () => {
|
||||
// Dropdown problems can only have one correct answer. When there is more than one correct answer
|
||||
// from a previous problem type, the correct attribute for selected answers need to be set to false.
|
||||
@@ -221,6 +226,11 @@ export const typeRowHooks = ({
|
||||
clearPreviouslySelectedAnswers();
|
||||
}
|
||||
}
|
||||
// Numeric input problems can only have correct answers. Switch all answers to correct when switching
|
||||
// to numeric input.
|
||||
if (typeKey === ProblemTypeKeys.NUMERIC) {
|
||||
updateAnswersToCorrect();
|
||||
}
|
||||
if (blockTitle === ProblemTypes[problemType].title) {
|
||||
setBlockTitle(ProblemTypes[typeKey].title);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MockUseState } from '../../../../../../testUtils';
|
||||
import messages from './messages';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import { ProblemTypeKeys, ProblemTypes } from '../../../../../data/constants/problem';
|
||||
|
||||
jest.mock('react', () => {
|
||||
const updateState = jest.fn();
|
||||
@@ -249,55 +250,55 @@ describe('Problem settings hooks', () => {
|
||||
});
|
||||
|
||||
describe('Type row hooks', () => {
|
||||
test('test onClick', () => {
|
||||
const typekey = 'optionresponse';
|
||||
const problemType = 'choiceresponse';
|
||||
const blockTitle = 'Multi-select';
|
||||
const setBlockTitle = jest.fn();
|
||||
const updateField = jest.fn();
|
||||
const updateAnswer = jest.fn();
|
||||
const answers = [{
|
||||
correct: true,
|
||||
id: 'a',
|
||||
},
|
||||
{
|
||||
correct: true,
|
||||
id: 'b',
|
||||
},
|
||||
{
|
||||
correct: false,
|
||||
id: 'c',
|
||||
}];
|
||||
const typeRowProps = {
|
||||
problemType: ProblemTypeKeys.MULTISELECT,
|
||||
typeKey: ProblemTypeKeys.DROPDOWN,
|
||||
blockTitle: ProblemTypes[ProblemTypeKeys.MULTISELECT].title,
|
||||
setBlockTitle: jest.fn(),
|
||||
updateField: jest.fn(),
|
||||
updateAnswer: jest.fn(),
|
||||
correctAnswerCount: 2,
|
||||
answers: [
|
||||
{ correct: true, id: 'a' },
|
||||
{ correct: true, id: 'b' },
|
||||
{ correct: false, id: 'c' },
|
||||
],
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('test onClick Multi-select to Dropdown', () => {
|
||||
output = hooks.typeRowHooks(typeRowProps);
|
||||
output.onClick();
|
||||
expect(typeRowProps.setBlockTitle).toHaveBeenCalledWith(ProblemTypes[ProblemTypeKeys.DROPDOWN].title);
|
||||
expect(typeRowProps.updateAnswer).toHaveBeenNthCalledWith(1, { ...typeRowProps.answers[0], correct: false });
|
||||
expect(typeRowProps.updateAnswer).toHaveBeenNthCalledWith(2, { ...typeRowProps.answers[1], correct: false });
|
||||
expect(typeRowProps.updateAnswer).not.toHaveBeenNthCalledWith(3, { ...typeRowProps.answers[2], correct: false });
|
||||
expect(typeRowProps.updateField).toHaveBeenCalledWith({ problemType: ProblemTypeKeys.DROPDOWN });
|
||||
});
|
||||
test('test onClick Multi-select to Numeric', () => {
|
||||
output = hooks.typeRowHooks({
|
||||
answers,
|
||||
blockTitle,
|
||||
correctAnswerCount: 2,
|
||||
problemType,
|
||||
setBlockTitle,
|
||||
typeKey: typekey,
|
||||
updateField,
|
||||
updateAnswer,
|
||||
...typeRowProps,
|
||||
typeKey: ProblemTypeKeys.NUMERIC,
|
||||
});
|
||||
output.onClick();
|
||||
expect(setBlockTitle).toHaveBeenCalledWith('Dropdown');
|
||||
expect(updateAnswer).toHaveBeenNthCalledWith(1, { ...answers[0], correct: false });
|
||||
expect(updateAnswer).toHaveBeenNthCalledWith(2, { ...answers[1], correct: false });
|
||||
expect(updateAnswer).not.toHaveBeenNthCalledWith(3, { ...answers[2], correct: false });
|
||||
expect(updateField).toHaveBeenCalledWith({ problemType: typekey });
|
||||
expect(typeRowProps.setBlockTitle).toHaveBeenCalledWith(ProblemTypes[ProblemTypeKeys.NUMERIC].title);
|
||||
expect(typeRowProps.updateAnswer).toHaveBeenNthCalledWith(1, { ...typeRowProps.answers[0], correct: true });
|
||||
expect(typeRowProps.updateAnswer).toHaveBeenNthCalledWith(2, { ...typeRowProps.answers[1], correct: true });
|
||||
expect(typeRowProps.updateAnswer).toHaveBeenNthCalledWith(3, { ...typeRowProps.answers[2], correct: true });
|
||||
expect(typeRowProps.updateField).toHaveBeenCalledWith({ problemType: ProblemTypeKeys.NUMERIC });
|
||||
});
|
||||
});
|
||||
describe('Type row hooks', () => {
|
||||
test('test onClick', () => {
|
||||
const switchToAdvancedEditor = jest.fn();
|
||||
const setConfirmOpen = jest.fn();
|
||||
window.scrollTo = jest.fn();
|
||||
hooks.confirmSwitchToAdvancedEditor({
|
||||
switchToAdvancedEditor,
|
||||
setConfirmOpen,
|
||||
});
|
||||
expect(switchToAdvancedEditor).toHaveBeenCalled();
|
||||
expect(setConfirmOpen).toHaveBeenCalledWith(false);
|
||||
expect(window.scrollTo).toHaveBeenCalled();
|
||||
test('test confirmSwitchToAdvancedEditor hook', () => {
|
||||
const switchToAdvancedEditor = jest.fn();
|
||||
const setConfirmOpen = jest.fn();
|
||||
window.scrollTo = jest.fn();
|
||||
hooks.confirmSwitchToAdvancedEditor({
|
||||
switchToAdvancedEditor,
|
||||
setConfirmOpen,
|
||||
});
|
||||
expect(switchToAdvancedEditor).toHaveBeenCalled();
|
||||
expect(setConfirmOpen).toHaveBeenCalledWith(false);
|
||||
expect(window.scrollTo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'lodash-es';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { indexToLetterMap } from '../../../containers/ProblemEditor/data/OLXParser';
|
||||
import { StrictDict } from '../../../utils';
|
||||
import { ShowAnswerTypesKeys } from '../../constants/problem';
|
||||
import { ProblemTypeKeys, ShowAnswerTypesKeys } from '../../constants/problem';
|
||||
|
||||
const nextAlphaId = (lastId) => String.fromCharCode(lastId.charCodeAt(0) + 1);
|
||||
const initialState = {
|
||||
@@ -108,8 +108,12 @@ const problem = createSlice({
|
||||
title: '',
|
||||
selectedFeedback: '',
|
||||
unselectedFeedback: '',
|
||||
correct: false,
|
||||
correct: state.problemType === ProblemTypeKeys.NUMERIC,
|
||||
};
|
||||
let { correctAnswerCount } = state;
|
||||
if (state.problemType === ProblemTypeKeys.NUMERIC) {
|
||||
correctAnswerCount += 1;
|
||||
}
|
||||
|
||||
const answers = [
|
||||
...currAnswers,
|
||||
@@ -117,6 +121,7 @@ const problem = createSlice({
|
||||
];
|
||||
return {
|
||||
...state,
|
||||
correctAnswerCount,
|
||||
answers,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { initialState, actions, reducer } from './reducers';
|
||||
import { ProblemTypeKeys } from '../../constants/problem';
|
||||
|
||||
const testingState = {
|
||||
...initialState,
|
||||
@@ -71,20 +72,35 @@ describe('problem reducer', () => {
|
||||
});
|
||||
});
|
||||
describe('addAnswer', () => {
|
||||
const answer = {
|
||||
id: 'A',
|
||||
correct: false,
|
||||
selectedFeedback: '',
|
||||
title: '',
|
||||
unselectedFeedback: '',
|
||||
};
|
||||
it('sets answers', () => {
|
||||
const answer = {
|
||||
id: 'A',
|
||||
correct: false,
|
||||
selectedFeedback: '',
|
||||
title: '',
|
||||
unselectedFeedback: '',
|
||||
};
|
||||
expect(reducer({ ...testingState, problemType: 'choiceresponse' }, actions.addAnswer())).toEqual({
|
||||
...testingState,
|
||||
problemType: 'choiceresponse',
|
||||
answers: [answer],
|
||||
});
|
||||
});
|
||||
it('sets answers for numeric input', () => {
|
||||
const numericTestState = {
|
||||
...testingState,
|
||||
problemType: ProblemTypeKeys.NUMERIC,
|
||||
correctAnswerCount: 0,
|
||||
};
|
||||
expect(reducer(numericTestState, actions.addAnswer())).toEqual({
|
||||
...numericTestState,
|
||||
correctAnswerCount: 1,
|
||||
answers: [{
|
||||
...answer,
|
||||
correct: true,
|
||||
}],
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('updateAnswer', () => {
|
||||
it('sets answers, as well as setting the correctAnswerCount ', () => {
|
||||
|
||||
Reference in New Issue
Block a user