diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.jsx index d85b9ea2e..76bff6235 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.jsx @@ -16,9 +16,9 @@ export const AnswersContainer = ({ // Redux answers, addAnswer, + updateField, }) => { - const { hasSingleAnswer } = initializeAnswerContainer(problemType); - + const { hasSingleAnswer } = initializeAnswerContainer({ answers, problemType, updateField }); return (
{answers.map((answer) => ( @@ -44,6 +44,7 @@ AnswersContainer.propTypes = { problemType: PropTypes.string.isRequired, answers: PropTypes.arrayOf(answerOptionProps).isRequired, addAnswer: PropTypes.func.isRequired, + updateField: PropTypes.func.isRequired, }; export const mapStateToProps = (state) => ({ @@ -52,6 +53,7 @@ export const mapStateToProps = (state) => ({ export const mapDispatchToProps = { addAnswer: actions.problem.addAnswer, + updateField: actions.problem.updateField, }; export default connect(mapStateToProps, mapDispatchToProps)(AnswersContainer); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.test.jsx new file mode 100644 index 000000000..f96a727f5 --- /dev/null +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.test.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { actions, selectors } from '../../../../../data/redux'; + +import * as module from './AnswersContainer'; + +jest.mock('../../../../../data/redux', () => ({ + actions: { + problem: { + updateField: jest.fn().mockName('actions.problem.updateField'), + addAnswer: jest.fn().mockName('actions.problem.addAnswer'), + }, + }, + selectors: { + problem: { + answers: jest.fn(state => ({ answers: state })), + }, + }, +})); +describe('AnswersContainer', () => { + const props = { + answers: [], + updateField: jest.fn(), + addAnswer: jest.fn(), + }; + describe('render', () => { + test('snapshot: renders correct default', () => { + expect(shallow()).toMatchSnapshot(); + }); + test('snapshot: renders correctly with answers', () => { + expect(shallow()).toMatchSnapshot(); + }); + }); + describe('mapStateToProps', () => { + const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; + test('answers from problem.answers', () => { + expect( + module.mapStateToProps(testState).answers, + ).toEqual(selectors.problem.answers(testState)); + }); + }); + describe('mapDispatchToProps', () => { + test('updateField from actions.problem.updateField', () => { + expect(module.mapDispatchToProps.updateField).toEqual(actions.problem.updateField); + }); + test('updateField from actions.problem.addAnswer', () => { + expect(module.mapDispatchToProps.addAnswer).toEqual(actions.problem.addAnswer); + }); + }); +}); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswersContainer.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswersContainer.test.jsx.snap new file mode 100644 index 000000000..848eaa19e --- /dev/null +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/__snapshots__/AnswersContainer.test.jsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AnswersContainer render snapshot: renders correct default 1`] = ` +
+ +
+`; + +exports[`AnswersContainer render snapshot: renders correctly with answers 1`] = ` +
+ +
+`; diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js index e77ab6196..6ef3e3c76 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.js @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import _ from 'lodash-es'; import * as module from './hooks'; import messages from './messages'; -import { ShowAnswerTypesKeys } from '../../../../../data/constants/problem'; +import { ProblemTypeKeys, ShowAnswerTypesKeys } from '../../../../../data/constants/problem'; export const state = { showAdvanced: (val) => useState(val), @@ -182,11 +182,23 @@ export const timerCardHooks = (updateSettings) => ({ }, }); -export const typeRowHooks = (typeKey, updateField) => { +export const typeRowHooks = ({ + answers, + correctAnswerCount, + typeKey, + updateField, + updateAnswer, +}) => { const onClick = () => { + if (typeKey === ProblemTypeKeys.SINGLESELECT || typeKey === ProblemTypeKeys.DROPDOWN) { + if (correctAnswerCount > 1) { + answers.forEach(answer => { + updateAnswer({ ...answer, correct: false }); + }); + } + } updateField({ problemType: typeKey }); }; - return { onClick, }; diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js index 9d9a41b9d..b43a5046c 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/hooks.test.js @@ -18,6 +18,7 @@ jest.mock('../../../../../data/redux', () => ({ problem: { updateSettings: (args) => ({ updateSettings: args }), updateField: (args) => ({ updateField: args }), + updateAnswer: (args) => ({ updateAnswer: args }), }, }, })); @@ -217,10 +218,19 @@ describe('Problem settings hooks', () => { describe('Type row hooks', () => { test('test onClick', () => { - const typekey = 'TEXTINPUT'; + const typekey = 'multiplechoiceresponse'; const updateField = jest.fn(); - output = hooks.typeRowHooks(typekey, updateField); + const updateAnswer = jest.fn(); + const answers = [{ correct: true, id: 'a' }, { correct: true, id: 'b' }]; + output = hooks.typeRowHooks({ + answers, + correctAnswerCount: 2, + typeKey: typekey, + updateField, + updateAnswer, + }); output.onClick(); + expect(updateAnswer).toHaveBeenCalledWith({ ...answers[1], correct: false }); expect(updateField).toHaveBeenCalledWith({ problemType: typekey }); }); }); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx index b97c357f6..cef24d973 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/index.jsx @@ -23,9 +23,12 @@ import './index.scss'; export const SettingsWidget = ({ problemType, // redux + answers, + correctAnswerCount, settings, updateSettings, updateField, + updateAnswer, }) => { const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards(); return ( @@ -38,7 +41,13 @@ export const SettingsWidget = ({ - + @@ -91,7 +100,16 @@ export const SettingsWidget = ({ }; SettingsWidget.propTypes = { + answers: PropTypes.arrayOf(PropTypes.shape({ + correct: PropTypes.bool, + id: PropTypes.string, + selectedFeedback: PropTypes.string, + title: PropTypes.string, + unselectedFeedback: PropTypes.string, + })).isRequired, + correctAnswerCount: PropTypes.number.isRequired, problemType: PropTypes.string.isRequired, + updateAnswer: PropTypes.func.isRequired, updateField: PropTypes.func.isRequired, updateSettings: PropTypes.func.isRequired, // eslint-disable-next-line @@ -100,11 +118,14 @@ SettingsWidget.propTypes = { const mapStateToProps = (state) => ({ settings: selectors.problem.settings(state), + answers: selectors.problem.answers(state), + correctAnswerCount: selectors.problem.correctAnswerCount(state), }); export const mapDispatchToProps = { updateSettings: actions.problem.updateSettings, updateField: actions.problem.updateField, + updateAnswer: actions.problem.updateAnswer, }; export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SettingsWidget)); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.jsx index 06a085db2..b8d509b29 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.jsx @@ -7,8 +7,11 @@ import messages from '../messages'; import TypeRow from './TypeRow'; export const TypeCard = ({ + answers, + correctAnswerCount, problemType, updateField, + updateAnswer, // inject intl, }) => { @@ -21,12 +24,15 @@ export const TypeCard = ({ > {problemTypeKeysArray.map((typeKey, i) => ( ))} @@ -34,9 +40,19 @@ export const TypeCard = ({ }; TypeCard.propTypes = { - intl: intlShape.isRequired, + answers: PropTypes.arrayOf(PropTypes.shape({ + correct: PropTypes.bool, + id: PropTypes.string, + selectedFeedback: PropTypes.string, + title: PropTypes.string, + unselectedFeedback: PropTypes.string, + })).isRequired, + correctAnswerCount: PropTypes.number.isRequired, problemType: PropTypes.string.isRequired, updateField: PropTypes.func.isRequired, + updateAnswer: PropTypes.func.isRequired, + // injected + intl: intlShape.isRequired, }; export default injectIntl(TypeCard); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.test.jsx index 1886661a5..d2a2f8518 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.test.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeCard.test.jsx @@ -6,8 +6,12 @@ import { ProblemTypeKeys } from '../../../../../../data/constants/problem'; describe('TypeCard', () => { const props = { + answers: [], + correctAnswerCount: 0, problemType: ProblemTypeKeys.TEXTINPUT, updateField: jest.fn().mockName('args.updateField'), + updateAnswer: jest.fn().mockName('args.updateAnswer'), + // injected intl: { formatMessage }, }; diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.jsx index 71810543c..811499d43 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.jsx @@ -5,13 +5,22 @@ import { Check } from '@edx/paragon/icons'; import { typeRowHooks } from '../hooks'; export const TypeRow = ({ + answers, + correctAnswerCount, typeKey, label, selected, lastRow, updateField, + updateAnswer, }) => { - const { onClick } = typeRowHooks(typeKey, updateField); + const { onClick } = typeRowHooks({ + answers, + correctAnswerCount, + typeKey, + updateField, + updateAnswer, + }); return ( <> @@ -25,10 +34,19 @@ export const TypeRow = ({ }; TypeRow.propTypes = { + answers: PropTypes.arrayOf(PropTypes.shape({ + correct: PropTypes.bool, + id: PropTypes.string, + selectedFeedback: PropTypes.string, + title: PropTypes.string, + unselectedFeedback: PropTypes.string, + })).isRequired, + correctAnswerCount: PropTypes.number.isRequired, typeKey: PropTypes.string.isRequired, label: PropTypes.string.isRequired, selected: PropTypes.bool.isRequired, lastRow: PropTypes.bool.isRequired, + updateAnswer: PropTypes.func.isRequired, updateField: PropTypes.func.isRequired, }; diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.test.jsx index 72c4579e8..2065e57c0 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.test.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/TypeRow.test.jsx @@ -10,11 +10,14 @@ jest.mock('../hooks', () => ({ describe('TypeRow', () => { const typeKey = 'TEXTINPUT'; const props = { + answers: [], + correctAnswerCount: 0, typeKey, label: 'Text Input Problem', selected: true, lastRow: false, updateField: jest.fn().mockName('args.updateField'), + updateAnswer: jest.fn().mockName('args.updateAnswer'), }; const typeRowHooksProps = { @@ -26,7 +29,13 @@ describe('TypeRow', () => { describe('behavior', () => { it(' calls typeRowHooks when initialized', () => { shallow(); - expect(typeRowHooks).toHaveBeenCalledWith(typeKey, props.updateField); + expect(typeRowHooks).toHaveBeenCalledWith({ + answers: props.answers, + correctAnswerCount: props.correctAnswerCount, + typeKey, + updateField: props.updateField, + updateAnswer: props.updateAnswer, + }); }); }); diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/TypeCard.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/TypeCard.test.jsx.snap index 251289f10..9b9ef8114 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/TypeCard.test.jsx.snap +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/TypeCard.test.jsx.snap @@ -6,51 +6,69 @@ exports[`TypeCard snapshot snapshot: renders type setting card 1`] = ` title="Type" > diff --git a/src/editors/containers/ProblemEditor/hooks.js b/src/editors/containers/ProblemEditor/hooks.js index 78bf14ca6..cb0a539dd 100644 --- a/src/editors/containers/ProblemEditor/hooks.js +++ b/src/editors/containers/ProblemEditor/hooks.js @@ -35,7 +35,14 @@ export const prepareEditorRef = () => { return { editorRef, refReady, setEditorRef }; }; -export const initializeAnswerContainer = (problemType) => { +export const initializeAnswerContainer = ({ answers, problemType, updateField }) => { const hasSingleAnswer = problemType === ProblemTypeKeys.DROPDOWN || problemType === ProblemTypeKeys.SINGLESELECT; + let answerCount = 0; + answers.forEach(answer => { + if (answer.correct) { + answerCount += 1; + } + }); + updateField({ correctAnswerCount: answerCount }); return { hasSingleAnswer }; }; diff --git a/src/editors/data/redux/problem/reducers.js b/src/editors/data/redux/problem/reducers.js index 765c26ede..41c6c3061 100644 --- a/src/editors/data/redux/problem/reducers.js +++ b/src/editors/data/redux/problem/reducers.js @@ -10,6 +10,7 @@ const initialState = { problemType: null, question: '', answers: [], + correctAnswerCount: 0, groupFeedbackList: [], additionalAttributes: {}, settings: { @@ -46,8 +47,17 @@ const problem = createSlice({ }), updateAnswer: (state, { payload }) => { const { id, hasSingleAnswer, ...answer } = payload; + let { correctAnswerCount } = state; const answers = state.answers.map(obj => { if (obj.id === id) { + if (_.has(answer, 'correct') && payload.correct) { + correctAnswerCount += 1; + return { ...obj, ...answer }; + } + if (_.has(answer, 'correct') && payload.correct === false) { + correctAnswerCount -= 1; + return { ...obj, ...answer }; + } return { ...obj, ...answer }; } // set other answers as incorrect if problem only has one answer correct @@ -59,14 +69,19 @@ const problem = createSlice({ }); return { ...state, + correctAnswerCount, answers, }; }, deleteAnswer: (state, { payload }) => { - const { id } = payload; + const { id, correct } = payload; if (state.answers.length <= 1) { return state; } + let { correctAnswerCount } = state; + if (correct) { + correctAnswerCount -= 1; + } const answers = state.answers.filter(obj => obj.id !== id).map((answer, index) => { const newId = indexToLetterMap[index]; if (answer.id === newId) { @@ -76,6 +91,7 @@ const problem = createSlice({ }); return { ...state, + correctAnswerCount, answers, }; }, diff --git a/src/editors/data/redux/problem/reducers.test.js b/src/editors/data/redux/problem/reducers.test.js new file mode 100644 index 000000000..db1b828f2 --- /dev/null +++ b/src/editors/data/redux/problem/reducers.test.js @@ -0,0 +1,147 @@ +import { initialState, actions, reducer } from './reducers'; + +const testingState = { + ...initialState, + arbitraryField: 'arbitrary', +}; + +describe('problem reducer', () => { + it('has initial state', () => { + expect(reducer(undefined, {})).toEqual(initialState); + }); + + const testValue = 'roll for initiative'; + + describe('handling actions', () => { + const setterTest = (action, target) => { + describe(action, () => { + it(`load ${target} from payload`, () => { + expect(reducer(testingState, actions[action](testValue))).toEqual({ + ...testingState, + [target]: testValue, + }); + }); + }); + }; + [ + ['updateQuestion', 'question'], + ].map(args => setterTest(...args)); + describe('load', () => { + it('sets answers', () => { + const answer = { + id: 'A', + correct: false, + feedback: '', + selectedFeedback: undefined, + title: '', + unselectedFeedback: undefined, + }; + expect(reducer(testingState, actions.addAnswer(answer))).toEqual({ + ...testingState, + answers: [answer], + }); + }); + }); + describe('setProblemType', () => { + it('sets problemType', () => { + const payload = { selected: 'soMePRoblEMtYPe' }; + expect(reducer(testingState, actions.setProblemType(payload))).toEqual({ + ...testingState, + problemType: 'soMePRoblEMtYPe', + }); + }); + }); + describe('setEnableTypeSelection', () => { + it('sets problemType to null', () => { + expect(reducer(testingState, actions.setEnableTypeSelection())).toEqual({ + ...testingState, + problemType: null, + }); + }); + }); + describe('updateField', () => { + it('sets given parameter', () => { + const payload = { problemType: 'soMePRoblEMtYPe' }; + expect(reducer(testingState, actions.updateField(payload))).toEqual({ + ...testingState, + ...payload, + }); + }); + }); + describe('updateSettings', () => { + it('sets given settings parameter', () => { + const payload = { hints: ['soMehInt'] }; + expect(reducer(testingState, actions.updateSettings(payload))).toEqual({ + ...testingState, + settings: { + ...testingState.settings, + ...payload, + }, + }); + }); + }); + describe('addAnswer', () => { + it('sets answers', () => { + const answer = { + id: 'A', + correct: false, + feedback: '', + selectedFeedback: undefined, + title: '', + unselectedFeedback: undefined, + }; + expect(reducer(testingState, actions.addAnswer(answer))).toEqual({ + ...testingState, + answers: [answer], + }); + }); + }); + describe('updateAnswer', () => { + it('sets answers, as well as setting the correctAnswerCount ', () => { + const answer = { id: 'A', correct: true }; + expect(reducer( + { + ...testingState, + answers: [{ + id: 'A', + correct: false, + }], + }, + actions.updateAnswer(answer), + )).toEqual({ + ...testingState, + correctAnswerCount: 1, + answers: [{ id: 'A', correct: true }], + }); + }); + }); + describe('deleteAnswer', () => { + it('sets answers, as well as setting the correctAnswerCount ', () => { + const answer = { id: 'A' }; + expect(reducer( + { + ...testingState, + correctAnswerCount: 1, + answers: [{ + id: 'A', + correct: false, + }, + { + id: 'B', + correct: true, + }], + }, + actions.deleteAnswer(answer), + )).toEqual({ + ...testingState, + correctAnswerCount: 1, + answers: [ + { + id: 'A', + correct: true, + }], + }); + }); + }); + }); +}); diff --git a/src/editors/data/redux/problem/selectors.js b/src/editors/data/redux/problem/selectors.js index 532bc5289..a9c267600 100644 --- a/src/editors/data/redux/problem/selectors.js +++ b/src/editors/data/redux/problem/selectors.js @@ -6,6 +6,7 @@ const mkSimpleSelector = (cb) => createSelector([module.problemState], cb); export const simpleSelectors = { problemType: mkSimpleSelector(problemData => problemData.problemType), answers: mkSimpleSelector(problemData => problemData.answers), + correctAnswerCount: mkSimpleSelector(problemData => problemData.correctAnswerCount), settings: mkSimpleSelector(problemData => problemData.settings), question: mkSimpleSelector(problemData => problemData.question), completeState: mkSimpleSelector(problemData => problemData), diff --git a/src/editors/data/redux/problem/selectors.test.js b/src/editors/data/redux/problem/selectors.test.js new file mode 100644 index 000000000..b4d36bb13 --- /dev/null +++ b/src/editors/data/redux/problem/selectors.test.js @@ -0,0 +1,52 @@ +// import * in order to mock in-file references +import { keyStore } from '../../../utils'; +import * as selectors from './selectors'; + +jest.mock('reselect', () => ({ + createSelector: jest.fn((preSelectors, cb) => ({ preSelectors, cb })), +})); + +const testState = { some: 'arbitraryValue' }; +const testValue = 'my VALUE'; + +describe('problem selectors unit tests', () => { + const { + problemState, + simpleSelectors, + } = selectors; + describe('problemState', () => { + it('returns the problem data', () => { + expect(problemState({ ...testState, problem: testValue })).toEqual(testValue); + }); + }); + describe('simpleSelectors', () => { + const testSimpleSelector = (key) => { + test(`${key} simpleSelector returns its value from the problem store`, () => { + const { preSelectors, cb } = simpleSelectors[key]; + expect(preSelectors).toEqual([problemState]); + expect(cb({ ...testState, [key]: testValue })).toEqual(testValue); + }); + }; + const simpleKeys = keyStore(simpleSelectors); + describe('simple selectors link their values from problem store', () => { + [ + simpleKeys.problemType, + simpleKeys.answers, + simpleKeys.correctAnswerCount, + simpleKeys.settings, + simpleKeys.question, + ].map(testSimpleSelector); + }); + test('simple selector completeState equals the entire state', () => { + const { preSelectors, cb } = simpleSelectors[simpleKeys.completeState]; + expect(preSelectors).toEqual([problemState]); + expect(cb({ + ...testState, + [simpleKeys.completeState]: testValue, + })).toEqual({ + ...testState, + [simpleKeys.completeState]: testValue, + }); + }); + }); +});