diff --git a/src/components/GradesView/EditModal/ModalHeaders.jsx b/src/components/GradesView/EditModal/ModalHeaders.jsx index df52d98..d9f4ca2 100644 --- a/src/components/GradesView/EditModal/ModalHeaders.jsx +++ b/src/components/GradesView/EditModal/ModalHeaders.jsx @@ -1,68 +1,53 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { useIntl } from '@edx/frontend-platform/i18n'; -import selectors from 'data/selectors'; +import { StrictDict } from 'utils'; +import { selectors } from 'data/redux/hooks'; import messages from './messages'; import HistoryHeader from './HistoryHeader'; +export const HistoryKeys = StrictDict({ + assignment: 'assignment', + student: 'student', + originalGrade: 'original-grade', + currentGrade: 'current-grade', +}); + /** * * Provides a list of HistoryHeaders for the student name, assignment, * original grade, and current override grade. */ -export const ModalHeaders = ({ - modalState, - originalGrade, - currentGrade, -}) => ( -
- } - value={modalState.assignmentName} - /> - } - value={modalState.updateUserName} - /> - } - value={originalGrade} - /> - } - value={currentGrade} - /> -
-); -ModalHeaders.defaultProps = { - currentGrade: null, - originalGrade: null, -}; -ModalHeaders.propTypes = { - // redux - currentGrade: PropTypes.number, - originalGrade: PropTypes.number, - modalState: PropTypes.shape({ - assignmentName: PropTypes.string.isRequired, - updateUserName: PropTypes.string, - }).isRequired, +export const ModalHeaders = () => { + const { assignmentName, updateUserName } = selectors.app.useModalData(); + const { currentGrade, originalGrade } = selectors.grades.useGradeData(); + const { formatMessage } = useIntl(); + return ( +
+ + + + +
+ ); }; -export const mapStateToProps = (state) => ({ - modalState: { - assignmentName: selectors.app.modalState.assignmentName(state), - updateUserName: selectors.app.modalState.updateUserName(state), - }, - currentGrade: selectors.grades.gradeOverrideCurrentEarnedGradedOverride(state), - originalGrade: selectors.grades.gradeOriginalEarnedGraded(state), -}); - -export default connect(mapStateToProps)(ModalHeaders); +export default ModalHeaders; diff --git a/src/components/GradesView/EditModal/ModalHeaders.test.jsx b/src/components/GradesView/EditModal/ModalHeaders.test.jsx index 57c2361..382eb6a 100644 --- a/src/components/GradesView/EditModal/ModalHeaders.test.jsx +++ b/src/components/GradesView/EditModal/ModalHeaders.test.jsx @@ -1,93 +1,84 @@ import React from 'react'; import { shallow } from 'enzyme'; -import selectors from 'data/selectors'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { selectors } from 'data/redux/hooks'; -import { - ModalHeaders, - mapStateToProps, -} from './ModalHeaders'; +import { formatMessage } from 'testUtils'; + +import HistoryHeader from './HistoryHeader'; +import ModalHeaders, { HistoryKeys } from './ModalHeaders'; +import messages from './messages'; jest.mock('./HistoryHeader', () => 'HistoryHeader'); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - app: { - editUpdateData: jest.fn(state => ({ editUpdateData: state })), - modalState: { - assignmentName: jest.fn(state => ({ assignmentName: state })), - updateUserName: jest.fn(state => ({ updateUserName: state })), - }, - }, - grades: { - gradeOverrideCurrentEarnedGradedOverride: jest.fn(state => ({ currentGrade: state })), - gradeOriginalEarnedGraded: jest.fn(state => ({ originalGrade: state })), - }, +jest.mock('data/redux/hooks', () => ({ + selectors: { + app: { useModalData: jest.fn() }, + grades: { useGradeData: jest.fn() }, }, })); -describe('ModalHeaders', () => { - let el; - const props = { - currentGrade: 2, - originalGrade: 20, - modalState: { - assignmentName: 'Qwerty', - updateUserName: 'Uiop', - }, - }; - describe('Component', () => { - describe('snapshots', () => { - beforeEach(() => { - }); - describe('gradeOverrideHistoryError is and empty and open is true', () => { - test('modal open and StatusAlert showing', () => { - el = shallow(); - expect(el).toMatchSnapshot(); - }); - }); - describe('gradeOverrideHistoryError is empty and open is false', () => { - test('modal closed and StatusAlert closed', () => { - el = shallow( - , - ); - expect(el).toMatchSnapshot(); - }); - }); +const modalData = { + assignmentName: 'test-assignment-name', + updateUserName: 'test-user-name', +}; +selectors.app.useModalData.mockReturnValue(modalData); +const gradeData = { + currentGrade: 'test-current-grade', + originalGrade: 'test-original-grade', +}; +selectors.grades.useGradeData.mockReturnValue(gradeData); + +let el; +describe('ModalHeaders', () => { + beforeEach(() => { + jest.clearAllMocks(); + el = shallow(); + }); + describe('behavior', () => { + it('initializes intl', () => { + expect(useIntl).toHaveBeenCalled(); + }); + it('initializes redux hooks', () => { + expect(selectors.app.useModalData).toHaveBeenCalled(); + expect(selectors.grades.useGradeData).toHaveBeenCalled(); }); }); - - describe('mapStateToProps', () => { - const testState = { he: 'lives in a', pineapple: 'under the sea' }; - let mapped; - beforeEach(() => { - mapped = mapStateToProps(testState); + describe('render', () => { + test('snapshot', () => { + expect(el).toMatchSnapshot(); }); - describe('modalState', () => { - test('assignmentName from app.modalState.assignmentName', () => { - expect( - mapped.modalState.assignmentName, - ).toEqual(selectors.app.modalState.assignmentName(testState)); - }); - test('updateUserName from app.modalState.updateUserName', () => { - expect( - mapped.modalState.updateUserName, - ).toEqual(selectors.app.modalState.updateUserName(testState)); + test('assignment header', () => { + const headerProps = el.find(HistoryHeader).at(0).props(); + expect(headerProps).toMatchObject({ + id: HistoryKeys.assignment, + label: formatMessage(messages.assignmentHeader), + value: modalData.assignmentName, }); }); - describe('originalGrade', () => { - test('from grades.gradeOverrideCurrentEarnedGradedOverride', () => { - expect(mapped.currentGrade).toEqual( - selectors.grades.gradeOverrideCurrentEarnedGradedOverride(testState), - ); + test('student header', () => { + const headerProps = el.find(HistoryHeader).at(1).props(); + expect(headerProps).toMatchObject({ + id: HistoryKeys.student, + label: formatMessage(messages.studentHeader), + value: modalData.updateUserName, }); }); - describe('originalGrade', () => { - test('from grades.gradeOriginalEarnedGrades', () => { - expect(mapped.originalGrade).toEqual( - selectors.grades.gradeOriginalEarnedGraded(testState), - ); + test('originalGrade header', () => { + const headerProps = el.find(HistoryHeader).at(2).props(); + expect(headerProps).toMatchObject({ + id: HistoryKeys.originalGrade, + label: formatMessage(messages.originalGradeHeader), + value: gradeData.originalGrade, + }); + }); + test('currentGrade header', () => { + const headerProps = el.find(HistoryHeader).at(3).props(); + expect(headerProps).toMatchObject({ + id: HistoryKeys.currentGrade, + label: formatMessage(messages.currentGradeHeader), + value: gradeData.currentGrade, }); }); }); diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput.test.jsx b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput.test.jsx deleted file mode 100644 index 895fc00..0000000 --- a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput.test.jsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import actions from 'data/actions'; -import selectors from 'data/selectors'; - -import { - AdjustedGradeInput, - mapStateToProps, - mapDispatchToProps, -} from './AdjustedGradeInput'; - -jest.mock('@edx/paragon', () => ({ - Form: { Control: () => 'Form.Control' }, -})); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - root: { - editModalPossibleGrade: jest.fn(state => ({ updateUserName: state })), - }, - app: { - modalState: { adjustedGradeValue: jest.fn(state => ({ adjustedGradeValue: state })) }, - }, - }, -})); -jest.mock('data/actions', () => ({ - __esModule: true, - default: { - app: { setModalState: jest.fn() }, - }, -})); -describe('AdjustedGradeInput', () => { - let el; - let props = { - value: 1, - possibleGrade: 5, - }; - beforeEach(() => { - props = { - ...props, - setModalState: jest.fn(), - }; - }); - describe('Component', () => { - beforeEach(() => { - el = shallow(); - }); - describe('snapshots', () => { - test('displays input control and "out of possible grade" label', () => { - el.instance().onChange = jest.fn().mockName('this.onChange'); - expect(el.instance().render()).toMatchSnapshot(); - }); - }); - describe('behavior', () => { - describe('onChange', () => { - it('calls props.setModalState event target value', () => { - const value = 42; - el.instance().onChange({ target: { value } }); - expect(props.setModalState).toHaveBeenCalledWith({ - adjustedGradeValue: value, - }); - }); - }); - }); - }); - - describe('mapStateToProps', () => { - const testState = { like: 'no one', ever: 'was' }; - let mapped; - beforeEach(() => { - mapped = mapStateToProps(testState); - }); - describe('modalState', () => { - test('possibleGrade from root.editModalPossibleGrade', () => { - expect( - mapped.possibleGrade, - ).toEqual(selectors.root.editModalPossibleGrade(testState)); - }); - test('updateUserName from app.modalState.updateUserName', () => { - expect( - mapped.value, - ).toEqual(selectors.app.modalState.adjustedGradeValue(testState)); - }); - }); - }); - describe('mapDispatchToProps', () => { - test('setModalState from actions.app.setModalState', () => { - expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState); - }); - }); -}); diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/__snapshots__/index.test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..b12be4b --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/__snapshots__/index.test.jsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AdjustedGradeInput component render snapshot 1`] = ` + + + some-hint-text + +`; diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.js b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.js new file mode 100644 index 0000000..2b6432a --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.js @@ -0,0 +1,21 @@ +import { actions, selectors } from 'data/redux/hooks'; +import { getLocalizedSlash } from 'i18n'; + +const useAdjustedGradeInputData = () => { + const possibleGrade = selectors.root.useEditModalPossibleGrade(); + const value = selectors.app.useModalData().adjustedGradeValue; + const setModalState = actions.app.useSetModalState(); + const hintText = possibleGrade && ` ${getLocalizedSlash()} ${possibleGrade}`; + + const onChange = ({ target }) => { + setModalState({ adjustedGradeValue: target.value }); + }; + + return { + value, + onChange, + hintText, + }; +}; + +export default useAdjustedGradeInputData; diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.test.jsx b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.test.jsx new file mode 100644 index 0000000..685d80c --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.test.jsx @@ -0,0 +1,69 @@ +import React from 'react'; + +import { getLocalizedSlash } from 'i18n'; +import { actions, selectors } from 'data/redux/hooks'; +import useAdjustedGradeInputData from './hooks'; + +jest.mock('data/redux/hooks', () => ({ + selectors: { + root: { + useEditModalPossibleGrade: jest.fn(), + }, + app: { + useModalData: jest.fn(), + }, + }, + actions: { + app: { + useSetModalState: jest.fn(), + }, + }, +})); +jest.mock('i18n', () => ({ getLocalizedSlash: jest.fn() })); + +const localizedSlash = 'localized-slash'; +getLocalizedSlash.mockReturnValue(localizedSlash); + +const possibleGrade = 105; +selectors.root.useEditModalPossibleGrade.mockReturnValue(possibleGrade); +const modalData = { adjustedGradeValue: 70 }; +const setModalState = jest.fn(); +selectors.app.useModalData.mockReturnValue(modalData); +actions.app.useSetModalState.mockReturnValue(setModalState); + +let out; +describe('useAdjustedGradeInputData hook', () => { + beforeEach(() => { + jest.clearAllMocks(); + out = useAdjustedGradeInputData(); + }); + describe('behavior', () => { + it('initializes redux hooks', () => { + expect(selectors.root.useEditModalPossibleGrade).toHaveBeenCalled(); + expect(selectors.app.useModalData).toHaveBeenCalled(); + expect(actions.app.useSetModalState).toHaveBeenCalled(); + }); + }); + describe('output', () => { + it('forwards adjusted grade value as value from modal data', () => { + expect(out.value).toEqual(modalData.adjustedGradeValue); + }); + describe('hintText', () => { + it('passes an undefined value if possibleGrade is not available', () => { + selectors.root.useEditModalPossibleGrade.mockReturnValueOnce(undefined); + out = useAdjustedGradeInputData(); + expect(out.hintText).toEqual(undefined); + }); + it('passes localized slash and possible grade if available', () => { + expect(out.hintText).toEqual(` ${localizedSlash} ${possibleGrade}`); + }); + }); + describe('onChange', () => { + it('sets modal state with event target value', () => { + const testValue = 'test-value'; + out.onChange({ target: { value: testValue } }); + expect(setModalState).toHaveBeenCalledWith({ adjustedGradeValue: testValue }); + }); + }); + }); +}); diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.jsx b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.jsx new file mode 100644 index 0000000..d070b9b --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.jsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import { Form } from '@edx/paragon'; + +import useAdjustedGradeInputData from './hooks'; + +/** + * + * Input control for adjusting the grade of a unit + * displays an "/ ${possibleGrade} if there is one in the data model. + */ +export const AdjustedGradeInput = () => { + const { + value, + onChange, + hintText, + } = useAdjustedGradeInputData(); + return ( + + + {hintText} + + ); +}; + +AdjustedGradeInput.propTypes = {}; + +export default AdjustedGradeInput; diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.test.jsx b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.test.jsx new file mode 100644 index 0000000..895dc6f --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.test.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Form } from '@edx/paragon'; + +import useAdjustedGradeInputData from './hooks'; +import AdjustedGradeInput from '.'; + +jest.mock('./hooks', () => jest.fn()); + +const hookProps = { + hintText: 'some-hint-text', + onChange: jest.fn().mockName('hook.onChange'), + value: 'test-value', +}; +useAdjustedGradeInputData.mockReturnValue(hookProps); + +let el; +describe('AdjustedGradeInput component', () => { + beforeEach(() => { + jest.clearAllMocks(); + el = shallow(); + }); + describe('behavior', () => { + it('initializes hook data', () => { + expect(useAdjustedGradeInputData).toHaveBeenCalled(); + }); + }); + describe('render', () => { + test('snapshot', () => { + expect(el).toMatchSnapshot(); + const control = el.find(Form.Control); + expect(control.props().value).toEqual(hookProps.value); + expect(control.props().onChange).toEqual(hookProps.onChange); + expect(el.contains(hookProps.hintText)).toEqual(true); + }); + }); +}); diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput.jsx deleted file mode 100644 index a9e1c18..0000000 --- a/src/components/GradesView/EditModal/OverrideTable/ReasonInput.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { Form } from '@edx/paragon'; - -import selectors from 'data/selectors'; -import actions from 'data/actions'; - -/** - * - * Input control for the "reason for change" field in the Edit modal. - */ -export class ReasonInput extends React.Component { - constructor(props) { - super(props); - this.ref = React.createRef(); - this.onChange = this.onChange.bind(this); - } - - componentDidMount() { - this.ref.current.focus(); - } - - onChange = (event) => { - this.props.setModalState({ reasonForChange: event.target.value }); - }; - - render() { - return ( - - ); - } -} -ReasonInput.propTypes = { - // redux - setModalState: PropTypes.func.isRequired, - value: PropTypes.string.isRequired, -}; - -export const mapStateToProps = (state) => ({ - value: selectors.app.modalState.reasonForChange(state), -}); - -export const mapDispatchToProps = { - setModalState: actions.app.setModalState, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ReasonInput); diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput.test.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput.test.jsx deleted file mode 100644 index 5f9c311..0000000 --- a/src/components/GradesView/EditModal/OverrideTable/ReasonInput.test.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import actions from 'data/actions'; -import selectors from 'data/selectors'; - -import { - ReasonInput, - mapStateToProps, - mapDispatchToProps, -} from './ReasonInput'; - -jest.mock('@edx/paragon', () => ({ - Form: { Control: () => 'Form.Control' }, -})); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - app: { - modalState: { reasonForChange: jest.fn(state => ({ reasonForChange: state })) }, - }, - }, -})); -jest.mock('data/actions', () => ({ - __esModule: true, - default: { - app: { setModalState: jest.fn() }, - }, -})); -describe('ReasonInput', () => { - let el; - let props = { - value: 'did not answer the question', - }; - beforeEach(() => { - props = { - ...props, - setModalState: jest.fn(), - }; - }); - describe('Component', () => { - beforeEach(() => { - el = shallow(, { disableLifecycleMethods: true }); - }); - describe('snapshots', () => { - test('displays reason for change input control', () => { - el.instance().onChange = jest.fn().mockName('this.onChange'); - expect(el.instance().render()).toMatchSnapshot(); - }); - }); - describe('behavior', () => { - describe('onChange', () => { - it('calls props.setModalState event target value', () => { - const value = 42; - el.instance().onChange({ target: { value } }); - expect(props.setModalState).toHaveBeenCalledWith({ - reasonForChange: value, - }); - }); - }); - describe('componentDidMount', () => { - it('focuses the input ref', () => { - const focus = jest.fn(); - expect(el.instance().ref).toEqual({ current: null }); - el.instance().ref.current = { focus }; - el.instance().componentDidMount(); - expect(el.instance().ref.current.focus).toHaveBeenCalledWith(); - }); - }); - }); - }); - - describe('mapStateToProps', () => { - const testState = { to: { catchThem: 'my real test', trainThem: 'my cause!' } }; - let mapped; - beforeEach(() => { - mapped = mapStateToProps(testState); - }); - describe('modalState', () => { - test('value from app.modalState.reasonForChange', () => { - expect(mapped.value).toEqual(selectors.app.modalState.reasonForChange(testState)); - }); - }); - }); - describe('mapDispatchToProps', () => { - test('setModalState from actions.app.setModalState', () => { - expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState); - }); - }); -}); diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/__snapshots__/index.test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..153368f --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/__snapshots__/index.test.jsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReasonInput component render snapshot 1`] = ` + +`; diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.js b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.js new file mode 100644 index 0000000..be0071b --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.js @@ -0,0 +1,25 @@ +import React from 'react'; + +import { actions, selectors } from 'data/redux/hooks'; + +const useReasonInputData = () => { + const ref = React.useRef(); + const { reasonForChange } = selectors.app.useModalData(); + const setModalState = actions.app.useSetModalState(); + + React.useEffect(() => { + ref.current.focus(); + }, [ref]); + + const onChange = (event) => { + setModalState({ reasonForChange: event.target.value }); + }; + + return { + value: reasonForChange, + onChange, + ref, + }; +}; + +export default useReasonInputData; diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.test.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.test.jsx new file mode 100644 index 0000000..6b5acec --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.test.jsx @@ -0,0 +1,63 @@ +import React from 'react'; + +import { actions, selectors } from 'data/redux/hooks'; +import useReasonInputData from './hooks'; + +jest.mock('data/redux/hooks', () => ({ + selectors: { + app: { + useModalData: jest.fn(), + }, + }, + actions: { + app: { + useSetModalState: jest.fn(), + }, + }, +})); + +const modalData = { reasonForChange: 'test-reason-for-change' }; +const setModalState = jest.fn(); +selectors.app.useModalData.mockReturnValue(modalData); +actions.app.useSetModalState.mockReturnValue(setModalState); + +const ref = { current: { focus: jest.fn() }, useRef: true }; +React.useRef.mockReturnValue(ref); + +let out; +describe('useReasonInputData hook', () => { + beforeEach(() => { + jest.clearAllMocks(); + out = useReasonInputData(); + }); + describe('behavior', () => { + it('initializes ref', () => { + expect(React.useRef).toHaveBeenCalled(); + }); + it('initializes redux hooks', () => { + expect(selectors.app.useModalData).toHaveBeenCalled(); + expect(actions.app.useSetModalState).toHaveBeenCalled(); + }); + it('focuses ref on load', () => { + const [[cb, prereqs]] = React.useEffect.mock.calls; + expect(prereqs).toEqual([ref]); + cb(); + expect(ref.current.focus).toHaveBeenCalled(); + }); + }); + describe('output', () => { + it('forwards reasonForChange as value from modal data', () => { + expect(out.value).toEqual(modalData.reasonForChange); + }); + it('forwards ref', () => { + expect(out.ref).toEqual(ref); + }); + describe('onChange', () => { + it('sets modal state with event target value', () => { + const testValue = 'test-value'; + out.onChange({ target: { value: testValue } }); + expect(setModalState).toHaveBeenCalledWith({ reasonForChange: testValue }); + }); + }); + }); +}); diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.jsx new file mode 100644 index 0000000..b76bd21 --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.jsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { Form } from '@edx/paragon'; + +import useReasonInputData from './hooks'; + +export const controlTestId = 'reason-input-control'; + +/** + * + * Input control for the "reason for change" field in the Edit modal. + */ +export const ReasonInput = () => { + const { ref, value, onChange } = useReasonInputData(); + return ( + + ); +}; + +ReasonInput.propTypes = {}; + +export default ReasonInput; diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.test.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.test.jsx new file mode 100644 index 0000000..8cf8134 --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.test.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Form } from '@edx/paragon'; + +import useReasonInputData from './hooks'; +import ReasonInput from '.'; + +jest.mock('./hooks', () => jest.fn()); + +const hookProps = { + ref: 'reason-input-ref', + onChange: jest.fn().mockName('hook.onChange'), + value: 'test-value', +}; +useReasonInputData.mockReturnValue(hookProps); + +let el; +describe('ReasonInput component', () => { + beforeEach(() => { + jest.clearAllMocks(); + el = shallow(); + }); + describe('behavior', () => { + it('initializes hook data', () => { + expect(useReasonInputData).toHaveBeenCalled(); + }); + }); + describe('render', () => { + test('snapshot', () => { + expect(el).toMatchSnapshot(); + const control = el.find(Form.Control); + expect(control.props().value).toEqual(hookProps.value); + expect(control.props().onChange).toEqual(hookProps.onChange); + }); + }); +}); diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/ref.test.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/ref.test.jsx new file mode 100644 index 0000000..e93bd75 --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/ref.test.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import useReasonInputData from './hooks'; +import ReasonInput, { controlTestId } from '.'; + +jest.unmock('react'); +jest.unmock('@edx/paragon'); +jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() })); + +const focus = jest.fn(); +const props = { + value: 'test-value', + onChange: jest.fn(), + ref: { current: { focus }, useRef: jest.fn() }, +}; +useReasonInputData.mockReturnValue(props); + +let el; +describe('ReasonInput ref', () => { + it('loads ref from hook', () => { + el = render(); + const control = el.getByTestId(controlTestId); + expect(control).toEqual(props.ref.current); + }); +}); diff --git a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/AdjustedGradeInput.test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/__snapshots__/AdjustedGradeInput.test.jsx.snap deleted file mode 100644 index 93a0024..0000000 --- a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/AdjustedGradeInput.test.jsx.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AdjustedGradeInput Component snapshots displays input control and "out of possible grade" label 1`] = ` - - - / 5 - -`; diff --git a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/ReasonInput.test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/__snapshots__/ReasonInput.test.jsx.snap deleted file mode 100644 index 5931cf9..0000000 --- a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/ReasonInput.test.jsx.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReasonInput Component snapshots displays reason for change input control 1`] = ` - -`; diff --git a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/index.test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..9fb2a58 --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/__snapshots__/index.test.jsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OverrideTable component render snapshot 1`] = ` +, + "date": Object { + "formatted": 2000-01-01T00:00:00.000Z, + }, + "reason": , + }, + ] + } + itemCount={2} +/> +`; diff --git a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/__snapshots__/test.jsx.snap deleted file mode 100644 index 1f1169b..0000000 --- a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/test.jsx.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = ` -, - "accessor": "date", - }, - Object { - "Header": , - "accessor": "grader", - }, - Object { - "Header": , - "accessor": "reason", - }, - Object { - "Header": , - "accessor": "adjustedGrade", - }, - ] - } - data={ - Array [ - Object { - "adjustedGrade": 0, - "date": "yesterday", - "grader": "me", - "reason": "you ate my sandwich", - }, - Object { - "adjustedGrade": 20, - "date": "today", - "grader": "me", - "reason": "you brought me a new sandwich", - }, - Object { - "adjustedGrade": , - "date": "todaaaaaay", - "reason": , - }, - ] - } - itemCount={2} -/> -`; diff --git a/src/components/GradesView/EditModal/OverrideTable/hooks.js b/src/components/GradesView/EditModal/OverrideTable/hooks.js new file mode 100644 index 0000000..d4dfc14 --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/hooks.js @@ -0,0 +1,26 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { gradeOverrideHistoryColumns as columns } from 'data/constants/app'; +import { selectors } from 'data/redux/hooks'; + +import messages from './messages'; + +const useOverrideTableData = () => { + const { formatMessage } = useIntl(); + + const hide = selectors.grades.useHasOverrideErrors(); + const gradeOverrides = selectors.grades.useGradeData().gradeOverrideHistoryResults; + const tableProps = {}; + if (!hide) { + tableProps.columns = [ + { Header: formatMessage(messages.dateHeader), accessor: columns.date }, + { Header: formatMessage(messages.graderHeader), accessor: columns.grader }, + { Header: formatMessage(messages.reasonHeader), accessor: columns.reason }, + { Header: formatMessage(messages.adjustedGradeHeader), accessor: columns.adjustedGrade }, + ]; + tableProps.data = gradeOverrides; + } + return { hide, ...tableProps }; +}; + +export default useOverrideTableData; diff --git a/src/components/GradesView/EditModal/OverrideTable/hooks.test.js b/src/components/GradesView/EditModal/OverrideTable/hooks.test.js new file mode 100644 index 0000000..8e684f0 --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/hooks.test.js @@ -0,0 +1,78 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; +import { formatMessage } from 'testUtils'; + +import { gradeOverrideHistoryColumns as columns } from 'data/constants/app'; +import { selectors } from 'data/redux/hooks'; + +import useOverrideTableData from './hooks'; +import messages from './messages'; + +jest.mock('data/redux/hooks', () => ({ + selectors: { + grades: { + useHasOverrideErrors: jest.fn(), + useGradeData: jest.fn(), + }, + }, +})); + +selectors.grades.useHasOverrideErrors.mockReturnValue(false); +const gradeOverrides = ['some', 'override', 'data']; +const gradeData = { gradeOverrideHistoryResults: gradeOverrides }; +selectors.grades.useGradeData.mockReturnValue(gradeData); + +let out; +describe('useOverrideTableData', () => { + beforeEach(() => { + jest.clearAllMocks(); + out = useOverrideTableData(); + }); + describe('behavior', () => { + it('initializes intl hook', () => { + expect(useIntl).toHaveBeenCalled(); + }); + it('initializes redux hooks', () => { + expect(selectors.grades.useHasOverrideErrors).toHaveBeenCalled(); + expect(selectors.grades.useGradeData).toHaveBeenCalled(); + }); + }); + describe('output', () => { + describe('no errors', () => { + test('hide is false', () => { + expect(out.hide).toEqual(false); + }); + describe('columns', () => { + test('date column', () => { + const { Header, accessor } = out.columns[0]; + expect(Header).toEqual(formatMessage(messages.dateHeader)); + expect(accessor).toEqual(columns.date); + }); + test('grader column', () => { + const { Header, accessor } = out.columns[1]; + expect(Header).toEqual(formatMessage(messages.graderHeader)); + expect(accessor).toEqual(columns.grader); + }); + test('reason column', () => { + const { Header, accessor } = out.columns[2]; + expect(Header).toEqual(formatMessage(messages.reasonHeader)); + expect(accessor).toEqual(columns.reason); + }); + test('adjustedGrade column', () => { + const { Header, accessor } = out.columns[3]; + expect(Header).toEqual(formatMessage(messages.adjustedGradeHeader)); + expect(accessor).toEqual(columns.adjustedGrade); + }); + }); + test('data passed from grade data', () => { + expect(out.data).toEqual(gradeOverrides); + }); + }); + describe('with errors', () => { + it('returns hide true and no other fields', () => { + selectors.grades.useHasOverrideErrors.mockReturnValue(true); + out = useOverrideTableData(); + expect(out).toEqual({ hide: true }); + }); + }); + }); +}); diff --git a/src/components/GradesView/EditModal/OverrideTable/index.jsx b/src/components/GradesView/EditModal/OverrideTable/index.jsx index bd5e765..59a524e 100644 --- a/src/components/GradesView/EditModal/OverrideTable/index.jsx +++ b/src/components/GradesView/EditModal/OverrideTable/index.jsx @@ -1,73 +1,38 @@ /* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */ import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; import { DataTable } from '@edx/paragon'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import { gradeOverrideHistoryColumns as columns } from 'data/constants/app'; -import selectors from 'data/selectors'; +import { formatDateForDisplay } from 'utils'; -import messages from './messages'; import ReasonInput from './ReasonInput'; import AdjustedGradeInput from './AdjustedGradeInput'; +import useOverrideTableData from './hooks'; /** * * Table containing previous grade override entries, and an "edit" row * with todays date, an AdjustedGradeInput and a ReasonInput */ -export const OverrideTable = ({ - hide, - gradeOverrides, - todaysDate, -}) => { - if (hide) { - return null; - } - return ( + +export const OverrideTable = () => { + const { hide, columns, data } = useOverrideTableData(); + + return hide ? null : ( , accessor: columns.date }, - { Header: , accessor: columns.grader }, - { Header: , accessor: columns.reason }, - { - Header: , - accessor: columns.adjustedGrade, - }, - ]} + columns={columns} data={[ - ...gradeOverrides, + ...data, { adjustedGrade: , - date: todaysDate, + date: formatDateForDisplay(new Date()), reason: , }, ]} - itemCount={gradeOverrides.length} + itemCount={data.length} /> ); }; -OverrideTable.defaultProps = { - gradeOverrides: [], -}; -OverrideTable.propTypes = { - // redux - gradeOverrides: PropTypes.arrayOf(PropTypes.shape({ - date: PropTypes.string, - grader: PropTypes.string, - reason: PropTypes.string, - adjustedGrade: PropTypes.number, - })), - hide: PropTypes.bool.isRequired, - todaysDate: PropTypes.string.isRequired, -}; +OverrideTable.propTypes = {}; -export const mapStateToProps = (state) => ({ - hide: selectors.grades.hasOverrideErrors(state), - gradeOverrides: selectors.grades.gradeOverrides(state), - todaysDate: selectors.app.modalState.todaysDate(state), -}); - -export default connect(mapStateToProps)(OverrideTable); +export default OverrideTable; diff --git a/src/components/GradesView/EditModal/OverrideTable/index.test.jsx b/src/components/GradesView/EditModal/OverrideTable/index.test.jsx new file mode 100644 index 0000000..f1affef --- /dev/null +++ b/src/components/GradesView/EditModal/OverrideTable/index.test.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { DataTable } from '@edx/paragon'; + +import { formatDateForDisplay } from 'utils'; + +import AdjustedGradeInput from './AdjustedGradeInput'; +import ReasonInput from './ReasonInput'; +import useOverrideTableData from './hooks'; +import OverrideTable from '.'; + +jest.mock('utils', () => ({ + formatDateForDisplay: (date) => ({ formatted: date }), +})); +jest.mock('./hooks', () => jest.fn()); +jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput'); +jest.mock('./ReasonInput', () => 'ReasonInput'); + +const hookProps = { + hide: false, + data: [ + { test: 'data' }, + { andOther: 'test-data' }, + ], + columns: 'test-columns', +}; +useOverrideTableData.mockReturnValue(hookProps); + +let el; +describe('OverrideTable component', () => { + beforeEach(() => { + jest + .clearAllMocks() + .useFakeTimers('modern') + .setSystemTime(new Date('2000-01-01')); + el = shallow(); + }); + describe('behavior', () => { + it('initializes hook data', () => { + expect(useOverrideTableData).toHaveBeenCalled(); + }); + }); + describe('render', () => { + test('null render if hide', () => { + useOverrideTableData.mockReturnValueOnce({ ...hookProps, hide: true }); + el = shallow(); + expect(el.isEmptyRender()).toEqual(true); + }); + test('snapshot', () => { + expect(el).toMatchSnapshot(); + const table = el.find(DataTable); + expect(table.props().columns).toEqual(hookProps.columns); + const data = [...table.props().data]; + const inputRow = data.pop(); + const formattedDate = formatDateForDisplay(new Date()); + expect(data).toEqual(hookProps.data); + expect(inputRow).toMatchObject({ + adjustedGrade: , + date: formattedDate, + reason: , + }); + }); + }); +}); diff --git a/src/components/GradesView/EditModal/OverrideTable/test.jsx b/src/components/GradesView/EditModal/OverrideTable/test.jsx deleted file mode 100644 index 96149b4..0000000 --- a/src/components/GradesView/EditModal/OverrideTable/test.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import selectors from 'data/selectors'; - -import { - OverrideTable, - mapStateToProps, -} from '.'; - -jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' })); -jest.mock('./ReasonInput', () => 'ReasonInput'); -jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput'); - -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - app: { - modalState: { - todaysDate: jest.fn(state => ({ todaysDate: state })), - }, - }, - grades: { - hasOverrideErrors: jest.fn(state => ({ hasOverrideErrors: state })), - gradeOverrides: jest.fn(state => ({ gradeOverrides: state })), - }, - }, -})); - -describe('OverrideTable', () => { - const props = { - gradeOverrides: [ - { - date: 'yesterday', - grader: 'me', - reason: 'you ate my sandwich', - adjustedGrade: 0, - }, - { - date: 'today', - grader: 'me', - reason: 'you brought me a new sandwich', - adjustedGrade: 20, - }, - ], - hide: false, - todaysDate: 'todaaaaaay', - }; - - describe('Component', () => { - describe('snapshots', () => { - it('returns null if hide is true', () => { - expect(shallow()).toEqual({}); - }); - describe('basic snapshot', () => { - test('shows a row for each entry and one editable row', () => { - expect(shallow()).toMatchSnapshot(); - }); - }); - }); - }); - - describe('mapStateToProps', () => { - const testState = { I: 'wanna', be: 'the', very: 'best' }; - let mapped; - beforeEach(() => { - mapped = mapStateToProps(testState); - }); - describe('modalState', () => { - test('hide from grades.hasOverrideErrors', () => { - expect(mapped.hide).toEqual(selectors.grades.hasOverrideErrors(testState)); - }); - test('gradeOverrides from grades.gradeOverrides', () => { - expect(mapped.gradeOverrides).toEqual(selectors.grades.gradeOverrides(testState)); - }); - test('todaysData from app.modalState.todaysDate', () => { - expect(mapped.todaysDate).toEqual(selectors.app.modalState.todaysDate(testState)); - }); - }); - }); -}); diff --git a/src/components/GradesView/EditModal/__snapshots__/ModalHeaders.test.jsx.snap b/src/components/GradesView/EditModal/__snapshots__/ModalHeaders.test.jsx.snap index 0cce19b..22efe80 100644 --- a/src/components/GradesView/EditModal/__snapshots__/ModalHeaders.test.jsx.snap +++ b/src/components/GradesView/EditModal/__snapshots__/ModalHeaders.test.jsx.snap @@ -1,99 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = ` +exports[`ModalHeaders render snapshot 1`] = `
- } - value="Qwerty" + label="Assignment" + value="test-assignment-name" /> - } - value="Uiop" + label="Student" + value="test-user-name" /> - } - value={20} + label="Original Grade" + value="test-original-grade" /> - } - value={2} - /> -
-`; - -exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = ` -
- - } - value="Qwerty" - /> - - } - value="Uiop" - /> - - } - value={20} - /> - - } - value={2} + label="Current Grade" + value="test-current-grade" />
`; diff --git a/src/components/GradesView/EditModal/__snapshots__/index.test.jsx.snap b/src/components/GradesView/EditModal/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..46a0c51 --- /dev/null +++ b/src/components/GradesView/EditModal/__snapshots__/index.test.jsx.snap @@ -0,0 +1,91 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EditModal component render with error snapshot 1`] = ` + + +
+ + + test-error + + +
+ Showing most recent actions (max 5). To see more, please contact support +
+
+ Note: Once you save, your changes will be visible to students. +
+
+
+ + + + Cancel + + + + +
+`; + +exports[`EditModal component render without error snapshot 1`] = ` + + +
+ + + +
+ Showing most recent actions (max 5). To see more, please contact support +
+
+ Note: Once you save, your changes will be visible to students. +
+
+
+ + + + Cancel + + + + +
+`; diff --git a/src/components/GradesView/EditModal/__snapshots__/test.jsx.snap b/src/components/GradesView/EditModal/__snapshots__/test.jsx.snap deleted file mode 100644 index 3bcdeb7..0000000 --- a/src/components/GradesView/EditModal/__snapshots__/test.jsx.snap +++ /dev/null @@ -1,125 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditModal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = ` - - -
- - - Weve been trying to contact you regarding... - - -
- -
-
- -
-
-
- - - - - - - - -
-`; - -exports[`EditModal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = ` - - -
- - - - - -
- -
-
- -
-
-
- - - - - - - - -
-`; diff --git a/src/components/GradesView/EditModal/hooks.js b/src/components/GradesView/EditModal/hooks.js new file mode 100644 index 0000000..a5811b7 --- /dev/null +++ b/src/components/GradesView/EditModal/hooks.js @@ -0,0 +1,29 @@ +import { selectors, actions, thunkActions } from 'data/redux/hooks'; + +export const useEditModalData = () => { + const error = selectors.grades.useGradeData().gradeOverrideHistoryError; + const isOpen = selectors.app.useModalData().open; + const closeModal = actions.app.useCloseModal(); + const doneViewingAssignment = actions.grades.useDoneViewingAssignment(); + const updateGrades = thunkActions.grades.useUpdateGrades(); + + const onClose = () => { + doneViewingAssignment(); + closeModal(); + }; + + const handleAdjustedGradeClick = () => { + updateGrades(); + doneViewingAssignment(); + closeModal(); + }; + + return { + onClose, + error, + handleAdjustedGradeClick, + isOpen, + }; +}; + +export default useEditModalData; diff --git a/src/components/GradesView/EditModal/hooks.test.js b/src/components/GradesView/EditModal/hooks.test.js new file mode 100644 index 0000000..7951a16 --- /dev/null +++ b/src/components/GradesView/EditModal/hooks.test.js @@ -0,0 +1,68 @@ +import { selectors, actions, thunkActions } from 'data/redux/hooks'; + +import useEditModalData from './hooks'; + +jest.mock('data/redux/hooks', () => ({ + actions: { + app: { useCloseModal: jest.fn() }, + grades: { useDoneViewingAssignment: jest.fn() }, + }, + selectors: { + app: { useModalData: jest.fn() }, + grades: { useGradeData: jest.fn() }, + }, + thunkActions: { + grades: { useUpdateGrades: jest.fn() }, + }, +})); + +const closeModal = jest.fn(); +const doneViewingAssignment = jest.fn(); +const updateGrades = jest.fn(); +actions.app.useCloseModal.mockReturnValue(closeModal); +actions.grades.useDoneViewingAssignment.mockReturnValue(doneViewingAssignment); +thunkActions.grades.useUpdateGrades.mockReturnValue(updateGrades); + +const gradeData = { gradeOverridHistoryError: 'test-error' }; +const modalData = { open: true }; +selectors.app.useModalData.mockReturnValue(modalData); +selectors.grades.useGradeData.mockReturnValue(gradeData); + +let out; +describe('useEditModalData', () => { + beforeEach(() => { + jest.clearAllMocks(); + out = useEditModalData(); + }); + describe('behavior', () => { + it('initializes redux hooks', () => { + expect(selectors.grades.useGradeData).toHaveBeenCalled(); + expect(selectors.app.useModalData).toHaveBeenCalled(); + expect(actions.app.useCloseModal).toHaveBeenCalled(); + expect(actions.grades.useDoneViewingAssignment).toHaveBeenCalled(); + expect(thunkActions.grades.useUpdateGrades).toHaveBeenCalled(); + }); + }); + describe('output', () => { + it('forwards error from gradeData.gradeOverrideHistoryError', () => { + expect(out.error).toEqual(gradeData.gradeOverrideHistoryError); + }); + it('forwards isOpen from modalData.open', () => { + expect(out.isOpen).toEqual(modalData.open); + }); + describe('handleAdjustedGradeClick', () => { + it('updates grades, calls doneViewingAssignment and closeModal', () => { + out.handleAdjustedGradeClick(); + expect(updateGrades).toHaveBeenCalled(); + expect(doneViewingAssignment).toHaveBeenCalled(); + expect(closeModal).toHaveBeenCalled(); + }); + }); + test('onClose calls doneViewingAssignment and closeModal', () => { + out.onClose(); + expect(doneViewingAssignment).toHaveBeenCalled(); + expect(closeModal).toHaveBeenCalled(); + expect(updateGrades).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/GradesView/EditModal/index.jsx b/src/components/GradesView/EditModal/index.jsx index d093be8..519b044 100644 --- a/src/components/GradesView/EditModal/index.jsx +++ b/src/components/GradesView/EditModal/index.jsx @@ -1,7 +1,4 @@ -/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */ import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; import { Button, @@ -9,15 +6,12 @@ import { ModalDialog, ActionRow, } from '@edx/paragon'; -import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useIntl } from '@edx/frontend-platform/i18n'; -import selectors from 'data/selectors'; -import actions from 'data/actions'; -import thunkActions from 'data/thunkActions'; - -import messages from './messages'; import OverrideTable from './OverrideTable'; import ModalHeaders from './ModalHeaders'; +import useEditModalData from './hooks'; +import messages from './messages'; /** * @@ -28,87 +22,48 @@ import ModalHeaders from './ModalHeaders'; * adjusting the grade. * (also provides a close button that clears the modal state) */ -export class EditModal extends React.Component { - constructor(props) { - super(props); - this.closeAssignmentModal = this.closeAssignmentModal.bind(this); - this.handleAdjustedGradeClick = this.handleAdjustedGradeClick.bind(this); - } +export const EditModal = () => { + const { formatMessage } = useIntl(); + const { + onClose, + error, + handleAdjustedGradeClick, + isOpen, + } = useEditModalData(); - closeAssignmentModal() { - this.props.doneViewingAssignment(); - this.props.closeModal(); - } + return ( + + +
+ + + {error} + + +
{formatMessage(messages.visibility)}
+
{formatMessage(messages.saveVisibility)}
+
+
- handleAdjustedGradeClick() { - this.props.updateGrades(); - this.closeAssignmentModal(); - } - - render() { - return ( - - -
- - - {this.props.gradeOverrideHistoryError} - - -
-
-
-
- - - - - - - - -
- ); - } -} - -EditModal.defaultProps = { - gradeOverrideHistoryError: '', + + + + {formatMessage(messages.closeText)} + + + + +
+ ); }; -EditModal.propTypes = { - // redux - gradeOverrideHistoryError: PropTypes.string, - open: PropTypes.bool.isRequired, - closeModal: PropTypes.func.isRequired, - doneViewingAssignment: PropTypes.func.isRequired, - updateGrades: PropTypes.func.isRequired, - // injected - intl: intlShape.isRequired, -}; - -export const mapStateToProps = (state) => ({ - gradeOverrideHistoryError: selectors.grades.gradeOverrideHistoryError(state), - open: selectors.app.modalState.open(state), -}); - -export const mapDispatchToProps = { - closeModal: actions.app.closeModal, - doneViewingAssignment: actions.grades.doneViewingAssignment, - updateGrades: thunkActions.grades.updateGrades, -}; - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(EditModal)); +export default EditModal; diff --git a/src/components/GradesView/EditModal/index.test.jsx b/src/components/GradesView/EditModal/index.test.jsx new file mode 100644 index 0000000..c0f9f21 --- /dev/null +++ b/src/components/GradesView/EditModal/index.test.jsx @@ -0,0 +1,133 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { + ActionRow, + Alert, + ModalDialog, +} from '@edx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { formatMessage } from 'testUtils'; + +import ModalHeaders from './ModalHeaders'; +import OverrideTable from './OverrideTable'; +import useEditModalData from './hooks'; +import EditModal from '.'; +import messages from './messages'; + +jest.mock('./hooks', () => jest.fn()); +jest.mock('./ModalHeaders', () => 'ModalHeaders'); +jest.mock('./OverrideTable', () => 'OverrideTable'); + +const hookProps = { + onClose: jest.fn().mockName('hooks.onClose'), + error: 'test-error', + handleAdjustedGradeClick: jest.fn().mockName('hooks.handleAdjustedGradeClick'), + isOpen: 'test-is-open', +}; +useEditModalData.mockReturnValue(hookProps); + +let el; +describe('EditModal component', () => { + beforeEach(() => { + jest.clearAllMocks(); + el = shallow(); + }); + describe('behavior', () => { + it('initializes intl hook', () => { + expect(useIntl).toHaveBeenCalled(); + }); + it('initializes component hooks', () => { + expect(useEditModalData).toHaveBeenCalled(); + }); + }); + describe('render', () => { + test('modal props', () => { + const modalProps = el.find(ModalDialog).props(); + expect(modalProps.title).toEqual(formatMessage(messages.title)); + expect(modalProps.isOpen).toEqual(hookProps.isOpen); + expect(modalProps.onClose).toEqual(hookProps.onClose); + }); + const loadBody = () => { + const body = el.find(ModalDialog).children().at(0); + const children = body.find('div').children(); + return { body, children }; + }; + const testBody = () => { + test('type', () => { + const { body } = loadBody(); + expect(body.type()).toEqual('ModalDialog.Body'); + }); + test('headers row', () => { + const { children } = loadBody(); + expect(children.at(0)).toMatchObject(shallow()); + }); + test('table row', () => { + const { children } = loadBody(); + expect(children.at(2)).toMatchObject(shallow()); + }); + test('messages', () => { + const { children } = loadBody(); + expect( + children.at(3).contains(formatMessage(messages.visibility)), + ).toEqual(true); + expect( + children.at(4).contains(formatMessage(messages.saveVisibility)), + ).toEqual(true); + }); + }; + const testFooter = () => { + let footer; + beforeEach(() => { + footer = el.find(ModalDialog).children().at(1); + }); + test('type', () => { + expect(footer.type()).toEqual('ModalDialog.Footer'); + }); + test('contains action row', () => { + expect(footer.children().at(0).type()).toEqual('ActionRow'); + }); + test('close button', () => { + const button = footer.find(ActionRow).children().at(0); + expect(button.contains(formatMessage(messages.closeText))).toEqual(true); + expect(button.type()).toEqual('ModalDialog.CloseButton'); + }); + test('adjusted grade button', () => { + const button = footer.find(ActionRow).children().at(1); + expect(button.contains(formatMessage(messages.saveGrade))).toEqual(true); + expect(button.type()).toEqual('Button'); + expect(button.props().onClick).toEqual(hookProps.handleAdjustedGradeClick); + }); + }; + describe('without error', () => { + beforeEach(() => { + useEditModalData.mockReturnValueOnce({ ...hookProps, error: undefined }); + el = shallow(); + }); + test('snapshot', () => { + expect(el).toMatchSnapshot(); + }); + testBody(); + testFooter(); + test('alert row', () => { + const alert = loadBody().children.at(1); + expect(alert.type()).toEqual('Alert'); + expect(alert.props().show).toEqual(false); + }); + }); + describe('with error', () => { + test('snapshot', () => { + expect(el).toMatchSnapshot(); + }); + testBody(); + test('alert row', () => { + const alert = loadBody().children.at(1); + expect(alert.type()).toEqual('Alert'); + expect(alert.props().show).toEqual(true); + expect(alert.contains(hookProps.error)).toEqual(true); + }); + testFooter(); + }); + }); +}); diff --git a/src/components/GradesView/EditModal/test.jsx b/src/components/GradesView/EditModal/test.jsx deleted file mode 100644 index c0f6cbf..0000000 --- a/src/components/GradesView/EditModal/test.jsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import actions from 'data/actions'; -import selectors from 'data/selectors'; -import thunkActions from 'data/thunkActions'; - -import { - EditModal, - mapDispatchToProps, - mapStateToProps, -} - from '.'; - -jest.mock('./OverrideTable', () => 'OverrideTable'); -jest.mock('./ModalHeaders', () => 'ModalHeaders'); -jest.mock('data/actions', () => ({ - __esModule: true, - default: { - app: { closeModal: jest.fn() }, - grades: { doneViewingAssignment: jest.fn() }, - }, -})); -jest.mock('data/thunkActions', () => ({ - __esModule: true, - default: { - grades: { updateGrades: jest.fn() }, - }, -})); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - app: { - modalState: { - open: jest.fn(state => ({ isModalOpen: state })), - }, - }, - grades: { - gradeOverrideHistoryError: jest.fn(state => ({ overrideHistoryError: state })), - }, - }, -})); -describe('EditModal', () => { - let props; - beforeEach(() => { - props = { - gradeOverrideHistoryError: 'Weve been trying to contact you regarding...', - open: true, - closeModal: jest.fn(), - doneViewingAssignment: jest.fn(), - updateGrades: jest.fn(), - - intl: { formatMessage: (msg) => msg.defaultMessage }, - }; - }); - - describe('Component', () => { - describe('behavior', () => { - let el; - beforeEach(() => { - el = shallow(); - }); - describe('closeAssignmentModal', () => { - it('calls props.doneViewingAssignment and props.closeModal', () => { - el.instance().closeAssignmentModal(); - expect(props.doneViewingAssignment).toHaveBeenCalledWith(); - expect(props.closeModal).toHaveBeenCalledWith(); - }); - }); - describe('handleAdjustedGradeClick', () => { - it('calls props.updateGardes and this.closeAssignmentModal', () => { - el.instance().closeAssignmentModal = jest.fn(); - el.instance().handleAdjustedGradeClick(); - expect(props.updateGrades).toHaveBeenCalledWith(); - expect(el.instance().closeAssignmentModal).toHaveBeenCalledWith(); - }); - }); - }); - describe('snapshots', () => { - let el; - beforeEach(() => { - el = shallow(); - el.instance().closeAssignmentModal = jest.fn().mockName('this.closeAssignmentModal'); - el.instance().handleAdjustedGradeClick = jest.fn().mockName( - 'this.handleAdjustedGradeClick', - ); - }); - describe('gradeOverrideHistoryError is and empty and open is true', () => { - test('modal open and StatusAlert showing', () => { - expect(el.instance().render()).toMatchSnapshot(); - }); - }); - describe('gradeOverrideHistoryError is empty and open is false', () => { - test('modal closed and StatusAlert closed', () => { - el.setProps({ open: false, gradeOverrideHistoryError: '' }); - expect(el.instance().render()).toMatchSnapshot(); - }); - }); - }); - }); - - describe('mapStateToProps', () => { - const testState = { martha: 'why did you say that name?!' }; - let mapped; - beforeEach(() => { - mapped = mapStateToProps(testState); - }); - test('gradeOverrideHistoryError from grades.gradeOverrideHistoryError', () => { - expect( - mapped.gradeOverrideHistoryError, - ).toEqual(selectors.grades.gradeOverrideHistoryError(testState)); - }); - test('open from app.modalState.open', () => { - expect(mapped.open).toEqual(selectors.app.modalState.open(testState)); - }); - }); - describe('mapDispatchToProps', () => { - test('closeModal from actions.app.closeModal', () => { - expect(mapDispatchToProps.closeModal).toEqual(actions.app.closeModal); - }); - test('doneViewingAssignemtn from actions.grades.doneViewingAssignment', () => { - expect( - mapDispatchToProps.doneViewingAssignment, - ).toEqual(actions.grades.doneViewingAssignment); - }); - test('updateGrades from thunkActions.grades.updateGrades', () => { - expect(mapDispatchToProps.updateGrades).toEqual(thunkActions.grades.updateGrades); - }); - }); -});