feat: Submit states (#37)
* feat: enable teams support * feat: submit grade pending behavior * feat: submit error behavior * fix: Update src/data/services/lms/fakeData/testUtils.js Co-authored-by: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com> Co-authored-by: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { Cancel, Highlight } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { gradingStatuses as statuses } from 'data/services/lms/constants';
|
||||
|
||||
import StopGradingConfirmModal from './StopGradingConfirmModal';
|
||||
@@ -93,6 +94,7 @@ export class StartGradingButton extends React.Component {
|
||||
variant="primary"
|
||||
iconAfter={args.iconAfter}
|
||||
onClick={this.handleClick}
|
||||
disabled={this.props.isPending}
|
||||
>
|
||||
<FormattedMessage {...args.label} />
|
||||
</Button>
|
||||
@@ -117,16 +119,18 @@ StartGradingButton.propTypes = {
|
||||
gradingStatus: PropTypes.string.isRequired,
|
||||
startGrading: PropTypes.func.isRequired,
|
||||
stopGrading: PropTypes.func.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
isPending: selectors.requests.isPending(state, { requestKey: RequestKeys.submitGrade }),
|
||||
gradeStatus: selectors.grading.selected.gradeStatus(state),
|
||||
gradingStatus: selectors.grading.selected.gradingStatus(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
startGrading: thunkActions.grading.startGrading,
|
||||
stopGrading: thunkActions.grading.stopGrading,
|
||||
stopGrading: thunkActions.grading.cancelGrading,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StartGradingButton);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { gradingStatuses as statuses } from 'data/services/lms/constants';
|
||||
|
||||
import {
|
||||
@@ -10,10 +11,21 @@ import {
|
||||
mapDispatchToProps,
|
||||
} from './StartGradingButton';
|
||||
|
||||
jest.mock('data/redux/grading/selectors', () => ({
|
||||
selected: {
|
||||
gradeStatus: (state) => ({ gradeStatus: state }),
|
||||
gradingStatus: (state) => ({ gradingStatus: state }),
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
grading: {
|
||||
selected: {
|
||||
gradeStatus: (state) => ({ gradeStatus: state }),
|
||||
gradingStatus: (state) => ({ gradingStatus: state }),
|
||||
},
|
||||
},
|
||||
requests: { isPending: (...args) => ({ isPending: args }) },
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
startGrading: jest.fn(),
|
||||
stopGrading: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('./OverrideGradeConfirmModal', () => 'OverrideGradeConfirmModal');
|
||||
@@ -23,7 +35,9 @@ let el;
|
||||
|
||||
describe('StartGradingButton component', () => {
|
||||
describe('component', () => {
|
||||
const props = {};
|
||||
const props = {
|
||||
isPending: false,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.startGrading = jest.fn().mockName('this.props.startGrading');
|
||||
props.stopGrading = jest.fn().mockName('this.props.stopGrading');
|
||||
@@ -55,6 +69,11 @@ describe('StartGradingButton component', () => {
|
||||
test('snapshot: ungraded (startGrading callback)', () => {
|
||||
expect(mockedEl(statuses.ungraded).instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: pending (disabled)', () => {
|
||||
el = mockedEl(statuses.ungraded);
|
||||
el.setProps({ isPending: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: graded, confirmOverride (startGrading callback)', () => {
|
||||
el = mockedEl(statuses.graded);
|
||||
el.setState({ showConfirmOverrideGrade: true });
|
||||
@@ -73,6 +92,14 @@ describe('StartGradingButton component', () => {
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('isPending loads from requests.isPending(submitGrade)', () => {
|
||||
expect(mapped.isPending).toEqual(
|
||||
selectors.requests.isPending(
|
||||
testState,
|
||||
{ requestKey: RequestKeys.submitGrade },
|
||||
),
|
||||
);
|
||||
});
|
||||
test('gradeStatus loads from grading.selected.gradeStatus', () => {
|
||||
expect(mapped.gradeStatus).toEqual(selectors.grading.selected.gradeStatus(testState));
|
||||
});
|
||||
@@ -84,8 +111,8 @@ describe('StartGradingButton component', () => {
|
||||
it('loads startGrading from thunkActions.grading.stargGrading', () => {
|
||||
expect(mapDispatchToProps.startGrading).toEqual(thunkActions.grading.startGrading);
|
||||
});
|
||||
it('loads stopGrading from thunkActions.grading.stopGrading', () => {
|
||||
expect(mapDispatchToProps.stopGrading).toEqual(thunkActions.grading.stopGrading);
|
||||
it('loads stopGrading from thunkActions.grading.cancelGrading', () => {
|
||||
expect(mapDispatchToProps.stopGrading).toEqual(thunkActions.grading.cancelGrading);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
exports[`StartGradingButton component component snapshotes snapshot: graded, confirmOverride (startGrading callback) 1`] = `
|
||||
<React.Fragment>
|
||||
<Button
|
||||
disabled={false}
|
||||
iconAfter={[MockFunction icons.Highlight]}
|
||||
onClick={[MockFunction this.handleClick]}
|
||||
variant="primary"
|
||||
@@ -30,12 +31,13 @@ exports[`StartGradingButton component component snapshotes snapshot: graded, con
|
||||
exports[`StartGradingButton component component snapshotes snapshot: inProgress, isOverride, confirmStop (stopGrading callback) 1`] = `
|
||||
<React.Fragment>
|
||||
<Button
|
||||
disabled={false}
|
||||
iconAfter={[MockFunction icons.Cancel]}
|
||||
onClick={[MockFunction this.handleClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Stop grading"
|
||||
defaultMessage="Stop grading this response"
|
||||
description="Review pane button text to stop grading"
|
||||
id="ora-grading.ReviewActions.StartGradingButton.stopGrading"
|
||||
/>
|
||||
@@ -56,9 +58,38 @@ exports[`StartGradingButton component component snapshotes snapshot: inProgress,
|
||||
|
||||
exports[`StartGradingButton component component snapshotes snapshot: locked (null) 1`] = `null`;
|
||||
|
||||
exports[`StartGradingButton component component snapshotes snapshot: ungraded (startGrading callback) 1`] = `
|
||||
exports[`StartGradingButton component component snapshotes snapshot: pending (disabled) 1`] = `
|
||||
<React.Fragment>
|
||||
<Button
|
||||
disabled={true}
|
||||
iconAfter={[MockFunction icons.Highlight]}
|
||||
onClick={[MockFunction this.handleClick]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Start grading"
|
||||
description="Review pane button text to start grading"
|
||||
id="ora-grading.ReviewActions.StartGradingButton.startGrading"
|
||||
/>
|
||||
</Button>
|
||||
<OverrideGradeConfirmModal
|
||||
isOpen={false}
|
||||
onCancel={[MockFunction this.hideConfirmOverrideGrade]}
|
||||
onConfirm={[MockFunction this.confirmOverrideGrade]}
|
||||
/>
|
||||
<StopGradingConfirmModal
|
||||
isOpen={false}
|
||||
isOverride={false}
|
||||
onCancel={[MockFunction this.hideConfirmStopGrading]}
|
||||
onConfirm={[MockFunction this.confirmStopGrading]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`StartGradingButton component component snapshotes snapshot: ungraded (startGrading callback) 1`] = `
|
||||
<React.Fragment>
|
||||
<Button
|
||||
disabled={false}
|
||||
iconAfter={[MockFunction icons.Highlight]}
|
||||
onClick={[MockFunction this.handleClick]}
|
||||
variant="primary"
|
||||
|
||||
@@ -28,7 +28,7 @@ const messages = defineMessages({
|
||||
},
|
||||
stopGrading: {
|
||||
id: 'ora-grading.ReviewActions.StartGradingButton.stopGrading',
|
||||
defaultMessage: 'Stop grading',
|
||||
defaultMessage: 'Stop grading this response',
|
||||
description: 'Review pane button text to stop grading',
|
||||
},
|
||||
confirmStopOverrideTitle: {
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ReviewActions = ({
|
||||
<StatusBadge className="review-actions-status mr-3" status={gradingStatus} />
|
||||
)}
|
||||
<span className="small">
|
||||
{pointsEarned && (
|
||||
{pointsPossible && (
|
||||
<FormattedMessage
|
||||
{...messages.pointsDisplay}
|
||||
values={{ pointsEarned, pointsPossible }}
|
||||
|
||||
@@ -5,29 +5,40 @@ import { connect } from 'react-redux';
|
||||
import { Col, Row } from '@edx/paragon';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import ResponseDisplay from 'containers/ResponseDisplay';
|
||||
import Rubric from 'containers/Rubric';
|
||||
import ReviewErrors from './ReviewErrors';
|
||||
|
||||
/**
|
||||
* <ReviewContent />
|
||||
*/
|
||||
export const ReviewContent = ({ showRubric }) => (
|
||||
export const ReviewContent = ({ isFailed, isLoaded, showRubric }) => (isLoaded || isFailed) && (
|
||||
<div className="content-block">
|
||||
<Row className="flex-nowrap">
|
||||
<Col><ResponseDisplay /></Col>
|
||||
{ showRubric && <Rubric /> }
|
||||
</Row>
|
||||
<ReviewErrors />
|
||||
{ isLoaded && (
|
||||
<Row className="flex-nowrap">
|
||||
<Col><ResponseDisplay /></Col>
|
||||
{ showRubric && <Rubric /> }
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
ReviewContent.defaultProps = {
|
||||
isFailed: false,
|
||||
isLoaded: false,
|
||||
showRubric: false,
|
||||
};
|
||||
ReviewContent.propTypes = {
|
||||
isFailed: PropTypes.bool,
|
||||
isLoaded: PropTypes.bool,
|
||||
showRubric: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
isFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchSubmission }),
|
||||
isLoaded: selectors.requests.isCompleted(state, { requestKey: RequestKeys.fetchSubmission }),
|
||||
showRubric: selectors.app.showRubric(state),
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import {
|
||||
ReviewContent,
|
||||
mapStateToProps,
|
||||
@@ -12,21 +13,30 @@ jest.mock('data/redux', () => ({
|
||||
app: {
|
||||
showRubric: (...args) => ({ showRubric: args }),
|
||||
},
|
||||
requests: {
|
||||
isCompleted: (...args) => ({ isCompleted: args }),
|
||||
isFailed: (...args) => ({ isFailed: args }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('containers/ResponseDisplay', () => 'ResponseDisplay');
|
||||
jest.mock('containers/Rubric', () => 'Rubric');
|
||||
|
||||
jest.useFakeTimers('modern');
|
||||
jest.mock('./ReviewErrors', () => 'ReviewErrors');
|
||||
|
||||
describe('ReviewContent component', () => {
|
||||
describe('component', () => {
|
||||
describe('render tests', () => {
|
||||
test('snapshot (show rubric)', () => {
|
||||
expect(shallow(<ReviewContent />)).toMatchSnapshot();
|
||||
test('snapshot: not loaded, no error', () => {
|
||||
expect(shallow(<ReviewContent />).isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot (hide rubric)', () => {
|
||||
expect(shallow(<ReviewContent showRubric />)).toMatchSnapshot();
|
||||
test('snapshot: show rubric', () => {
|
||||
expect(shallow(<ReviewContent isLoaded />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: hide rubric', () => {
|
||||
expect(shallow(<ReviewContent isLoaded showRubric />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: failed, showRubric (errors only)', () => {
|
||||
expect(shallow(<ReviewContent showRubric isFailed />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -36,8 +46,15 @@ describe('ReviewContent component', () => {
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
const requestKey = RequestKeys.fetchSubmission;
|
||||
test('showRubric loads from app.showRubric', () => {
|
||||
expect(mapped.showRubric).toEqual(selectors.app.showRubric(testState));
|
||||
});
|
||||
test('isFailed loads from requests.isFailed(fetchSubmission)', () => {
|
||||
expect(mapped.isFailed).toEqual(selectors.requests.isFailed(testState, { requestKey }));
|
||||
});
|
||||
test('isLoadeed loads from requests.isCompleted(fetchSubmission)', () => {
|
||||
expect(mapped.isLoaded).toEqual(selectors.requests.isCompleted(testState, { requestKey }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { thunkActions } from 'data/redux';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <ReviewError />
|
||||
*/
|
||||
export const ReviewError = ({ reload }) => (
|
||||
<Alert
|
||||
variant="danger"
|
||||
icon={Info}
|
||||
actions={[
|
||||
<Button onClick={reload}>
|
||||
<FormattedMessage {...messages.reloadSubmission} />
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage {...messages.loadErrorHeading} />
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<FormattedMessage {...messages.loadErrorMessage} />
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
ReviewError.defaultProps = {
|
||||
};
|
||||
ReviewError.propTypes = {
|
||||
// redux
|
||||
reload: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = () => ({
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
reload: thunkActions.grading.loadSubmission,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ReviewError);
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { thunkActions } from 'data/redux';
|
||||
|
||||
import {
|
||||
ReviewError,
|
||||
mapDispatchToProps,
|
||||
} from './ReviewError';
|
||||
|
||||
let el;
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
describe('ReviewError component', () => {
|
||||
const props = {};
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
props.reload = jest.fn();
|
||||
});
|
||||
describe('render tests', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<ReviewError {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads reload from thunkActions.grading.reloadSubmission', () => {
|
||||
expect(mapDispatchToProps.reload).toEqual(thunkActions.grading.loadSubmission);
|
||||
});
|
||||
});
|
||||
});
|
||||
50
src/containers/ReviewModal/ReviewErrors/FetchErrors.jsx
Normal file
50
src/containers/ReviewModal/ReviewErrors/FetchErrors.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
import ReviewError from './ReviewError';
|
||||
|
||||
/**
|
||||
* <FetchErrors />
|
||||
*/
|
||||
export const FetchErrors = ({
|
||||
isFailed,
|
||||
reload,
|
||||
}) => isFailed && (
|
||||
<ReviewError
|
||||
key="loadFailed"
|
||||
actions={{
|
||||
confirm: {
|
||||
onClick: reload,
|
||||
message: messages.reloadSubmission,
|
||||
},
|
||||
}}
|
||||
headingMessage={messages.loadErrorHeading}
|
||||
>
|
||||
<FormattedMessage {...messages.loadErrorMessage} />
|
||||
</ReviewError>
|
||||
);
|
||||
FetchErrors.defaultProps = {
|
||||
};
|
||||
FetchErrors.propTypes = {
|
||||
// redux
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
reload: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
isFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchSubmission }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
reload: thunkActions.grading.loadSubmission,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FetchErrors);
|
||||
61
src/containers/ReviewModal/ReviewErrors/FetchErrors.test.jsx
Normal file
61
src/containers/ReviewModal/ReviewErrors/FetchErrors.test.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import {
|
||||
FetchErrors,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './FetchErrors';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isFailed: (...args) => ({ isFailed: args }),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
loadSubmission: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./ReviewError', () => 'ReviewError');
|
||||
|
||||
const requestKey = RequestKeys.fetchSubmission;
|
||||
|
||||
describe('FetchErrors component', () => {
|
||||
const props = {
|
||||
isFailed: false,
|
||||
};
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
props.reload = jest.fn();
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('snapshot: no failure', () => {
|
||||
expect(<FetchErrors {...props} />).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: with failure', () => {
|
||||
expect(<FetchErrors {...props} isFailed={false} />).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('isFailed loads from requests.isFailed(fetchSubmission)', () => {
|
||||
expect(mapped.isFailed).toEqual(selectors.requests.isFailed(testState, { requestKey }));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads reload from thunkActions.grading.loadSubmission', () => {
|
||||
expect(mapDispatchToProps.reload).toEqual(thunkActions.grading.loadSubmission);
|
||||
});
|
||||
});
|
||||
});
|
||||
62
src/containers/ReviewModal/ReviewErrors/ReviewError.jsx
Normal file
62
src/containers/ReviewModal/ReviewErrors/ReviewError.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert, Button } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messageShape = PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
defaultMessage: PropTypes.string,
|
||||
});
|
||||
|
||||
const ReviewError = ({
|
||||
actions: {
|
||||
cancel,
|
||||
confirm,
|
||||
},
|
||||
headingMessage,
|
||||
children,
|
||||
}) => {
|
||||
const actions = [];
|
||||
if (cancel) {
|
||||
actions.push((
|
||||
<Button key="cancel" onClick={cancel.onClick} variant="outline-primary">
|
||||
<FormattedMessage {...cancel.message} />
|
||||
</Button>
|
||||
));
|
||||
}
|
||||
if (confirm) {
|
||||
actions.push((
|
||||
<Button key="confirm" onClick={confirm.onClick}>
|
||||
<FormattedMessage {...confirm.message} />
|
||||
</Button>
|
||||
));
|
||||
}
|
||||
return (
|
||||
<Alert
|
||||
variant="danger"
|
||||
icon={Info}
|
||||
actions={actions}
|
||||
>
|
||||
<Alert.Heading><FormattedMessage {...headingMessage} /></Alert.Heading>
|
||||
<p>{children}</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
ReviewError.propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
cancel: PropTypes.shape({
|
||||
onClick: PropTypes.func,
|
||||
message: messageShape,
|
||||
}),
|
||||
confirm: PropTypes.shape({
|
||||
onClick: PropTypes.func,
|
||||
message: messageShape,
|
||||
}),
|
||||
}).isRequired,
|
||||
headingMessage: messageShape.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default ReviewError;
|
||||
92
src/containers/ReviewModal/ReviewErrors/SubmitErrors.jsx
Normal file
92
src/containers/ReviewModal/ReviewErrors/SubmitErrors.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { actions, selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys, ErrorStatuses } from 'data/constants/requests';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
import ReviewError from './ReviewError';
|
||||
|
||||
/**
|
||||
* <SubmitErrors />
|
||||
*/
|
||||
export class SubmitErrors extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.dismissError = this.dismissError.bind(this);
|
||||
}
|
||||
|
||||
get gradeNotSubmitted() {
|
||||
return {
|
||||
confirm: { onClick: this.props.resubmit, message: messages.resubmitGrade },
|
||||
headingMessage: messages.gradeNotSubmittedHeading,
|
||||
contentMessage: messages.gradeNotSubmittedContent,
|
||||
};
|
||||
}
|
||||
|
||||
get errorSubmittingGrade() {
|
||||
return {
|
||||
headingMessage: messages.errorSubmittingGradeHeading,
|
||||
contentMessage: messages.errorSubmittingGradeContent,
|
||||
};
|
||||
}
|
||||
|
||||
get errorProps() {
|
||||
if (this.props.errorStatus === ErrorStatuses.badRequest) {
|
||||
return this.gradeNotSubmitted;
|
||||
}
|
||||
if (this.props.errorStatus === ErrorStatuses.conflict) {
|
||||
return this.errorSubmittingGrade;
|
||||
}
|
||||
// TODO: Network-Log an error here for unhandled error type
|
||||
return this.gradeNotSubmitted;
|
||||
}
|
||||
|
||||
dismissError() {
|
||||
this.props.clearRequest({ requestKey: RequestKeys.submitGrade });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.errorStatus) {
|
||||
return null;
|
||||
}
|
||||
const props = this.errorProps;
|
||||
|
||||
return (
|
||||
<ReviewError
|
||||
actions={{
|
||||
cancel: { onClick: this.dismissError, message: messages.dismiss },
|
||||
confirm: props.confirm,
|
||||
}}
|
||||
headingMessage={props.headingMessage}
|
||||
>
|
||||
<FormattedMessage {...props.contentMessage} />
|
||||
</ReviewError>
|
||||
);
|
||||
}
|
||||
}
|
||||
SubmitErrors.defaultProps = {
|
||||
errorStatus: undefined,
|
||||
};
|
||||
SubmitErrors.propTypes = {
|
||||
// redux
|
||||
clearRequest: PropTypes.func.isRequired,
|
||||
errorStatus: PropTypes.number,
|
||||
resubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const requestKey = RequestKeys.submitGrade;
|
||||
export const mapStateToProps = (state) => ({
|
||||
errorStatus: selectors.requests.errorStatus(state, { requestKey }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
clearRequest: actions.requests.clearRequest,
|
||||
resubmit: thunkActions.grading.submitGrade,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SubmitErrors);
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { actions, selectors, thunkActions } from 'data/redux';
|
||||
import { ErrorStatuses, RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import {
|
||||
SubmitErrors,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './SubmitErrors';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
actions: {
|
||||
requests: {
|
||||
clearRequest: jest.fn().mockName('actions.requests.clearRequest'),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
requests: {
|
||||
errorStatus: (...args) => ({ errorStatus: args }),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
submitGrade: jest.fn().mockName('thunkActions.grading.submitGrade'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let el;
|
||||
jest.mock('./ReviewError', () => 'ReviewError');
|
||||
|
||||
const requestKey = RequestKeys.submitGrade;
|
||||
|
||||
describe('SubmitErrors component', () => {
|
||||
const props = {};
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
props.resubmit = jest.fn();
|
||||
props.clearRequest = jest.fn();
|
||||
el = shallow(<SubmitErrors {...props} />);
|
||||
el.instance().dismissError = jest.fn().mockName('this.dismissError');
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('snapshot: no failure', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: with network failure', () => {
|
||||
el.setProps({ errorStatus: ErrorStatuses.badRequest });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: with conflict failure', () => {
|
||||
el.setProps({ errorStatus: ErrorStatuses.conflict });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('errorStatus loads from requests.errorStatus(fetchSubmission)', () => {
|
||||
expect(mapped.errorStatus).toEqual(
|
||||
selectors.requests.errorStatus(testState, { requestKey }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads clearRequest from actions.requests.clearRequest', () => {
|
||||
expect(mapDispatchToProps.clearRequest).toEqual(actions.requests.clearRequest);
|
||||
});
|
||||
it('loads resubmit from thunkActions.grading.submitGrade', () => {
|
||||
expect(mapDispatchToProps.resubmit).toEqual(thunkActions.grading.submitGrade);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FetchErrors component component snapshots snapshot: no failure 1`] = `
|
||||
<FetchErrors
|
||||
isFailed={false}
|
||||
reload={[MockFunction]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`FetchErrors component component snapshots snapshot: with failure 1`] = `
|
||||
<FetchErrors
|
||||
isFailed={false}
|
||||
reload={[MockFunction]}
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,72 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SubmitErrors component component snapshots snapshot: no failure 1`] = `null`;
|
||||
|
||||
exports[`SubmitErrors component component snapshots snapshot: with conflict failure 1`] = `
|
||||
<ReviewError
|
||||
actions={
|
||||
Object {
|
||||
"cancel": Object {
|
||||
"message": Object {
|
||||
"defaultMessage": "Dismiss",
|
||||
"description": "Dismiss error action button text",
|
||||
"id": "ora-grading.ReviewModal.dismiss",
|
||||
},
|
||||
"onClick": [MockFunction this.dismissError],
|
||||
},
|
||||
"confirm": undefined,
|
||||
}
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "Error submitting grade",
|
||||
"description": "Error Submitting Grade heading text",
|
||||
"id": "ora-grading.ReviewModal.errorSubmittingGrade.Heading",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="It looks like someone else got here first! Your grade submission has been rejected"
|
||||
description="Error Submitting Grade content"
|
||||
id="ora-grading.ReviewModal.errorSubmittingGrade.Content"
|
||||
/>
|
||||
</ReviewError>
|
||||
`;
|
||||
|
||||
exports[`SubmitErrors component component snapshots snapshot: with network failure 1`] = `
|
||||
<ReviewError
|
||||
actions={
|
||||
Object {
|
||||
"cancel": Object {
|
||||
"message": Object {
|
||||
"defaultMessage": "Dismiss",
|
||||
"description": "Dismiss error action button text",
|
||||
"id": "ora-grading.ReviewModal.dismiss",
|
||||
},
|
||||
"onClick": [MockFunction this.dismissError],
|
||||
},
|
||||
"confirm": Object {
|
||||
"message": Object {
|
||||
"defaultMessage": "Resubmit grate",
|
||||
"description": "Resubmit grade button after network failure",
|
||||
"id": "ora-grading.ReviewModal.resubmitGrade",
|
||||
},
|
||||
"onClick": [MockFunction],
|
||||
},
|
||||
}
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "Grade not submitted",
|
||||
"description": "Grade submission network error heading",
|
||||
"id": "ora-grading.ReviewModal.gradeNotSubmitted.heading",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="We're sorry, something went wrong when we tried to submit this grade. Please try again."
|
||||
description="Grade submission network error message"
|
||||
id="ora-grading.ReviewModal.gradeNotSubmitted.heading"
|
||||
/>
|
||||
</ReviewError>
|
||||
`;
|
||||
@@ -0,0 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReviewErrors component component snapshot: no failure 1`] = `
|
||||
<Fragment>
|
||||
<FetchErrors />
|
||||
<SubmitErrors />
|
||||
</Fragment>
|
||||
`;
|
||||
20
src/containers/ReviewModal/ReviewErrors/index.jsx
Normal file
20
src/containers/ReviewModal/ReviewErrors/index.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
import FetchErrors from './FetchErrors';
|
||||
import SubmitErrors from './SubmitErrors';
|
||||
|
||||
/**
|
||||
* <ReviewErrors />
|
||||
*/
|
||||
export const ReviewErrors = () => (
|
||||
<>
|
||||
<FetchErrors />
|
||||
<SubmitErrors />
|
||||
</>
|
||||
);
|
||||
ReviewErrors.defaultProps = {
|
||||
};
|
||||
ReviewErrors.propTypes = {
|
||||
};
|
||||
|
||||
export default ReviewErrors;
|
||||
15
src/containers/ReviewModal/ReviewErrors/index.test.jsx
Normal file
15
src/containers/ReviewModal/ReviewErrors/index.test.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ReviewErrors } from '.';
|
||||
|
||||
jest.mock('./FetchErrors', () => 'FetchErrors');
|
||||
jest.mock('./SubmitErrors', () => 'SubmitErrors');
|
||||
|
||||
describe('ReviewErrors component', () => {
|
||||
describe('component', () => {
|
||||
test('snapshot: no failure', () => {
|
||||
expect(shallow(<ReviewErrors />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
53
src/containers/ReviewModal/ReviewErrors/messages.js
Normal file
53
src/containers/ReviewModal/ReviewErrors/messages.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
const messages = defineMessages({
|
||||
loadErrorHeading: {
|
||||
id: 'ora-grading.ReviewModal.loadErrorHeading',
|
||||
defaultMessage: 'Error loading submissions',
|
||||
description: 'Submission response load failure alert header',
|
||||
},
|
||||
loadErrorMessage: {
|
||||
id: 'ora-grading.ReviewModal.loadErrorMessage1',
|
||||
defaultMessage: 'An error occurred while loading this submission. Try reloading this submission.',
|
||||
description: 'Submission response load failure alert message',
|
||||
},
|
||||
reloadSubmission: {
|
||||
id: 'ora-grading.ReviewModal.reloadSubmission',
|
||||
defaultMessage: 'Reload submission',
|
||||
description: 'Reload button text in case of network failure',
|
||||
},
|
||||
gradeNotSubmittedHeading: {
|
||||
id: 'ora-grading.ReviewModal.gradeNotSubmitted.heading',
|
||||
defaultMessage: 'Grade not submitted',
|
||||
description: 'Grade submission network error heading',
|
||||
},
|
||||
gradeNotSubmittedContent: {
|
||||
id: 'ora-grading.ReviewModal.gradeNotSubmitted.heading',
|
||||
defaultMessage: "We're sorry, something went wrong when we tried to submit this grade. Please try again.",
|
||||
description: 'Grade submission network error message',
|
||||
},
|
||||
resubmitGrade: {
|
||||
id: 'ora-grading.ReviewModal.resubmitGrade',
|
||||
defaultMessage: 'Resubmit grate',
|
||||
description: 'Resubmit grade button after network failure',
|
||||
},
|
||||
dismiss: {
|
||||
id: 'ora-grading.ReviewModal.dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
description: 'Dismiss error action button text',
|
||||
},
|
||||
errorSubmittingGradeHeading: {
|
||||
id: 'ora-grading.ReviewModal.errorSubmittingGrade.Heading',
|
||||
defaultMessage: 'Error submitting grade',
|
||||
description: 'Error Submitting Grade heading text',
|
||||
},
|
||||
errorSubmittingGradeContent: {
|
||||
id: 'ora-grading.ReviewModal.errorSubmittingGrade.Content',
|
||||
defaultMessage: 'It looks like someone else got here first! Your grade submission has been rejected',
|
||||
description: 'Error Submitting Grade content',
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default StrictDict(messages);
|
||||
@@ -1,9 +1,18 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReviewContent component component render tests snapshot (hide rubric) 1`] = `
|
||||
exports[`ReviewContent component component render tests snapshot: failed, showRubric (errors only) 1`] = `
|
||||
<div
|
||||
className="content-block"
|
||||
>
|
||||
<ReviewErrors />
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ReviewContent component component render tests snapshot: hide rubric 1`] = `
|
||||
<div
|
||||
className="content-block"
|
||||
>
|
||||
<ReviewErrors />
|
||||
<Row
|
||||
className="flex-nowrap"
|
||||
>
|
||||
@@ -15,10 +24,11 @@ exports[`ReviewContent component component render tests snapshot (hide rubric) 1
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ReviewContent component component render tests snapshot (show rubric) 1`] = `
|
||||
exports[`ReviewContent component component render tests snapshot: show rubric 1`] = `
|
||||
<div
|
||||
className="content-block"
|
||||
>
|
||||
<ReviewErrors />
|
||||
<Row
|
||||
className="flex-nowrap"
|
||||
>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReviewError component component render tests snapshot 1`] = `
|
||||
<Alert
|
||||
actions={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Reload submission"
|
||||
description="Reload button text in case of network failure"
|
||||
id="ora-grading.ReviewModal.reloadSubmission"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Error loading submissions"
|
||||
description="Submission response load failure alert header"
|
||||
id="ora-grading.ReviewModal.loadErrorHeading"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="An error occurred while loading this submission. Try reloading this submission."
|
||||
description="Submission response load failure alert message"
|
||||
id="ora-grading.ReviewModal.loadErrorMessage1"
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -30,9 +30,7 @@ exports[`ReviewModal component component snapshots error 1`] = `
|
||||
onClose={[MockFunction this.onClose]}
|
||||
title="test-ora-name"
|
||||
>
|
||||
<React.Fragment>
|
||||
<ReviewError />
|
||||
</React.Fragment>
|
||||
<ReviewContent />
|
||||
</FullscreenModal>
|
||||
`;
|
||||
|
||||
@@ -45,7 +43,7 @@ exports[`ReviewModal component component snapshots loading 1`] = `
|
||||
onClose={[MockFunction this.onClose]}
|
||||
title="test-ora-name"
|
||||
>
|
||||
<React.Fragment />
|
||||
<ReviewContent />
|
||||
<LoadingMessage
|
||||
message={
|
||||
Object {
|
||||
@@ -67,8 +65,6 @@ exports[`ReviewModal component component snapshots success 1`] = `
|
||||
onClose={[MockFunction this.onClose]}
|
||||
title="test-ora-name"
|
||||
>
|
||||
<React.Fragment>
|
||||
<ReviewContent />
|
||||
</React.Fragment>
|
||||
<ReviewContent />
|
||||
</FullscreenModal>
|
||||
`;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import LoadingMessage from 'components/LoadingMessage';
|
||||
import ReviewActions from 'containers/ReviewActions';
|
||||
import ReviewError from './ReviewError';
|
||||
import ReviewContent from './ReviewContent';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -29,11 +28,11 @@ export class ReviewModal extends React.Component {
|
||||
}
|
||||
|
||||
get isLoading() {
|
||||
return !(this.props.hasError || this.props.isLoaded);
|
||||
return !(this.props.errorStatus || this.props.isLoaded);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isOpen, isLoaded, hasError } = this.props;
|
||||
const { isOpen, isLoaded, errorStatus } = this.props;
|
||||
return (
|
||||
<FullscreenModal
|
||||
title={this.props.oraName}
|
||||
@@ -43,19 +42,15 @@ export class ReviewModal extends React.Component {
|
||||
className="review-modal"
|
||||
modalBodyClassName="review-modal-body"
|
||||
>
|
||||
{isOpen && (
|
||||
<>
|
||||
{isLoaded && <ReviewContent />}
|
||||
{hasError && <ReviewError />}
|
||||
</>
|
||||
)}
|
||||
{isOpen && <ReviewContent />}
|
||||
{/* even if the modal is closed, in case we want to add transitions later */}
|
||||
{!(isLoaded || hasError) && <LoadingMessage message={messages.loadingResponse} />}
|
||||
{!(isLoaded || errorStatus) && <LoadingMessage message={messages.loadingResponse} />}
|
||||
</FullscreenModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
ReviewModal.defaultProps = {
|
||||
errorStatus: null,
|
||||
response: null,
|
||||
};
|
||||
ReviewModal.propTypes = {
|
||||
@@ -66,7 +61,7 @@ ReviewModal.propTypes = {
|
||||
}),
|
||||
setShowReview: PropTypes.func.isRequired,
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
hasError: PropTypes.bool.isRequired,
|
||||
errorStatus: PropTypes.number,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
@@ -74,7 +69,7 @@ export const mapStateToProps = (state) => ({
|
||||
oraName: selectors.app.ora.name(state),
|
||||
response: selectors.grading.selected.response(state),
|
||||
isLoaded: selectors.requests.isCompleted(state, { requestKey: RequestKeys.fetchSubmission }),
|
||||
hasError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchSubmission }),
|
||||
errorStatus: selectors.requests.errorStatus(state, { requestKey: RequestKeys.fetchSubmission }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
|
||||
@@ -24,7 +24,7 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
requests: {
|
||||
isCompleted: (...args) => ({ isCompleted: args }),
|
||||
isFailed: (...args) => ({ isFailed: args }),
|
||||
errorStatus: (...args) => ({ errorStatus: args }),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@@ -35,7 +35,6 @@ jest.mock('data/redux', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('containers/ReviewActions', () => 'ReviewActions');
|
||||
jest.mock('./ReviewError', () => 'ReviewError');
|
||||
jest.mock('./ReviewContent', () => 'ReviewContent');
|
||||
jest.mock('components/LoadingMessage', () => 'LoadingMessage');
|
||||
|
||||
@@ -48,7 +47,6 @@ describe('ReviewModal component', () => {
|
||||
response: { text: (<div>some text</div>) },
|
||||
showRubric: false,
|
||||
isLoaded: false,
|
||||
hasError: false,
|
||||
};
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
@@ -69,7 +67,7 @@ describe('ReviewModal component', () => {
|
||||
expect(render()).toMatchSnapshot();
|
||||
});
|
||||
test('error', () => {
|
||||
el.setProps({ isOpen: true, hasError: true });
|
||||
el.setProps({ isOpen: true, errorStatus: 200 });
|
||||
expect(render()).toMatchSnapshot();
|
||||
});
|
||||
test('success', () => {
|
||||
@@ -96,8 +94,8 @@ describe('ReviewModal component', () => {
|
||||
test('isLoaded loads from requests.isCompleted(fetchSubmission)', () => {
|
||||
expect(mapped.isLoaded).toEqual(selectors.requests.isCompleted(testState, { requestKey }));
|
||||
});
|
||||
test('hasError loads from requests.isFailed(fetchSubmission)', () => {
|
||||
expect(mapped.hasError).toEqual(selectors.requests.isFailed(testState, { requestKey }));
|
||||
test('errorStatus loads from requests.errorStatus(fetchSubmission)', () => {
|
||||
expect(mapped.errorStatus).toEqual(selectors.requests.errorStatus(testState, { requestKey }));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
const messages = defineMessages({
|
||||
loadErrorHeading: {
|
||||
@@ -21,6 +22,26 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Loading response',
|
||||
description: 'loading text for submission response review screen',
|
||||
},
|
||||
gradeNotSubmittedHeading: {
|
||||
id: 'ora-grading.ReviewModal.gradeNotSubmitted.heading',
|
||||
defaultMessage: 'Grade not submitted',
|
||||
description: 'Grade submission network error heading',
|
||||
},
|
||||
gradeNotSubmittedContent: {
|
||||
id: 'ora-grading.ReviewModal.gradeNotSubmitted.heading',
|
||||
defaultMessage: "We're sorry, something went wrong when we tried to submit this grade. Please try again.",
|
||||
description: 'Grade submission network error message',
|
||||
},
|
||||
resubmitGrade: {
|
||||
id: 'ora-grading.ReviewModal.resubmitGrade',
|
||||
defaultMessage: 'Resubmit grate',
|
||||
description: 'Resubmit grade button after network failure',
|
||||
},
|
||||
dismiss: {
|
||||
id: 'ora-grading.ReviewModal.dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
description: 'Dismiss error action button text',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
export default StrictDict(messages);
|
||||
|
||||
@@ -69,4 +69,8 @@
|
||||
justify-content: center;
|
||||
padding: map-get($spacers, 3);
|
||||
}
|
||||
|
||||
button.pgn__stateful-btn.pgn__stateful-btn-state-pending {
|
||||
opacity: .4 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,27 +19,22 @@ exports[`Rubric Container snapshot is grading 1`] = `
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
key="1"
|
||||
orderNum={1}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
key="2"
|
||||
orderNum={2}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
key="3"
|
||||
orderNum={3}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
key="4"
|
||||
orderNum={4}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
key="5"
|
||||
orderNum={5}
|
||||
/>
|
||||
<hr />
|
||||
@@ -48,15 +43,109 @@ exports[`Rubric Container snapshot is grading 1`] = `
|
||||
<div
|
||||
className="grading-rubric-footer"
|
||||
>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
>
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
]
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Grade Submitted"
|
||||
description="Submit Grade button text after successful submission"
|
||||
id="ora-grading.Rubric.gradeSubmitted"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Submit grade"
|
||||
description="Submit Grade button text"
|
||||
id="ora-grading.Rubric.submitGrade"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Submitting grade"
|
||||
description="Submit Grade button text while submitting"
|
||||
id="ora-grading.Rubric.submittingGrade"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.submitGradeHandler]}
|
||||
state="default"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
`;
|
||||
|
||||
exports[`Rubric Container snapshot is grading, submit pending 1`] = `
|
||||
<Card
|
||||
className="grading-rubric-card"
|
||||
>
|
||||
<Card.Body
|
||||
className="grading-rubric-body"
|
||||
>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
defaultMessage="Submit grade"
|
||||
description="Submit Grade button text"
|
||||
id="ora-grading.Rubric.submitGrade"
|
||||
defaultMessage="Rubric"
|
||||
description="Rubric interface label"
|
||||
id="ora-grading.Rubric.rubric"
|
||||
/>
|
||||
</Button>
|
||||
</h3>
|
||||
<hr
|
||||
className="m-2.5"
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
orderNum={1}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
orderNum={2}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
orderNum={3}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
orderNum={4}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={true}
|
||||
orderNum={5}
|
||||
/>
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Body>
|
||||
<div
|
||||
className="grading-rubric-footer"
|
||||
>
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
]
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Grade Submitted"
|
||||
description="Submit Grade button text after successful submission"
|
||||
id="ora-grading.Rubric.gradeSubmitted"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Submit grade"
|
||||
description="Submit Grade button text"
|
||||
id="ora-grading.Rubric.submitGrade"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Submitting grade"
|
||||
description="Submit Grade button text while submitting"
|
||||
id="ora-grading.Rubric.submittingGrade"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.submitGradeHandler]}
|
||||
state="pending"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
`;
|
||||
@@ -80,27 +169,22 @@ exports[`Rubric Container snapshot is not grading 1`] = `
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
key="1"
|
||||
orderNum={1}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
key="2"
|
||||
orderNum={2}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
key="3"
|
||||
orderNum={3}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
key="4"
|
||||
orderNum={4}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
key="5"
|
||||
orderNum={5}
|
||||
/>
|
||||
<hr />
|
||||
@@ -108,3 +192,78 @@ exports[`Rubric Container snapshot is not grading 1`] = `
|
||||
</Card.Body>
|
||||
</Card>
|
||||
`;
|
||||
|
||||
exports[`Rubric Container snapshot submit completed 1`] = `
|
||||
<Card
|
||||
className="grading-rubric-card"
|
||||
>
|
||||
<Card.Body
|
||||
className="grading-rubric-body"
|
||||
>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
defaultMessage="Rubric"
|
||||
description="Rubric interface label"
|
||||
id="ora-grading.Rubric.rubric"
|
||||
/>
|
||||
</h3>
|
||||
<hr
|
||||
className="m-2.5"
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
orderNum={2}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
orderNum={3}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
orderNum={4}
|
||||
/>
|
||||
<CriterionContainer
|
||||
isGrading={false}
|
||||
orderNum={5}
|
||||
/>
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Body>
|
||||
<div
|
||||
className="grading-rubric-footer"
|
||||
>
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
]
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Grade Submitted"
|
||||
description="Submit Grade button text after successful submission"
|
||||
id="ora-grading.Rubric.gradeSubmitted"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Submit grade"
|
||||
description="Submit Grade button text"
|
||||
id="ora-grading.Rubric.submitGrade"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Submitting grade"
|
||||
description="Submit Grade button text while submitting"
|
||||
id="ora-grading.Rubric.submittingGrade"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.submitGradeHandler]}
|
||||
state="complete"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
`;
|
||||
|
||||
@@ -2,10 +2,12 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Card, Button } from '@edx/paragon';
|
||||
import { Card, StatefulButton } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import CriterionContainer from 'containers/CriterionContainer';
|
||||
import RubricFeedback from './RubricFeedback';
|
||||
@@ -13,6 +15,13 @@ import messages from './messages';
|
||||
|
||||
import './Rubric.scss';
|
||||
|
||||
const ButtonStates = StrictDict({
|
||||
default: 'default',
|
||||
pending: 'pending',
|
||||
complete: 'complete',
|
||||
error: 'error',
|
||||
});
|
||||
|
||||
/**
|
||||
* <Rubric />
|
||||
*/
|
||||
@@ -23,6 +32,16 @@ export class Rubric extends React.Component {
|
||||
this.submitGradeHandler = this.submitGradeHandler.bind(this);
|
||||
}
|
||||
|
||||
get submitButtonState() {
|
||||
if (this.props.isPending) {
|
||||
return ButtonStates.pending;
|
||||
}
|
||||
if (this.props.isCompleted) {
|
||||
return ButtonStates.complete;
|
||||
}
|
||||
return ButtonStates.default;
|
||||
}
|
||||
|
||||
submitGradeHandler() {
|
||||
this.props.submitGrade();
|
||||
}
|
||||
@@ -44,11 +63,18 @@ export class Rubric extends React.Component {
|
||||
<hr />
|
||||
<RubricFeedback />
|
||||
</Card.Body>
|
||||
{isGrading && (
|
||||
{(isGrading || this.props.isCompleted) && (
|
||||
<div className="grading-rubric-footer">
|
||||
<Button onClick={this.submitGradeHandler}>
|
||||
<FormattedMessage {...messages.submitGrade} />
|
||||
</Button>
|
||||
<StatefulButton
|
||||
onClick={this.submitGradeHandler}
|
||||
state={this.submitButtonState}
|
||||
disabledStates={[ButtonStates.pending]}
|
||||
labels={{
|
||||
[ButtonStates.default]: <FormattedMessage {...messages.submitGrade} />,
|
||||
[ButtonStates.pending]: <FormattedMessage {...messages.submittingGrade} />,
|
||||
[ButtonStates.complete]: <FormattedMessage {...messages.gradeSubmitted} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -59,13 +85,19 @@ Rubric.defaultProps = {
|
||||
criteriaIndices: [],
|
||||
};
|
||||
Rubric.propTypes = {
|
||||
isCompleted: PropTypes.bool.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
criteriaIndices: PropTypes.arrayOf(PropTypes.number),
|
||||
submitGrade: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const requestKey = RequestKeys.submitGrade;
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
isCompleted: selectors.requests.isCompleted(state, { requestKey }),
|
||||
isGrading: selectors.grading.selected.isGrading(state),
|
||||
isPending: selectors.requests.isPending(state, { requestKey }),
|
||||
criteriaIndices: selectors.app.rubric.criteriaIndices(state),
|
||||
});
|
||||
|
||||
|
||||
@@ -18,9 +18,13 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
grading: {
|
||||
selected: {
|
||||
isGrading: jest.fn((...args) => ({ isGragrding: args })),
|
||||
isGrading: jest.fn((...args) => ({ isGrading: args })),
|
||||
},
|
||||
},
|
||||
requests: {
|
||||
isCompleted: jest.fn((...args) => ({ isCompleted: args })),
|
||||
isPending: jest.fn((...args) => ({ isPending: args })),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
@@ -31,6 +35,8 @@ jest.mock('data/redux', () => ({
|
||||
|
||||
describe('Rubric Container', () => {
|
||||
const props = {
|
||||
isCompleted: false,
|
||||
isPending: false,
|
||||
isGrading: true,
|
||||
criteriaIndices: [1, 2, 3, 4, 5],
|
||||
submitGrade: jest.fn().mockName('this.props.submitGrade'),
|
||||
@@ -43,14 +49,23 @@ describe('Rubric Container', () => {
|
||||
.mockName('this.submitGradeHandler');
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
beforeEach(() => {
|
||||
el.instance().submitGradeHandler = jest.fn().mockName('this.submitGradeHandler');
|
||||
});
|
||||
test('is grading', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('is not grading', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
});
|
||||
expect(el).toMatchSnapshot();
|
||||
el.setProps({ isGrading: false });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('is grading, submit pending', () => {
|
||||
el.setProps({ isPending: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('submit completed', () => {
|
||||
el.setProps({ isCompleted: true, isGrading: false });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
gradeSubmitted: {
|
||||
id: 'ora-grading.Rubric.gradeSubmitted',
|
||||
defaultMessage: 'Grade Submitted',
|
||||
description: 'Submit Grade button text after successful submission',
|
||||
},
|
||||
rubric: {
|
||||
id: 'ora-grading.Rubric.rubric',
|
||||
defaultMessage: 'Rubric',
|
||||
@@ -11,6 +16,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Submit grade',
|
||||
description: 'Submit Grade button text',
|
||||
},
|
||||
submittingGrade: {
|
||||
id: 'ora-grading.Rubric.submittingGrade',
|
||||
defaultMessage: 'Submitting grade',
|
||||
description: 'Submit Grade button text while submitting',
|
||||
},
|
||||
overallComments: {
|
||||
id: 'ora-grading.Rubric.overallComments',
|
||||
defaultMessage: 'Overall comments',
|
||||
|
||||
@@ -16,3 +16,16 @@ export const RequestKeys = StrictDict({
|
||||
prefetchPrev: 'prefetchPrev',
|
||||
submitGrade: 'submitGrade',
|
||||
});
|
||||
|
||||
export const ErrorCodes = StrictDict({
|
||||
missingParam: 'ERR_MISSING_PARAM',
|
||||
});
|
||||
|
||||
export const ErrorStatuses = StrictDict({
|
||||
badRequest: 400,
|
||||
unauthorized: 401,
|
||||
forbidden: 403,
|
||||
notFound: 404,
|
||||
conflict: 409,
|
||||
serverError: 500,
|
||||
});
|
||||
|
||||
@@ -107,12 +107,11 @@ export const updateGradingData = (state, data) => {
|
||||
*/
|
||||
export const updateCriterion = (state, orderNum, data) => {
|
||||
const entry = state.gradingData[state.current.submissionUUID];
|
||||
const criteria = [...entry.criteria];
|
||||
criteria[orderNum] = { ...entry.criteria[orderNum], ...data };
|
||||
return updateGradingData(state, {
|
||||
...entry,
|
||||
criteria: {
|
||||
...entry.criteria,
|
||||
[orderNum]: { ...entry.criteria[orderNum], ...data },
|
||||
},
|
||||
criteria,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -196,15 +195,43 @@ const grading = createSlice({
|
||||
},
|
||||
};
|
||||
},
|
||||
stopGrading: (state) => {
|
||||
loadStatus: (state, { payload }) => {
|
||||
const gradingData = { ...state.gradingData };
|
||||
delete gradingData[state.current.submissionUUID];
|
||||
return {
|
||||
...state,
|
||||
gradeData: {
|
||||
...state.gradeData,
|
||||
[state.current.submissionUUID]: { ...payload.gradeData },
|
||||
},
|
||||
gradingData,
|
||||
current: {
|
||||
...state.current,
|
||||
gradeStatus: payload.gradeStatus,
|
||||
lockStatus: payload.lockStatus,
|
||||
},
|
||||
};
|
||||
},
|
||||
stopGrading: (state, { payload }) => {
|
||||
const { submissionUUID } = state.current;
|
||||
const localGradeData = { ...state.localGradeData };
|
||||
delete localGradeData[state.current.submissionUUID];
|
||||
delete localGradeData[submissionUUID];
|
||||
const gradeData = { ...state.gradeData };
|
||||
let lockStatus = lockStatuses.unlocked;
|
||||
let { gradeStatus } = state.current;
|
||||
if (payload) {
|
||||
const { submissionStatus } = payload;
|
||||
gradeData[submissionUUID] = submissionStatus.gradeData;
|
||||
lockStatus = submissionStatus.lockStatus;
|
||||
gradeStatus = submissionStatus.gradeStatus;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
localGradeData,
|
||||
current: {
|
||||
...state.current,
|
||||
lockStatus: lockStatuses.unlocked,
|
||||
lockStatus,
|
||||
gradeStatus,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -39,6 +39,10 @@ const requests = createSlice({
|
||||
error: payload.error,
|
||||
},
|
||||
}),
|
||||
clearRequest: (state, { payload }) => ({
|
||||
...state,
|
||||
[payload.requestKey]: {},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ export const isPending = ({ status }) => status === RequestStates.pending;
|
||||
export const isCompleted = ({ status }) => status === RequestStates.completed;
|
||||
export const isFailed = ({ status }) => status === RequestStates.failed;
|
||||
export const error = (request) => request.error;
|
||||
export const errorStatus = (request) => request.error?.response?.status;
|
||||
export const errorCode = (request) => request.error?.response?.data;
|
||||
|
||||
export const data = (request) => request.data;
|
||||
|
||||
export const allowNavigation = ({ requests }) => (
|
||||
@@ -25,5 +28,7 @@ export default StrictDict({
|
||||
isCompleted: statusSelector(isCompleted),
|
||||
isFailed: statusSelector(isFailed),
|
||||
error: statusSelector(error),
|
||||
errorCode: statusSelector(errorCode),
|
||||
errorStatus: statusSelector(errorStatus),
|
||||
data: statusSelector(data),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import { actions, selectors } from 'data/redux';
|
||||
import { RequestKeys, ErrorStatuses } from 'data/constants/requests';
|
||||
|
||||
import * as module from './grading';
|
||||
import requests from './requests';
|
||||
@@ -39,10 +40,14 @@ export const loadSelectionForReview = (submissionUUIDs) => (dispatch) => {
|
||||
|
||||
export const loadSubmission = () => (dispatch, getState) => {
|
||||
const submissionUUID = selectors.grading.selected.submissionUUID(getState());
|
||||
dispatch(actions.requests.clearRequest({ requestKey: RequestKeys.submitGrade }));
|
||||
dispatch(requests.fetchSubmission({
|
||||
submissionUUID,
|
||||
onSuccess: (response) => {
|
||||
dispatch(actions.grading.loadSubmission({ ...response, submissionUUID }));
|
||||
if (selectors.grading.selected.isGrading(getState())) {
|
||||
dispatch(module.startGrading());
|
||||
}
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -55,12 +60,13 @@ export const loadSubmission = () => (dispatch, getState) => {
|
||||
* based on the rubric config.
|
||||
*/
|
||||
export const startGrading = () => (dispatch, getState) => {
|
||||
dispatch(actions.requests.clearRequest({ requestKey: RequestKeys.submitGrade }));
|
||||
dispatch(requests.setLock({
|
||||
value: true,
|
||||
submissionUUID: selectors.grading.selected.submissionUUID(getState()),
|
||||
onSuccess: (response) => {
|
||||
dispatch(actions.app.setShowRubric(true));
|
||||
let { gradeData } = response;
|
||||
let gradeData = selectors.grading.selected.gradeData(getState());
|
||||
if (!gradeData) {
|
||||
gradeData = selectors.app.emptyGrade(getState());
|
||||
}
|
||||
@@ -74,6 +80,7 @@ export const startGrading = () => (dispatch, getState) => {
|
||||
* Releases the lock and dispatches stopGrading on success.
|
||||
*/
|
||||
export const cancelGrading = () => (dispatch, getState) => {
|
||||
dispatch(actions.requests.clearRequest({ requestKey: RequestKeys.submitGrade }));
|
||||
dispatch(requests.setLock({
|
||||
value: false,
|
||||
submissionUUID: selectors.grading.selected.submissionUUID(getState()),
|
||||
@@ -103,8 +110,10 @@ export const submitGrade = () => (dispatch, getState) => {
|
||||
onSuccess: (response) => {
|
||||
dispatch(actions.grading.completeGrading(response));
|
||||
},
|
||||
onFailure: () => {
|
||||
// on failure action
|
||||
onFailure: (error) => {
|
||||
if (error.response.status === ErrorStatuses.conflict) {
|
||||
dispatch(actions.grading.stopGrading(error.response.data));
|
||||
}
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { actions, selectors } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import * as thunkActions from './grading';
|
||||
|
||||
jest.mock('./requests', () => ({
|
||||
@@ -21,8 +22,9 @@ jest.mock('data/redux/grading/selectors', () => ({
|
||||
doesExist: jest.fn((state) => ({ nextDoesExist: state })),
|
||||
},
|
||||
selected: {
|
||||
submissionUUID: (state) => ({ selectedsubmissionUUID: state }),
|
||||
gradeData: jest.fn((state) => ({ gradeData: state })),
|
||||
isGrading: jest.fn((state) => ({ isGrading: state })),
|
||||
submissionUUID: (state) => ({ selectedsubmissionUUID: state }),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -32,13 +34,11 @@ describe('grading thunkActions', () => {
|
||||
const response = 'test-response';
|
||||
const objResponse = { response };
|
||||
let dispatch;
|
||||
let dispatched;
|
||||
let actionArgs;
|
||||
const getState = () => testState;
|
||||
|
||||
const getDispatched = (calledAction) => {
|
||||
calledAction(dispatch, getState);
|
||||
[[dispatched]] = dispatch.mock.calls;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -48,7 +48,11 @@ describe('grading thunkActions', () => {
|
||||
describe('loadSubmission', () => {
|
||||
beforeEach(() => {
|
||||
getDispatched(thunkActions.loadSubmission());
|
||||
actionArgs = dispatched.fetchSubmission;
|
||||
actionArgs = dispatch.mock.calls[1][0].fetchSubmission;
|
||||
});
|
||||
test('dispatches clearRequest for submitGrade', () => {
|
||||
const requestKey = RequestKeys.submitGrade;
|
||||
expect(dispatch.mock.calls[0]).toEqual([actions.requests.clearRequest({ requestKey })]);
|
||||
});
|
||||
test('dispatches fetchSubmission', () => {
|
||||
expect(actionArgs).not.toEqual(undefined);
|
||||
@@ -88,7 +92,7 @@ describe('grading thunkActions', () => {
|
||||
});
|
||||
});
|
||||
describe('loadPrev', () => {
|
||||
test('dispatches actions.grading.loadPrev and then loadSubmission', () => {
|
||||
test('clears submitGrade status and dispatches actions.grading.loadPrev and then loadSubmission', () => {
|
||||
thunkActions.loadPrev()(dispatch, getState);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
[actions.grading.loadPrev()],
|
||||
@@ -117,13 +121,17 @@ describe('grading thunkActions', () => {
|
||||
describe('startGrading', () => {
|
||||
beforeEach(() => {
|
||||
getDispatched(thunkActions.startGrading());
|
||||
actionArgs = dispatched.setLock;
|
||||
actionArgs = dispatch.mock.calls[1][0].setLock;
|
||||
});
|
||||
test('dispatches setLock with selected submissionUUID and value: true', () => {
|
||||
expect(actionArgs).not.toEqual(undefined);
|
||||
expect(actionArgs.value).toEqual(true);
|
||||
expect(actionArgs.submissionUUID).toEqual(selectors.grading.selected.submissionUUID(testState));
|
||||
});
|
||||
test('dispatches clearRequest for submitGrade', () => {
|
||||
const requestKey = RequestKeys.submitGrade;
|
||||
expect(dispatch.mock.calls[0]).toEqual([actions.requests.clearRequest({ requestKey })]);
|
||||
});
|
||||
describe('onSuccess', () => {
|
||||
const gradeData = { some: 'test grade data' };
|
||||
const startResponse = { other: 'fields', gradeData };
|
||||
@@ -132,7 +140,12 @@ describe('grading thunkActions', () => {
|
||||
});
|
||||
test('dispatches startGrading with selected gradeData if truthy', () => {
|
||||
actionArgs.onSuccess(startResponse);
|
||||
expect(dispatch.mock.calls).toContainEqual([actions.grading.startGrading(startResponse)]);
|
||||
expect(dispatch.mock.calls).toContainEqual([
|
||||
actions.grading.startGrading({
|
||||
...startResponse,
|
||||
gradeData: selectors.grading.selected.gradeData(testState),
|
||||
}),
|
||||
]);
|
||||
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
|
||||
});
|
||||
test('dispatches startGrading with empty grade if selected gradeData is not truthy', () => {
|
||||
@@ -140,6 +153,7 @@ describe('grading thunkActions', () => {
|
||||
const expected = [
|
||||
actions.grading.startGrading({ ...startResponse, gradeData: emptyGrade }),
|
||||
];
|
||||
selectors.grading.selected.gradeData.mockReturnValue(undefined);
|
||||
actionArgs.onSuccess({ ...startResponse, gradeData: undefined });
|
||||
expect(dispatch.mock.calls).toContainEqual(expected);
|
||||
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
|
||||
@@ -159,11 +173,15 @@ describe('grading thunkActions', () => {
|
||||
});
|
||||
beforeEach(() => {
|
||||
getDispatched(thunkActions.cancelGrading());
|
||||
actionArgs = dispatched.setLock;
|
||||
actionArgs = dispatch.mock.calls[1][0].setLock;
|
||||
});
|
||||
afterAll(() => {
|
||||
thunkActions.stopGrading = stopGrading;
|
||||
});
|
||||
test('dispatches clearRequest for submitGrade', () => {
|
||||
const requestKey = RequestKeys.submitGrade;
|
||||
expect(dispatch.mock.calls[0]).toEqual([actions.requests.clearRequest({ requestKey })]);
|
||||
});
|
||||
test('dispatches setLock with selected submissionUUID and value: false', () => {
|
||||
expect(actionArgs).not.toEqual(undefined);
|
||||
expect(actionArgs.value).toEqual(false);
|
||||
|
||||
@@ -99,10 +99,10 @@ export const fetchSubmission = ({ submissionUUID, ...rest }) => (dispatch) => {
|
||||
* @param {[func]} onSuccess - onSuccess method ((response) => { ... })
|
||||
* @param {[func]} onFailure - onFailure method ((error) => { ... })
|
||||
*/
|
||||
export const setLock = ({ submissionUUID, ...rest }) => (dispatch) => {
|
||||
export const setLock = ({ value, submissionUUID, ...rest }) => (dispatch) => {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.setLock,
|
||||
promise: api.lockSubmission(submissionUUID),
|
||||
promise: value ? api.lockSubmission(submissionUUID) : api.unlockSubmission(submissionUUID),
|
||||
...rest,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -8,7 +8,8 @@ jest.mock('data/services/lms/api', () => ({
|
||||
fetchSubmissionResponse: (submissionUUID) => ({ fetchSubmissionResponse: submissionUUID }),
|
||||
fetchSubmissionStatus: (submissionUUID) => ({ fetchSubmissionStatus: submissionUUID }),
|
||||
fetchSubmission: (submissionUUID) => ({ fetchSubmission: submissionUUID }),
|
||||
lockSubmission: ({ submissionUUID, value }) => ({ lockSubmission: { submissionUUID, value } }),
|
||||
lockSubmission: ({ submissionUUID }) => ({ lockSubmission: { submissionUUID } }),
|
||||
unlockSubmission: ({ submissionUUID }) => ({ unlockSubmission: { submissionUUID } }),
|
||||
updateGrade: (submissionUUID, gradeData) => ({ updateGrade: { submissionUUID, gradeData } }),
|
||||
}));
|
||||
|
||||
@@ -153,10 +154,10 @@ describe('requests thunkActions module', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('setLock', () => {
|
||||
describe('setLock: true', () => {
|
||||
testNetworkRequestAction({
|
||||
action: requests.setLock,
|
||||
args: { submissionUUID },
|
||||
args: { submissionUUID, value: true },
|
||||
expectedString: 'with setLock promise',
|
||||
expectedData: {
|
||||
requestKey: RequestKeys.setLock,
|
||||
@@ -164,6 +165,17 @@ describe('requests thunkActions module', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('setLock: false', () => {
|
||||
testNetworkRequestAction({
|
||||
action: requests.setLock,
|
||||
args: { submissionUUID, value: false },
|
||||
expectedString: 'with setLock promise',
|
||||
expectedData: {
|
||||
requestKey: RequestKeys.setLock,
|
||||
promise: api.unlockSubmission(submissionUUID),
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('submitGrade', () => {
|
||||
const gradeData = 'test-grade-data';
|
||||
testNetworkRequestAction({
|
||||
|
||||
@@ -2,7 +2,12 @@ import { StrictDict } from 'utils';
|
||||
import { locationId } from 'data/constants/app';
|
||||
import { paramKeys } from './constants';
|
||||
import urls from './urls';
|
||||
import { get, post, stringifyUrl } from './utils';
|
||||
import {
|
||||
client,
|
||||
get,
|
||||
post,
|
||||
stringifyUrl,
|
||||
} from './utils';
|
||||
|
||||
/*********************************************************************************
|
||||
* GET Actions
|
||||
@@ -75,11 +80,8 @@ export const fetchSubmissionResponse = (submissionUUID) => get(
|
||||
}),
|
||||
).then(response => response.data);
|
||||
|
||||
/* I assume this is the "Start Grading" call, even for if a
|
||||
* submission is already graded and we are attempting re-lock.
|
||||
* Assuming the check for if allowed would happen locally first.
|
||||
/**
|
||||
* post('api/lock', { ora_location, submissionUUID });
|
||||
* @param {bool} value - new lock value
|
||||
* @param {string} submissionUUID
|
||||
*/
|
||||
const lockSubmission = (submissionUUID) => post(
|
||||
@@ -88,12 +90,23 @@ const lockSubmission = (submissionUUID) => post(
|
||||
[paramKeys.submissionUUID]: submissionUUID,
|
||||
}),
|
||||
).then(response => response.data);
|
||||
/**
|
||||
* unlockSubmission(submissionUUID)
|
||||
* @param {string} submissionUUID
|
||||
*/
|
||||
const unlockSubmission = (submissionUUID) => client().delete(
|
||||
stringifyUrl(urls.fetchSubmissionLockUrl, {
|
||||
[paramKeys.oraLocation]: locationId,
|
||||
[paramKeys.submissionUUID]: submissionUUID,
|
||||
}),
|
||||
).then(response => response.data);
|
||||
|
||||
/*
|
||||
* post('api/updateGrade', { submissionUUID, gradeData })
|
||||
* @param {object} gradeData - full grading submission data
|
||||
*/
|
||||
const updateGrade = (submissionUUID, gradeData) => post(
|
||||
stringifyUrl(urls.updateSubmissioonGradeUrl, {
|
||||
stringifyUrl(urls.updateSubmissionGradeUrl, {
|
||||
[paramKeys.oraLocation]: locationId,
|
||||
[paramKeys.submissionUUID]: submissionUUID,
|
||||
}),
|
||||
@@ -107,4 +120,5 @@ export default StrictDict({
|
||||
fetchSubmissionStatus,
|
||||
lockSubmission,
|
||||
updateGrade,
|
||||
unlockSubmission,
|
||||
});
|
||||
|
||||
73
src/data/services/lms/fakeData/testUtils.js
Normal file
73
src/data/services/lms/fakeData/testUtils.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { ErrorStatuses, RequestKeys } from 'data/constants/requests';
|
||||
import { gradeStatuses, lockStatuses } from 'data/services/lms/constants';
|
||||
import { actions } from 'data/redux';
|
||||
|
||||
export const errorData = (status, data = '') => ({
|
||||
response: {
|
||||
status,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
export const networkErrorData = errorData(ErrorStatuses.badRequest);
|
||||
|
||||
const gradeData = {
|
||||
overallFeedback: 'was okay',
|
||||
criteria: [{ feedback: 'did alright', name: 'firstCriterion', selectedOption: 'good' }],
|
||||
};
|
||||
|
||||
export const genTestUtils = ({ dispatch }) => {
|
||||
const mockStart = (requestKey) => () => {
|
||||
dispatch(actions.requests.startRequest(requestKey));
|
||||
};
|
||||
|
||||
const mockError = (requestKey, status, data) => () => {
|
||||
dispatch(actions.requests.failRequest({
|
||||
requestKey,
|
||||
error: errorData(status, data),
|
||||
}));
|
||||
};
|
||||
|
||||
const mockNetworkError = (requestKey) => (
|
||||
mockError(requestKey, ErrorStatuses.badRequest)
|
||||
);
|
||||
|
||||
return {
|
||||
init: StrictDict({
|
||||
start: mockStart(RequestKeys.initialize),
|
||||
networkError: mockNetworkError(RequestKeys.initialize),
|
||||
}),
|
||||
fetch: StrictDict({
|
||||
start: mockStart(RequestKeys.fetchSubmission),
|
||||
mockError: mockError(RequestKeys.fetchSubmission, ErrorStatuses.badRequest),
|
||||
}),
|
||||
submitGrade: StrictDict({
|
||||
start: mockStart(RequestKeys.submitGrade),
|
||||
success: () => {
|
||||
dispatch(actions.requests.completeRequest({
|
||||
requestKey: RequestKeys.submitGrade,
|
||||
response: {
|
||||
gradeStatus: gradeStatuses.graded,
|
||||
lockStatus: lockStatuses.unlocked,
|
||||
gradeData,
|
||||
},
|
||||
}));
|
||||
},
|
||||
networkError: mockError(RequestKeys.submitGrade, ErrorStatuses.badRequest),
|
||||
rejectedError: mockError(
|
||||
RequestKeys.submitGrade,
|
||||
ErrorStatuses.conflict,
|
||||
{
|
||||
submissionStatus: {
|
||||
gradeStatus: gradeStatuses.ungraded,
|
||||
lockStatus: lockStatuses.locked,
|
||||
gradeData,
|
||||
},
|
||||
},
|
||||
),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export default genTestUtils;
|
||||
@@ -10,7 +10,7 @@ const oraInitializeUrl = `${baseEsgUrl}initialize`;
|
||||
const fetchSubmissionUrl = `${baseEsgUrl}submission`;
|
||||
const fetchSubmissionStatusUrl = `${baseEsgUrl}submission/status`;
|
||||
const fetchSubmissionLockUrl = `${baseEsgUrl}submission/lock`;
|
||||
const updateSubmissioonGradeUrl = `${baseEsgUrl}submission/grade`;
|
||||
const updateSubmissionGradeUrl = `${baseEsgUrl}submission/grade`;
|
||||
|
||||
const course = (courseId) => `${baseUrl}/courses/${courseId}`;
|
||||
|
||||
@@ -25,7 +25,7 @@ export default StrictDict({
|
||||
fetchSubmissionUrl,
|
||||
fetchSubmissionStatusUrl,
|
||||
fetchSubmissionLockUrl,
|
||||
updateSubmissioonGradeUrl,
|
||||
updateSubmissionGradeUrl,
|
||||
baseUrl,
|
||||
course,
|
||||
openResponse,
|
||||
|
||||
@@ -15,6 +15,8 @@ export const get = (...args) => getAuthenticatedHttpClient().get(...args);
|
||||
*/
|
||||
export const post = (...args) => getAuthenticatedHttpClient().post(...args);
|
||||
|
||||
export const client = getAuthenticatedHttpClient;
|
||||
|
||||
/**
|
||||
* stringifyUrl(url, query)
|
||||
* simple wrapper around queryString.stringifyUrl that sets skip behavior
|
||||
|
||||
@@ -3,6 +3,8 @@ import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
|
||||
import { createLogger } from 'redux-logger';
|
||||
|
||||
import apiTestUtils from 'data/services/lms/fakeData/testUtils';
|
||||
|
||||
import reducer, { actions, selectors } from './redux';
|
||||
|
||||
export const createStore = () => {
|
||||
@@ -22,6 +24,7 @@ export const createStore = () => {
|
||||
window.store = store;
|
||||
window.actions = actions;
|
||||
window.selectors = selectors;
|
||||
window.apiTestUtils = apiTestUtils(store);
|
||||
}
|
||||
|
||||
return store;
|
||||
|
||||
@@ -71,6 +71,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
Content: 'Popover.Content',
|
||||
},
|
||||
Row: 'Row',
|
||||
StatefulButton: 'StatefulButton',
|
||||
TextFilter: 'TextFilter',
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user