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