refactor: EditModal component updates

This commit is contained in:
Ben Warzeski
2023-05-11 13:27:14 -04:00
parent ca64cc614a
commit 0e6f52fca9
33 changed files with 1047 additions and 1006 deletions

View File

@@ -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',
});
/**
* <ModalHeaders />
* Provides a list of HistoryHeaders for the student name, assignment,
* original grade, and current override grade.
*/
export const ModalHeaders = ({
modalState,
originalGrade,
currentGrade,
}) => (
<div>
<HistoryHeader
id="assignment"
label={<FormattedMessage {...messages.assignmentHeader} />}
value={modalState.assignmentName}
/>
<HistoryHeader
id="student"
label={<FormattedMessage {...messages.studentHeader} />}
value={modalState.updateUserName}
/>
<HistoryHeader
id="original-grade"
label={<FormattedMessage {...messages.originalGradeHeader} />}
value={originalGrade}
/>
<HistoryHeader
id="current-grade"
label={<FormattedMessage {...messages.currentGradeHeader} />}
value={currentGrade}
/>
</div>
);
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 (
<div>
<HistoryHeader
id={HistoryKeys.assignment}
label={formatMessage(messages.assignmentHeader)}
value={assignmentName}
/>
<HistoryHeader
id={HistoryKeys.student}
label={formatMessage(messages.studentHeader)}
value={updateUserName}
/>
<HistoryHeader
id={HistoryKeys.originalGrade}
label={formatMessage(messages.originalGradeHeader)}
value={originalGrade}
/>
<HistoryHeader
id={HistoryKeys.currentGrade}
label={formatMessage(messages.currentGradeHeader)}
value={currentGrade}
/>
</div>
);
};
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;

View File

@@ -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(<ModalHeaders {...props} />);
expect(el).toMatchSnapshot();
});
});
describe('gradeOverrideHistoryError is empty and open is false', () => {
test('modal closed and StatusAlert closed', () => {
el = shallow(
<ModalHeaders {...props} open={false} gradeOverrideHistoryError="" />,
);
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(<ModalHeaders />);
});
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,
});
});
});

View File

@@ -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(<AdjustedGradeInput {...props} />);
});
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);
});
});
});

View File

@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AdjustedGradeInput component render snapshot 1`] = `
<span>
<Form.Control
name="adjustedGradeValue"
onChange={[MockFunction hook.onChange]}
type="text"
value="test-value"
/>
some-hint-text
</span>
`;

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { Form } from '@edx/paragon';
import useAdjustedGradeInputData from './hooks';
/**
* <AdjustedGradeInput />
* 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 (
<span>
<Form.Control
type="text"
name="adjustedGradeValue"
value={value}
onChange={onChange}
/>
{hintText}
</span>
);
};
AdjustedGradeInput.propTypes = {};
export default AdjustedGradeInput;

View File

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

View File

@@ -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';
/**
* <ReasonInput />
* 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 (
<Form.Control
type="text"
name="reasonForChange"
value={this.props.value}
onChange={this.onChange}
ref={this.ref}
/>
);
}
}
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);

View File

@@ -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(<ReasonInput {...props} />, { 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);
});
});
});

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReasonInput component render snapshot 1`] = `
<Form.Control
data-testid="reason-input-control"
name="reasonForChange"
onChange={[MockFunction hook.onChange]}
type="text"
value="test-value"
/>
`;

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
import React from 'react';
import { Form } from '@edx/paragon';
import useReasonInputData from './hooks';
export const controlTestId = 'reason-input-control';
/**
* <ReasonInput />
* Input control for the "reason for change" field in the Edit modal.
*/
export const ReasonInput = () => {
const { ref, value, onChange } = useReasonInputData();
return (
<Form.Control
type="text"
name="reasonForChange"
data-testid={controlTestId}
{...{ value, onChange, ref }}
/>
);
};
ReasonInput.propTypes = {};
export default ReasonInput;

View File

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

View File

@@ -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(<ReasonInput />);
const control = el.getByTestId(controlTestId);
expect(control).toEqual(props.ref.current);
});
});

View File

@@ -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`] = `
<span>
<Control
name="adjustedGradeValue"
onChange={[MockFunction this.onChange]}
type="text"
value={1}
/>
/ 5
</span>
`;

View File

@@ -1,10 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReasonInput Component snapshots displays reason for change input control 1`] = `
<Control
name="reasonForChange"
onChange={[MockFunction this.onChange]}
type="text"
value="did not answer the question"
/>
`;

View File

@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`OverrideTable component render snapshot 1`] = `
<DataTable
columns="test-columns"
data={
Array [
Object {
"test": "data",
},
Object {
"andOther": "test-data",
},
Object {
"adjustedGrade": <AdjustedGradeInput />,
"date": Object {
"formatted": 2000-01-01T00:00:00.000Z,
},
"reason": <ReasonInput />,
},
]
}
itemCount={2}
/>
`;

View File

@@ -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`] = `
<DataTable
columns={
Array [
Object {
"Header": <FormattedMessage
defaultMessage="Date"
description="Edit Modal Override Table Date column header"
id="gradebook.GradesView.EditModal.Overrides.dateHeader"
/>,
"accessor": "date",
},
Object {
"Header": <FormattedMessage
defaultMessage="Grader"
description="Edit Modal Override Table Grader column header"
id="gradebook.GradesView.EditModal.Overrides.graderHeader"
/>,
"accessor": "grader",
},
Object {
"Header": <FormattedMessage
defaultMessage="Reason"
description="Edit Modal Override Table Reason column header"
id="gradebook.GradesView.EditModal.Overrides.reasonHeader"
/>,
"accessor": "reason",
},
Object {
"Header": <FormattedMessage
defaultMessage="Adjusted grade"
description="Edit Modal Override Table Adjusted grade column header"
id="gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader"
/>,
"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": <AdjustedGradeInput />,
"date": "todaaaaaay",
"reason": <ReasonInput />,
},
]
}
itemCount={2}
/>
`;

View File

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

View File

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

View File

@@ -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';
/**
* <OverrideTable />
* 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 : (
<DataTable
columns={[
{ Header: <FormattedMessage {...messages.dateHeader} />, accessor: columns.date },
{ Header: <FormattedMessage {...messages.graderHeader} />, accessor: columns.grader },
{ Header: <FormattedMessage {...messages.reasonHeader} />, accessor: columns.reason },
{
Header: <FormattedMessage {...messages.adjustedGradeHeader} />,
accessor: columns.adjustedGrade,
},
]}
columns={columns}
data={[
...gradeOverrides,
...data,
{
adjustedGrade: <AdjustedGradeInput />,
date: todaysDate,
date: formatDateForDisplay(new Date()),
reason: <ReasonInput />,
},
]}
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;

View File

@@ -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(<OverrideTable />);
});
describe('behavior', () => {
it('initializes hook data', () => {
expect(useOverrideTableData).toHaveBeenCalled();
});
});
describe('render', () => {
test('null render if hide', () => {
useOverrideTableData.mockReturnValueOnce({ ...hookProps, hide: true });
el = shallow(<OverrideTable />);
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: <AdjustedGradeInput />,
date: formattedDate,
reason: <ReasonInput />,
});
});
});
});

View File

@@ -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(<OverrideTable {...props} hide />)).toEqual({});
});
describe('basic snapshot', () => {
test('shows a row for each entry and one editable row', () => {
expect(shallow(<OverrideTable {...props} />)).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));
});
});
});
});

View File

@@ -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`] = `
<div>
<HistoryHeader
id="assignment"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Edit Modal Assignment header"
id="gradebook.GradesView.EditModal.headers.assignment"
/>
}
value="Qwerty"
label="Assignment"
value="test-assignment-name"
/>
<HistoryHeader
id="student"
label={
<FormattedMessage
defaultMessage="Student"
description="Edit Modal Student header"
id="gradebook.GradesView.EditModal.headers.student"
/>
}
value="Uiop"
label="Student"
value="test-user-name"
/>
<HistoryHeader
id="original-grade"
label={
<FormattedMessage
defaultMessage="Original Grade"
description="Edit Modal Original Grade header"
id="gradebook.GradesView.EditModal.headers.originalGrade"
/>
}
value={20}
label="Original Grade"
value="test-original-grade"
/>
<HistoryHeader
id="current-grade"
label={
<FormattedMessage
defaultMessage="Current Grade"
description="Edit Modal Current Grade header"
id="gradebook.GradesView.EditModal.headers.currentGrade"
/>
}
value={2}
/>
</div>
`;
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
<div>
<HistoryHeader
id="assignment"
label={
<FormattedMessage
defaultMessage="Assignment"
description="Edit Modal Assignment header"
id="gradebook.GradesView.EditModal.headers.assignment"
/>
}
value="Qwerty"
/>
<HistoryHeader
id="student"
label={
<FormattedMessage
defaultMessage="Student"
description="Edit Modal Student header"
id="gradebook.GradesView.EditModal.headers.student"
/>
}
value="Uiop"
/>
<HistoryHeader
id="original-grade"
label={
<FormattedMessage
defaultMessage="Original Grade"
description="Edit Modal Original Grade header"
id="gradebook.GradesView.EditModal.headers.originalGrade"
/>
}
value={20}
/>
<HistoryHeader
id="current-grade"
label={
<FormattedMessage
defaultMessage="Current Grade"
description="Edit Modal Current Grade header"
id="gradebook.GradesView.EditModal.headers.currentGrade"
/>
}
value={2}
label="Current Grade"
value="test-current-grade"
/>
</div>
`;

View File

@@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditModal component render with error snapshot 1`] = `
<ModalDialog
hasCloseButton={true}
isFullscreenOnMobile={true}
isOpen="test-is-open"
onClose={[MockFunction hooks.onClose]}
size="xl"
title="Edit Grades"
>
<ModalDialog.Body>
<div>
<ModalHeaders />
<Alert
dismissible={false}
show={true}
variant="danger"
>
test-error
</Alert>
<OverrideTable />
<div>
Showing most recent actions (max 5). To see more, please contact support
</div>
<div>
Note: Once you save, your changes will be visible to students.
</div>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton
variant="tertiary"
>
Cancel
</ModalDialog.CloseButton>
<Button
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
variant="primary"
>
Save Grades
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
`;
exports[`EditModal component render without error snapshot 1`] = `
<ModalDialog
hasCloseButton={true}
isFullscreenOnMobile={true}
isOpen="test-is-open"
onClose={[MockFunction hooks.onClose]}
size="xl"
title="Edit Grades"
>
<ModalDialog.Body>
<div>
<ModalHeaders />
<Alert
dismissible={false}
show={false}
variant="danger"
/>
<OverrideTable />
<div>
Showing most recent actions (max 5). To see more, please contact support
</div>
<div>
Note: Once you save, your changes will be visible to students.
</div>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton
variant="tertiary"
>
Cancel
</ModalDialog.CloseButton>
<Button
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
variant="primary"
>
Save Grades
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
`;

View File

@@ -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`] = `
<ModalDialog
hasCloseButton={true}
isFullscreenOnMobile={true}
isOpen={true}
onClose={[MockFunction this.closeAssignmentModal]}
size="xl"
title="Edit Grades"
>
<ModalDialog.Body>
<div>
<ModalHeaders />
<Alert
dismissible={false}
show={true}
variant="danger"
>
Weve been trying to contact you regarding...
</Alert>
<OverrideTable />
<div>
<FormattedMessage
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
description="Edit Modal visibility hint message"
id="gradebook.GradesView.EditModal.contactSupport"
/>
</div>
<div>
<FormattedMessage
defaultMessage="Note: Once you save, your changes will be visible to students."
description="Edit Modal saved changes effect hint"
id="gradebook.GradesView.EditModal.saveVisibility"
/>
</div>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton
variant="tertiary"
>
<FormattedMessage
defaultMessage="Cancel"
description="Edit Modal close button text"
id="gradebook.GradesView.EditModal.closeText"
/>
</ModalDialog.CloseButton>
<Button
onClick={[MockFunction this.handleAdjustedGradeClick]}
variant="primary"
>
<FormattedMessage
defaultMessage="Save Grades"
description="Edit Modal Save button label"
id="gradebook.GradesView.EditModal.saveGrade"
/>
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
`;
exports[`EditModal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
<ModalDialog
hasCloseButton={true}
isFullscreenOnMobile={true}
isOpen={false}
onClose={[MockFunction this.closeAssignmentModal]}
size="xl"
title="Edit Grades"
>
<ModalDialog.Body>
<div>
<ModalHeaders />
<Alert
dismissible={false}
show={false}
variant="danger"
>
</Alert>
<OverrideTable />
<div>
<FormattedMessage
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
description="Edit Modal visibility hint message"
id="gradebook.GradesView.EditModal.contactSupport"
/>
</div>
<div>
<FormattedMessage
defaultMessage="Note: Once you save, your changes will be visible to students."
description="Edit Modal saved changes effect hint"
id="gradebook.GradesView.EditModal.saveVisibility"
/>
</div>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton
variant="tertiary"
>
<FormattedMessage
defaultMessage="Cancel"
description="Edit Modal close button text"
id="gradebook.GradesView.EditModal.closeText"
/>
</ModalDialog.CloseButton>
<Button
onClick={[MockFunction this.handleAdjustedGradeClick]}
variant="primary"
>
<FormattedMessage
defaultMessage="Save Grades"
description="Edit Modal Save button label"
id="gradebook.GradesView.EditModal.saveGrade"
/>
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
`;

View File

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

View File

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

View File

@@ -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';
/**
* <EditModal />
@@ -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 (
<ModalDialog
title={formatMessage(messages.title)}
isOpen={isOpen}
onClose={onClose}
size="xl"
hasCloseButton
isFullscreenOnMobile
>
<ModalDialog.Body>
<div>
<ModalHeaders />
<Alert variant="danger" show={!!error} dismissible={false}>
{error}
</Alert>
<OverrideTable />
<div>{formatMessage(messages.visibility)}</div>
<div>{formatMessage(messages.saveVisibility)}</div>
</div>
</ModalDialog.Body>
handleAdjustedGradeClick() {
this.props.updateGrades();
this.closeAssignmentModal();
}
render() {
return (
<ModalDialog
title={this.props.intl.formatMessage(messages.title)}
isOpen={this.props.open}
onClose={this.closeAssignmentModal}
size="xl"
hasCloseButton
isFullscreenOnMobile
>
<ModalDialog.Body>
<div>
<ModalHeaders />
<Alert
variant="danger"
show={!!this.props.gradeOverrideHistoryError}
dismissible={false}
>
{this.props.gradeOverrideHistoryError}
</Alert>
<OverrideTable />
<div><FormattedMessage {...messages.visibility} /></div>
<div><FormattedMessage {...messages.saveVisibility} /></div>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
<FormattedMessage {...messages.closeText} />
</ModalDialog.CloseButton>
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
<FormattedMessage {...messages.saveGrade} />
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
}
}
EditModal.defaultProps = {
gradeOverrideHistoryError: '',
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{formatMessage(messages.closeText)}
</ModalDialog.CloseButton>
<Button variant="primary" onClick={handleAdjustedGradeClick}>
{formatMessage(messages.saveGrade)}
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};
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;

View File

@@ -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(<EditModal />);
});
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(<ModalHeaders />));
});
test('table row', () => {
const { children } = loadBody();
expect(children.at(2)).toMatchObject(shallow(<OverrideTable />));
});
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(<EditModal />);
});
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();
});
});
});

View File

@@ -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(<EditModal {...props} />);
});
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(<EditModal {...props} />);
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);
});
});
});