feat: numeric input UI to allow only correct (#256)

This commit is contained in:
Raymond Zhou
2023-03-01 10:02:27 -08:00
committed by GitHub
parent c65f60ec10
commit 493ef9026e
10 changed files with 243 additions and 81 deletions

View File

@@ -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">

View File

@@ -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' };

View File

@@ -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]}
/>

View File

@@ -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>
`;

View File

@@ -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;

View File

@@ -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();
});
});

View File

@@ -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);
}

View File

@@ -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();
});
});

View File

@@ -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,
};
},

View File

@@ -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 ', () => {