From 7634e0ad7e66e622469030a28bb7c2014df7009f Mon Sep 17 00:00:00 2001
From: Ben Warzeski
Date: Wed, 24 Nov 2021 13:05:18 -0500
Subject: [PATCH] feat: Load submission pending and error states (#32)
* feat: list pending and error states
* fix: merge clunk
* feat: fetchSubmission pending and error states
* fix: lint fix
* fix: fix tests
* refactor: combined submission fetch
* fix: make merge fixes happy
* fix: linting
* fix: simplify selectors and fix action bar score
* fix: fix tests
* Update src/data/redux/requests/selectors.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>
---
src/components/LoadingMessage.jsx | 25 ++
src/containers/ListView/ListError.jsx | 63 +++++
src/containers/ListView/ListError.test.jsx | 67 +++++
src/containers/ListView/ListView.scss | 4 +
.../ListView/ListViewBreadcrumb.jsx | 6 +-
src/containers/ListView/SubmissionsTable.jsx | 157 +++++++++++
.../ListView/SubmissionsTable.test.jsx | 246 ++++++++++++++++++
.../__snapshots__/ListError.test.jsx.snap | 44 ++++
.../ListViewBreadcrumb.test.jsx.snap | 8 +-
.../SubmissionsTable.test.jsx.snap | 123 +++++++++
.../__snapshots__/index.test.jsx.snap | 156 +++--------
src/containers/ListView/index.jsx | 145 ++---------
src/containers/ListView/index.test.jsx | 228 +++-------------
src/containers/ListView/messages.js | 25 ++
.../__snapshots__/index.test.jsx.snap | 44 +++-
.../components/SubmissionNavigation.jsx | 8 +-
.../components/SubmissionNavigation.test.jsx | 3 +
.../SubmissionNavigation.test.jsx.snap | 4 +-
src/containers/ReviewActions/index.jsx | 32 ++-
src/containers/ReviewActions/index.test.jsx | 25 +-
src/containers/ReviewModal/ReviewContent.jsx | 36 +++
.../ReviewModal/ReviewContent.test.jsx | 43 +++
src/containers/ReviewModal/ReviewError.jsx | 51 ++++
.../ReviewModal/ReviewError.test.jsx | 34 +++
.../__snapshots__/ReviewContent.test.jsx.snap | 30 +++
.../__snapshots__/ReviewError.test.jsx.snap | 35 +++
.../__snapshots__/index.test.jsx.snap | 74 ++++++
src/containers/ReviewModal/index.jsx | 44 ++--
src/containers/ReviewModal/index.test.jsx | 108 ++++++++
src/containers/ReviewModal/messages.js | 26 ++
src/data/redux/grading/reducer.js | 35 +--
src/data/redux/grading/selectors.js | 10 +-
src/data/redux/requests/index.js | 1 +
src/data/redux/requests/selectors.js | 29 +++
src/data/redux/thunkActions/grading.js | 97 ++-----
src/data/redux/thunkActions/grading.test.js | 235 ++++-------------
src/setupTest.js | 6 +
src/test/app.test.jsx | 40 +--
38 files changed, 1536 insertions(+), 811 deletions(-)
create mode 100644 src/components/LoadingMessage.jsx
create mode 100644 src/containers/ListView/ListError.jsx
create mode 100644 src/containers/ListView/ListError.test.jsx
create mode 100644 src/containers/ListView/SubmissionsTable.jsx
create mode 100644 src/containers/ListView/SubmissionsTable.test.jsx
create mode 100644 src/containers/ListView/__snapshots__/ListError.test.jsx.snap
create mode 100644 src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap
create mode 100644 src/containers/ReviewModal/ReviewContent.jsx
create mode 100644 src/containers/ReviewModal/ReviewContent.test.jsx
create mode 100644 src/containers/ReviewModal/ReviewError.jsx
create mode 100644 src/containers/ReviewModal/ReviewError.test.jsx
create mode 100644 src/containers/ReviewModal/__snapshots__/ReviewContent.test.jsx.snap
create mode 100644 src/containers/ReviewModal/__snapshots__/ReviewError.test.jsx.snap
create mode 100644 src/containers/ReviewModal/__snapshots__/index.test.jsx.snap
create mode 100644 src/containers/ReviewModal/index.test.jsx
create mode 100644 src/containers/ReviewModal/messages.js
create mode 100644 src/data/redux/requests/selectors.js
diff --git a/src/components/LoadingMessage.jsx b/src/components/LoadingMessage.jsx
new file mode 100644
index 0000000..584d1f2
--- /dev/null
+++ b/src/components/LoadingMessage.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Spinner } from '@edx/paragon';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+
+/**
+ *
+ */
+export const LoadingMessage = ({ message }) => (
+
+
+
+
+);
+LoadingMessage.defaultProps = {
+};
+LoadingMessage.propTypes = {
+ message: PropTypes.shape({
+ id: PropTypes.string,
+ defaultMessage: PropTypes.string,
+ }).isRequired,
+};
+
+export default LoadingMessage;
diff --git a/src/containers/ListView/ListError.jsx b/src/containers/ListView/ListError.jsx
new file mode 100644
index 0000000..b29b4dd
--- /dev/null
+++ b/src/containers/ListView/ListError.jsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import {
+ Alert,
+ Button,
+ Hyperlink,
+} from '@edx/paragon';
+import { Info } from '@edx/paragon/icons';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+
+import urls from 'data/services/lms/urls';
+
+import { selectors, thunkActions } from 'data/redux';
+
+import messages from './messages';
+
+/**
+ *
+ */
+export const ListError = ({ courseId, initializeApp }) => (
+ Reload Submissions,
+ ]}
+ >
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+);
+ListError.defaultProps = {
+};
+ListError.propTypes = {
+ // redux
+ courseId: PropTypes.string.isRequired,
+ initializeApp: PropTypes.func.isRequired,
+};
+
+export const mapStateToProps = (state) => ({
+ courseId: selectors.app.courseId(state),
+});
+
+export const mapDispatchToProps = {
+ initializeApp: thunkActions.app.initialize,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ListError);
diff --git a/src/containers/ListView/ListError.test.jsx b/src/containers/ListView/ListError.test.jsx
new file mode 100644
index 0000000..0463e7a
--- /dev/null
+++ b/src/containers/ListView/ListError.test.jsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { selectors, thunkActions } from 'data/redux';
+
+import { formatMessage } from 'testUtils';
+import {
+ ListError,
+ mapDispatchToProps,
+ mapStateToProps,
+} from './ListError';
+
+jest.mock('data/redux', () => ({
+ selectors: {
+ app: {
+ courseId: (...args) => ({ courseId: args }),
+ },
+ },
+ thunkActions: {
+ app: {
+ initialize: jest.fn(),
+ },
+ },
+}));
+
+jest.mock('data/services/lms/urls', () => ({
+ openResponse: (courseId) => `api/openResponse/${courseId}`,
+}));
+
+let el;
+jest.useFakeTimers('modern');
+
+describe('ListError component', () => {
+ describe('component', () => {
+ const props = {
+ courseId: 'test-course-id',
+ };
+ beforeEach(() => {
+ props.loadSelectionForReview = jest.fn();
+ props.intl = { formatMessage };
+ props.initializeApp = jest.fn();
+ });
+ describe('render tests', () => {
+ beforeEach(() => {
+ el = shallow();
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ });
+ });
+ describe('mapStateToProps', () => {
+ let mapped;
+ const testState = { some: 'test-state' };
+ beforeEach(() => {
+ mapped = mapStateToProps(testState);
+ });
+ test('courseId loads from app.courseId', () => {
+ expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ it('loads initializeApp from thunkActions.app.initialize', () => {
+ expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize);
+ });
+ });
+});
diff --git a/src/containers/ListView/ListView.scss b/src/containers/ListView/ListView.scss
index e69de29..9278823 100644
--- a/src/containers/ListView/ListView.scss
+++ b/src/containers/ListView/ListView.scss
@@ -0,0 +1,4 @@
+span.pgn__icon.breadcrumb-arrow {
+ width: 16px !important;
+ height: 16px !important;
+};
diff --git a/src/containers/ListView/ListViewBreadcrumb.jsx b/src/containers/ListView/ListViewBreadcrumb.jsx
index 25465d4..3f99281 100644
--- a/src/containers/ListView/ListViewBreadcrumb.jsx
+++ b/src/containers/ListView/ListViewBreadcrumb.jsx
@@ -17,13 +17,13 @@ import messages from './messages';
export const ListViewBreadcrumb = ({ courseId, oraName }) => (
<>
-
+
{oraName}
-
-
+
+
>
diff --git a/src/containers/ListView/SubmissionsTable.jsx b/src/containers/ListView/SubmissionsTable.jsx
new file mode 100644
index 0000000..5b89b51
--- /dev/null
+++ b/src/containers/ListView/SubmissionsTable.jsx
@@ -0,0 +1,157 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import {
+ DataTable,
+ TextFilter,
+ MultiSelectDropdownFilter,
+} from '@edx/paragon';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
+import { gradingStatuses } from 'data/services/lms/constants';
+import lmsMessages from 'data/services/lms/messages';
+
+import { selectors, thunkActions } from 'data/redux';
+
+import StatusBadge from 'components/StatusBadge';
+
+import messages from './messages';
+
+/**
+ *
+ */
+export class SubmissionsTable extends React.Component {
+ constructor(props) {
+ super(props);
+ this.handleViewAllResponsesClick = this.handleViewAllResponsesClick.bind(this);
+ this.selectedBulkAction = this.selectedBulkAction.bind(this);
+ }
+
+ get gradeStatusOptions() {
+ return Object.keys(gradingStatuses).map(statusKey => ({
+ name: this.translate(lmsMessages[gradingStatuses[statusKey]]),
+ value: gradingStatuses[statusKey],
+ }));
+ }
+
+ formatDate = ({ value }) => {
+ const date = new Date(value);
+ return date.toLocaleString();
+ }
+
+ formatGrade = ({ value: score }) => (
+ score === null ? '-' : `${score.pointsEarned}/${score.pointsPossible}`
+ );
+
+ formatStatus = ({ value }) => ();
+
+ translate = (...args) => this.props.intl.formatMessage(...args);
+
+ handleViewAllResponsesClick(data) {
+ const getsubmissionUUID = (row) => row.original.submissionUUID;
+ const rows = data.selectedRows.length ? data.selectedRows : data.tableInstance.rows;
+ this.props.loadSelectionForReview(rows.map(getsubmissionUUID));
+ }
+
+ selectedBulkAction(selectedFlatRows) {
+ return {
+ buttonText: this.translate(
+ messages.viewSelectedResponses,
+ { value: selectedFlatRows.length },
+ ),
+ className: 'view-selected-responses-btn',
+ handleClick: this.handleViewAllResponsesClick,
+ variant: 'primary',
+ };
+ }
+
+ render() {
+ if (!this.props.listData.length) {
+ return null;
+ }
+ return (
+
+
+
+
+
+
+ );
+ }
+}
+SubmissionsTable.defaultProps = {
+ listData: [],
+};
+SubmissionsTable.propTypes = {
+ // injected
+ intl: intlShape.isRequired,
+ // redux
+ listData: PropTypes.arrayOf(PropTypes.shape({
+ username: PropTypes.string,
+ dateSubmitted: PropTypes.number,
+ gradingStatus: PropTypes.string,
+ score: PropTypes.shape({
+ pointsEarned: PropTypes.number,
+ pointsPossible: PropTypes.number,
+ }),
+ })),
+ loadSelectionForReview: PropTypes.func.isRequired,
+};
+
+export const mapStateToProps = (state) => ({
+ listData: selectors.submissions.listData(state),
+});
+
+export const mapDispatchToProps = {
+ loadSelectionForReview: thunkActions.grading.loadSelectionForReview,
+};
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SubmissionsTable));
diff --git a/src/containers/ListView/SubmissionsTable.test.jsx b/src/containers/ListView/SubmissionsTable.test.jsx
new file mode 100644
index 0000000..2610dad
--- /dev/null
+++ b/src/containers/ListView/SubmissionsTable.test.jsx
@@ -0,0 +1,246 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import {
+ DataTable,
+ MultiSelectDropdownFilter,
+ TextFilter,
+} from '@edx/paragon';
+
+import { selectors, thunkActions } from 'data/redux';
+import { gradingStatuses as statuses } from 'data/services/lms/constants';
+
+import StatusBadge from 'components/StatusBadge';
+import { formatMessage } from 'testUtils';
+import messages from './messages';
+import {
+ SubmissionsTable,
+ mapStateToProps,
+ mapDispatchToProps,
+} from './SubmissionsTable';
+
+jest.mock('data/redux', () => ({
+ selectors: {
+ submissions: {
+ listData: (...args) => ({ listData: args }),
+ },
+ },
+ thunkActions: {
+ grading: {
+ loadSelectionForReview: (...args) => ({ loadSelectionForReview: args }),
+ },
+ },
+}));
+
+let el;
+jest.useFakeTimers('modern');
+
+describe('SubmissionsTable component', () => {
+ describe('component', () => {
+ const props = {
+ listData: [
+ {
+ username: 'username-1',
+ dateSubmitted: 16131215154955,
+ gradingStatus: statuses.ungraded,
+ score: {
+ pointsEarned: 1,
+ pointsPossible: 10,
+ },
+ },
+ {
+ username: 'username-2',
+ dateSubmitted: 16131225154955,
+ gradingStatus: statuses.graded,
+ score: {
+ pointsEarned: 2,
+ pointsPossible: 10,
+ },
+ },
+ {
+ username: 'username-3',
+ dateSubmitted: 16131215250955,
+ gradingStatus: statuses.inProgress,
+ score: {
+ pointsEarned: 3,
+ pointsPossible: 10,
+ },
+ },
+ ],
+ };
+ beforeEach(() => {
+ props.loadSelectionForReview = jest.fn();
+ props.intl = { formatMessage };
+ });
+ describe('render tests', () => {
+ const mockMethod = (methodName) => {
+ el.instance()[methodName] = jest.fn().mockName(`this.${methodName}`);
+ };
+ beforeEach(() => {
+ el = shallow();
+ });
+ describe('snapshots', () => {
+ beforeEach(() => {
+ mockMethod('handleViewAllResponsesClick');
+ mockMethod('selectedBulkAction');
+ mockMethod('formatDate');
+ mockMethod('formatGrade');
+ mockMethod('formatStatus');
+ });
+ test('snapshot: empty (no list data)', () => {
+ el = shallow();
+ expect(el).toMatchSnapshot();
+ expect(el.isEmptyRender()).toEqual(true);
+ });
+ test('snapshot: happy path', () => {
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ });
+ describe('DataTable', () => {
+ let table;
+ let tableProps;
+ beforeEach(() => {
+ table = el.find(DataTable);
+ tableProps = table.props();
+ });
+ test.each([
+ 'isFilterable',
+ 'isSelectable',
+ 'isSortable',
+ 'isPaginated',
+ ])('%s', key => expect(tableProps[key]).toEqual(true));
+ test.each([
+ ['numBreakoutFilters', 2],
+ ['defaultColumnValues', { Filter: TextFilter }],
+ ['itemCount', 3],
+ ['initialState', { pageSize: 10, pageIndex: 0 }],
+ ])('%s = %p', (key, value) => expect(tableProps[key]).toEqual(value));
+ test('bulkActions linked to selectedBulkAction', () => {
+ expect(tableProps.bulkActions).toEqual([el.instance().selectedBulkAction]);
+ });
+ describe('columns', () => {
+ let columns;
+ beforeEach(() => {
+ columns = tableProps.columns;
+ });
+ test('username column', () => {
+ expect(columns[0]).toEqual({
+ Header: messages.username.defaultMessage,
+ accessor: 'username',
+ });
+ });
+ test('submission date column', () => {
+ expect(columns[1]).toEqual({
+ Header: messages.learnerSubmissionDate.defaultMessage,
+ accessor: 'dateSubmitted',
+ Cell: el.instance().formatDate,
+ disableFilters: true,
+ });
+ });
+ test('grade column', () => {
+ expect(columns[2]).toEqual({
+ Header: messages.grade.defaultMessage,
+ accessor: 'score',
+ Cell: el.instance().formatGrade,
+ disableFilters: true,
+ });
+ });
+ test('grading status column', () => {
+ expect(columns[3]).toEqual({
+ Header: messages.gradingStatus.defaultMessage,
+ accessor: 'gradingStatus',
+ Cell: el.instance().formatStatus,
+ Filter: MultiSelectDropdownFilter,
+ filter: 'includesValue',
+ filterChoices: el.instance().gradeStatusOptions,
+ });
+ });
+ });
+ });
+ });
+ describe('behavior', () => {
+ describe('formatDate method', () => {
+ it('returns the date in locale time string', () => {
+ const fakeDate = 16131215154955;
+ const fakeDateString = 'test-date-string';
+ const mock = jest.spyOn(Date.prototype, 'toLocaleString').mockReturnValue(fakeDateString);
+ expect(el.instance().formatDate({ value: fakeDate })).toEqual(fakeDateString);
+ mock.mockRestore();
+ });
+ });
+ describe('formatGrade method', () => {
+ it('returns "-" if grade is null', () => {
+ expect(el.instance().formatGrade({ value: null })).toEqual('-');
+ });
+ it('returns / if grade exists', () => {
+ expect(
+ el.instance().formatGrade({ value: { pointsEarned: 1, pointsPossible: 10 } }),
+ ).toEqual('1/10');
+ });
+ });
+ describe('formatStatus method', () => {
+ it('returns a StatusBadge with the given status', () => {
+ const status = 'graded';
+ expect(el.instance().formatStatus({ value: 'graded' })).toEqual(
+ ,
+ );
+ });
+ });
+ describe('handleViewAllResponsesClick', () => {
+ it('calls loadSelectionForReview with submissionUUID from all rows if there are no selectedRows', () => {
+ const data = {
+ selectedRows: [
+ ],
+ tableInstance: {
+ rows: [
+ { original: { submissionUUID: '123' } },
+ { original: { submissionUUID: '456' } },
+ { original: { submissionUUID: '789' } },
+ ],
+ },
+ };
+ el.instance().handleViewAllResponsesClick(data);
+ expect(el.instance().props.loadSelectionForReview).toHaveBeenCalledWith(['123', '456', '789']);
+ });
+ it('calls loadSelectionForReview with submissionUUID from selected rows if there are any', () => {
+ const data = {
+ selectedRows: [
+ { original: { submissionUUID: '123' } },
+ { original: { submissionUUID: '456' } },
+ { original: { submissionUUID: '789' } },
+ ],
+ };
+ el.instance().handleViewAllResponsesClick(data);
+ expect(
+ el.instance().props.loadSelectionForReview,
+ ).toHaveBeenCalledWith(['123', '456', '789']);
+ });
+ });
+ describe('selectedBulkAction', () => {
+ it('includes selection length and triggers handleViewAllResponsesClick', () => {
+ const rows = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+ const action = el.instance().selectedBulkAction(rows);
+ expect(action.buttonText).toEqual(expect.stringContaining(rows.length.toString()));
+ expect(action.handleClick).toEqual(el.instance().handleViewAllResponsesClick);
+ });
+ });
+ });
+ });
+ describe('mapStateToProps', () => {
+ let mapped;
+ const testState = { some: 'test-state' };
+ beforeEach(() => {
+ mapped = mapStateToProps(testState);
+ });
+ test('listData loads from submissions.listData', () => {
+ expect(mapped.listData).toEqual(selectors.submissions.listData(testState));
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ it('loads loadSelectionForReview from thunkActions.grading.loadSelectionForReview', () => {
+ expect(
+ mapDispatchToProps.loadSelectionForReview,
+ ).toEqual(thunkActions.grading.loadSelectionForReview);
+ });
+ });
+});
diff --git a/src/containers/ListView/__snapshots__/ListError.test.jsx.snap b/src/containers/ListView/__snapshots__/ListError.test.jsx.snap
new file mode 100644
index 0000000..3cdd0e3
--- /dev/null
+++ b/src/containers/ListView/__snapshots__/ListError.test.jsx.snap
@@ -0,0 +1,44 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ListError component component render tests snapshot 1`] = `
+
+ Reload Submissions
+ ,
+ ]
+ }
+ variant="danger"
+>
+
+
+
+
+
+
+ ,
+ }
+ }
+ />
+
+
+`;
diff --git a/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap b/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap
index 687656a..aac3626 100644
--- a/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap
+++ b/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap
@@ -7,8 +7,8 @@ exports[`ListViewBreadcrumb component component snapshot: empty (no list data) 1
destination="openResponseUrl(test-course-id)"
>
diff --git a/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap b/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap
new file mode 100644
index 0000000..5c373df
--- /dev/null
+++ b/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap
@@ -0,0 +1,123 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SubmissionsTable component component render tests snapshots snapshot: empty (no list data) 1`] = `""`;
+
+exports[`SubmissionsTable component component render tests snapshots snapshot: happy path 1`] = `
+
+
+
+
+
+
+`;
diff --git a/src/containers/ListView/__snapshots__/index.test.jsx.snap b/src/containers/ListView/__snapshots__/index.test.jsx.snap
index 8c1b0a2..651dc65 100644
--- a/src/containers/ListView/__snapshots__/index.test.jsx.snap
+++ b/src/containers/ListView/__snapshots__/index.test.jsx.snap
@@ -1,129 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ListView component component render tests snapshots snapshot: empty (no list data) 1`] = `""`;
-
-exports[`ListView component component render tests snapshots snapshot: happy path 1`] = `
+exports[`ListView component component render tests snapshots snapshot: error 1`] = `
-
-
-
-
-
-
+
+
+
+`;
+
+exports[`ListView component component render tests snapshots snapshot: loaded 1`] = `
+
+
+
+
+
+`;
+
+exports[`ListView component component render tests snapshots snapshot: loading 1`] = `
+
+
+
+
+
+
+
+
`;
diff --git a/src/containers/ListView/index.jsx b/src/containers/ListView/index.jsx
index 20ae540..6107148 100644
--- a/src/containers/ListView/index.jsx
+++ b/src/containers/ListView/index.jsx
@@ -3,22 +3,19 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
- DataTable,
- TextFilter,
- MultiSelectDropdownFilter,
Container,
+ Spinner,
} from '@edx/paragon';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-
-import { gradingStatuses } from 'data/services/lms/constants';
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { selectors, thunkActions } from 'data/redux';
-import lmsMessages from 'data/services/lms/messages';
+import { RequestKeys } from 'data/constants/requests';
-import StatusBadge from 'components/StatusBadge';
import ReviewModal from 'containers/ReviewModal';
+import ListError from './ListError';
import ListViewBreadcrumb from './ListViewBreadcrumb';
+import SubmissionsTable from './SubmissionsTable';
import messages from './messages';
import './ListView.scss';
@@ -29,142 +26,46 @@ export class ListView extends React.Component {
constructor(props) {
super(props);
this.props.initializeApp();
- this.handleViewAllResponsesClick = this.handleViewAllResponsesClick.bind(this);
- this.selectedBulkAction = this.selectedBulkAction.bind(this);
- }
-
- get gradeStatusOptions() {
- return Object.keys(gradingStatuses).map(statusKey => ({
- name: this.translate(lmsMessages[gradingStatuses[statusKey]]),
- value: gradingStatuses[statusKey],
- }));
- }
-
- formatDate = ({ value }) => {
- const date = new Date(value);
- return date.toLocaleString();
- }
-
- formatGrade = ({ value: grade }) => (
- grade === null ? '-' : `${grade.pointsEarned}/${grade.pointsPossible}`
- );
-
- formatStatus = ({ value }) => ();
-
- translate = (...args) => this.props.intl.formatMessage(...args);
-
- handleViewAllResponsesClick(data) {
- const getsubmissionUUID = (row) => row.original.submissionUUID;
- const rows = data.selectedRows.length ? data.selectedRows : data.tableInstance.rows;
- this.props.loadSelectionForReview(rows.map(getsubmissionUUID));
- }
-
- selectedBulkAction(selectedFlatRows) {
- return {
- buttonText: this.translate(
- messages.viewSelectedResponses,
- { value: selectedFlatRows.length },
- ),
- className: 'view-selected-responses-btn',
- handleClick: this.handleViewAllResponsesClick,
- variant: 'primary',
- };
}
render() {
- // hide if submissions are not loaded.
- if (this.props.listData.length === 0) {
- return null;
- }
-
+ const { isLoaded, hasError } = this.props;
return (
-
-
-
-
-
-
+ { isLoaded && }
+ { hasError && }
+ { (!isLoaded && !hasError) && (
+
+
+
+
+ )}
);
}
}
ListView.defaultProps = {
- listData: [],
};
ListView.propTypes = {
- // injected
- intl: intlShape.isRequired,
// redux
+ courseId: PropTypes.string.isRequired,
initializeApp: PropTypes.func.isRequired,
- listData: PropTypes.arrayOf(PropTypes.shape({
- username: PropTypes.string,
- dateSubmitted: PropTypes.number,
- gradingStatus: PropTypes.string,
- score: PropTypes.shape({
- pointsEarned: PropTypes.number,
- pointsPossible: PropTypes.number,
- }),
- })),
- loadSelectionForReview: PropTypes.func.isRequired,
+ isLoaded: PropTypes.bool.isRequired,
+ isPending: PropTypes.bool.isRequired,
+ hasError: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
- listData: selectors.submissions.listData(state),
+ courseId: selectors.app.courseId(state),
+ isLoaded: selectors.requests.isCompleted(state, { requestKey: RequestKeys.initialize }),
+ isPending: selectors.requests.isPending(state, { requestKey: RequestKeys.initialize }),
+ hasError: selectors.requests.isFailed(state, { requestKey: RequestKeys.initialize }),
});
export const mapDispatchToProps = {
initializeApp: thunkActions.app.initialize,
- loadSelectionForReview: thunkActions.grading.loadSelectionForReview,
};
-export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ListView));
+export default connect(mapStateToProps, mapDispatchToProps)(ListView);
diff --git a/src/containers/ListView/index.test.jsx b/src/containers/ListView/index.test.jsx
index 7db3709..eca4530 100644
--- a/src/containers/ListView/index.test.jsx
+++ b/src/containers/ListView/index.test.jsx
@@ -1,18 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
-import {
- DataTable,
- MultiSelectDropdownFilter,
- TextFilter,
-} from '@edx/paragon';
-
import { selectors, thunkActions } from 'data/redux';
-import { gradingStatuses as statuses } from 'data/services/lms/constants';
+import { RequestKeys } from 'data/constants/requests';
-import StatusBadge from 'components/StatusBadge';
import { formatMessage } from 'testUtils';
-import messages from './messages';
import {
ListView,
mapStateToProps,
@@ -22,9 +14,19 @@ import {
jest.mock('components/StatusBadge', () => 'StatusBadge');
jest.mock('containers/ReviewModal', () => 'ReviewModal');
jest.mock('./ListViewBreadcrumb', () => 'ListViewBreadcrumb');
+jest.mock('./ListError', () => 'ListError');
+jest.mock('./SubmissionsTable', () => 'SubmissionsTable');
jest.mock('data/redux', () => ({
selectors: {
+ app: {
+ courseId: (...args) => ({ courseId: args }),
+ },
+ requests: {
+ isCompleted: (...args) => ({ isCompleted: args }),
+ isPending: (...args) => ({ isPending: args }),
+ isFailed: (...args) => ({ isFailed: args }),
+ },
submissions: {
listData: (...args) => ({ listData: args }),
},
@@ -33,225 +35,77 @@ jest.mock('data/redux', () => ({
app: {
initialize: (...args) => ({ initialize: args }),
},
- grading: {
- loadSelectionForReview: (...args) => ({ loadSelectionForReview: args }),
- },
},
}));
+jest.mock('@edx/paragon', () => ({
+ Container: 'Container',
+ Spinner: 'Spinner',
+}));
+
let el;
jest.useFakeTimers('modern');
describe('ListView component', () => {
describe('component', () => {
const props = {
- listData: [
- {
- username: 'username-1',
- dateSubmitted: 16131215154955,
- gradingStatus: statuses.ungraded,
- grade: {
- pointsEarned: 1,
- pointsPossible: 10,
- },
- },
- {
- username: 'username-2',
- dateSubmitted: 16131225154955,
- gradingStatus: statuses.graded,
- grade: {
- pointsEarned: 2,
- pointsPossible: 10,
- },
- },
- {
- username: 'username-3',
- dateSubmitted: 16131215250955,
- gradingStatus: statuses.inProgress,
- grade: {
- pointsEarned: 3,
- pointsPossible: 10,
- },
- },
- ],
+ courseId: 'test-course-id',
+ isLoaded: false,
+ isPending: false,
+ hasError: false,
};
beforeEach(() => {
props.initializeApp = jest.fn();
- props.loadSelectionForReview = jest.fn();
props.intl = { formatMessage };
});
describe('render tests', () => {
- const mockMethod = (methodName) => {
- el.instance()[methodName] = jest.fn().mockName(`this.${methodName}`);
- };
beforeEach(() => {
el = shallow();
});
describe('snapshots', () => {
- beforeEach(() => {
- mockMethod('handleViewAllResponsesClick');
- mockMethod('selectedBulkAction');
- mockMethod('formatDate');
- mockMethod('formatGrade');
- mockMethod('formatStatus');
- });
- test('snapshot: empty (no list data)', () => {
- el = shallow();
+ test('snapshot: loading', () => {
expect(el).toMatchSnapshot();
- expect(el.isEmptyRender()).toEqual(true);
});
- test('snapshot: happy path', () => {
+ test('snapshot: loaded', () => {
+ el.setProps({ isLoaded: true });
expect(el.instance().render()).toMatchSnapshot();
});
- });
- describe('DataTable', () => {
- let table;
- let tableProps;
- beforeEach(() => {
- table = el.find(DataTable);
- tableProps = table.props();
- });
- test.each([
- 'isFilterable',
- 'isSelectable',
- 'isSortable',
- 'isPaginated',
- ])('%s', key => expect(tableProps[key]).toEqual(true));
- test.each([
- ['numBreakoutFilters', 2],
- ['defaultColumnValues', { Filter: TextFilter }],
- ['itemCount', 3],
- ['initialState', { pageSize: 10, pageIndex: 0 }],
- ])('%s = %p', (key, value) => expect(tableProps[key]).toEqual(value));
- test('bulkActions linked to selectedBulkAction', () => {
- expect(tableProps.bulkActions).toEqual([el.instance().selectedBulkAction]);
- });
- describe('columns', () => {
- let columns;
- beforeEach(() => {
- columns = tableProps.columns;
- });
- test('username column', () => {
- expect(columns[0]).toEqual({
- Header: messages.username.defaultMessage,
- accessor: 'username',
- });
- });
- test('submission date column', () => {
- expect(columns[1]).toEqual({
- Header: messages.learnerSubmissionDate.defaultMessage,
- accessor: 'dateSubmitted',
- Cell: el.instance().formatDate,
- disableFilters: true,
- });
- });
- test('grade column', () => {
- expect(columns[2]).toEqual({
- Header: messages.grade.defaultMessage,
- accessor: 'score',
- Cell: el.instance().formatGrade,
- disableFilters: true,
- });
- });
- test('grading status column', () => {
- expect(columns[3]).toEqual({
- Header: messages.gradingStatus.defaultMessage,
- accessor: 'gradingStatus',
- Cell: el.instance().formatStatus,
- Filter: MultiSelectDropdownFilter,
- filter: 'includesValue',
- filterChoices: el.instance().gradeStatusOptions,
- });
- });
+ test('snapshot: error', () => {
+ el.setProps({ hasError: true });
+ expect(el.instance().render()).toMatchSnapshot();
});
});
});
describe('behavior', () => {
- describe('formatDate method', () => {
- it('returns the date in locale time string', () => {
- const fakeDate = 16131215154955;
- const fakeDateString = 'test-date-string';
- const mock = jest.spyOn(Date.prototype, 'toLocaleString').mockReturnValue(fakeDateString);
- expect(el.instance().formatDate({ value: fakeDate })).toEqual(fakeDateString);
- mock.mockRestore();
- });
- });
- describe('formatGrade method', () => {
- it('returns "-" if grade is null', () => {
- expect(el.instance().formatGrade({ value: null })).toEqual('-');
- });
- it('returns / if grade exists', () => {
- expect(
- el.instance().formatGrade({ value: { pointsEarned: 1, pointsPossible: 10 } }),
- ).toEqual('1/10');
- });
- });
- describe('formatStatus method', () => {
- it('returns a StatusBadge with the given status', () => {
- const status = 'graded';
- expect(el.instance().formatStatus({ value: 'graded' })).toEqual(
- ,
- );
- });
- });
- describe('handleViewAllResponsesClick', () => {
- it('calls loadSelectionForReview with submissionUUID from all rows if there are no selectedRows', () => {
- const data = {
- selectedRows: [
- ],
- tableInstance: {
- rows: [
- { original: { submissionUUID: '123' } },
- { original: { submissionUUID: '456' } },
- { original: { submissionUUID: '789' } },
- ],
- },
- };
- el.instance().handleViewAllResponsesClick(data);
- expect(el.instance().props.loadSelectionForReview).toHaveBeenCalledWith(['123', '456', '789']);
- });
- it('calls loadSelectionForReview with submissionUUID from selected rows if there are any', () => {
- const data = {
- selectedRows: [
- { original: { submissionUUID: '123' } },
- { original: { submissionUUID: '456' } },
- { original: { submissionUUID: '789' } },
- ],
- };
- el.instance().handleViewAllResponsesClick(data);
- expect(
- el.instance().props.loadSelectionForReview,
- ).toHaveBeenCalledWith(['123', '456', '789']);
- });
- });
- describe('selectedBulkAction', () => {
- it('includes selection length and triggers handleViewAllResponsesClick', () => {
- const rows = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
- const action = el.instance().selectedBulkAction(rows);
- expect(action.buttonText).toEqual(expect.stringContaining(rows.length.toString()));
- expect(action.handleClick).toEqual(el.instance().handleViewAllResponsesClick);
- });
+ it('calls initializeApp on load', () => {
+ el = shallow();
+ expect(props.initializeApp).toHaveBeenCalled();
});
});
});
describe('mapStateToProps', () => {
let mapped;
const testState = { some: 'test-state' };
+ const requestKey = RequestKeys.initialize;
beforeEach(() => {
mapped = mapStateToProps(testState);
});
- test('listData loads from submissions.listData', () => {
- expect(mapped.listData).toEqual(selectors.submissions.listData(testState));
+ test('courseId loads from app.courseId', () => {
+ expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
+ });
+ test('isLoaded loads from requests.isCompleted', () => {
+ expect(mapped.isLoaded).toEqual(selectors.requests.isCompleted(testState, { requestKey }));
+ });
+ test('isPending loads from requests.isPending', () => {
+ expect(mapped.isPending).toEqual(selectors.requests.isPending(testState, { requestKey }));
+ });
+ test('hasError loads from requests.isFailed', () => {
+ expect(mapped.hasError).toEqual(selectors.requests.isFailed(testState, { requestKey }));
});
});
describe('mapDispatchToProps', () => {
it('loads initializeApp from thunkActions.app.initialize', () => {
expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize);
});
- it('loads loadSelectionForReview from thunkActions.grading.loadSelectionForReview', () => {
- expect(
- mapDispatchToProps.loadSelectionForReview,
- ).toEqual(thunkActions.grading.loadSelectionForReview);
- });
});
});
diff --git a/src/containers/ListView/messages.js b/src/containers/ListView/messages.js
index 953306e..71c9f33 100644
--- a/src/containers/ListView/messages.js
+++ b/src/containers/ListView/messages.js
@@ -51,6 +51,31 @@ const messages = defineMessages({
defaultMessage: 'Grading status',
description: 'Grading status table column header for submission list view',
},
+ loadErrorHeading: {
+ id: 'ora-grading.ListView.loadErrorHeading',
+ defaultMessage: 'Error loading submissions',
+ description: 'Initialization failure alert header',
+ },
+ loadErrorMessage: {
+ id: 'ora-grading.ListView.loadErrorMessage1',
+ defaultMessage: 'An error occurred while loading the submissions for this response. Try reloading the page or going {backToResponses}.',
+ description: 'Initialization failure alert message line 2',
+ },
+ backToResponsesLowercase: {
+ id: 'ora-grading.ListView.backToResponsesLowercase',
+ defaultMessage: 'back to all Open Responses',
+ description: 'lowercase string for link to list of all open responses in lms',
+ },
+ reloadSubmissions: {
+ id: 'ora-grading.ListView.reloadSubmissions',
+ defaultMessage: 'Reload submissions',
+ description: 'Reload button text in case of network failure',
+ },
+ loadingResponses: {
+ id: 'ora-grading.ListView.loadingResponses',
+ defaultMessage: 'Loading responses',
+ description: 'loading text for submission response list',
+ },
});
export default messages;
diff --git a/src/containers/ReviewActions/__snapshots__/index.test.jsx.snap b/src/containers/ReviewActions/__snapshots__/index.test.jsx.snap
index a2a131b..2331a6e 100644
--- a/src/containers/ReviewActions/__snapshots__/index.test.jsx.snap
+++ b/src/containers/ReviewActions/__snapshots__/index.test.jsx.snap
@@ -53,7 +53,49 @@ exports[`ReviewActions component component snapshot: do not show rubric 1`] = `
`;
-exports[`ReviewActions component component snapshot: show rubric, no points 1`] = `
+exports[`ReviewActions component component snapshot: loading 1`] = `
+
+
+
+
+ test-username
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`ReviewActions component component snapshot: show rubric, no score 1`] = `
(
({
+ allowNavigation: selectors.requests.allowNavigation(state),
activeIndex: selectors.grading.activeIndex(state),
hasNextSubmission: selectors.grading.next.doesExist(state),
hasPrevSubmission: selectors.grading.prev.doesExist(state),
diff --git a/src/containers/ReviewActions/components/SubmissionNavigation.test.jsx b/src/containers/ReviewActions/components/SubmissionNavigation.test.jsx
index 48a37cd..bac80d5 100644
--- a/src/containers/ReviewActions/components/SubmissionNavigation.test.jsx
+++ b/src/containers/ReviewActions/components/SubmissionNavigation.test.jsx
@@ -21,6 +21,9 @@ jest.mock('data/redux/grading/selectors', () => ({
activeIndex: (state) => ({ activeIndex: state }),
selectionLength: (state) => ({ selectionlength: state }),
}));
+jest.mock('data/redux/requests/selectors', () => ({
+ allowNavigation: (state) => ({ allowNavigation: state }),
+}));
describe('SubmissionNavigation component', () => {
describe('component', () => {
diff --git a/src/containers/ReviewActions/components/__snapshots__/SubmissionNavigation.test.jsx.snap b/src/containers/ReviewActions/components/__snapshots__/SubmissionNavigation.test.jsx.snap
index a95bc2a..01865ad 100644
--- a/src/containers/ReviewActions/components/__snapshots__/SubmissionNavigation.test.jsx.snap
+++ b/src/containers/ReviewActions/components/__snapshots__/SubmissionNavigation.test.jsx.snap
@@ -7,7 +7,7 @@ exports[`SubmissionNavigation component component snapshot: no next submission (
(
{username}
-
+ { gradingStatus && (
+
+ )}
{pointsEarned && (
-
-
+ {isLoaded && (
+ <>
+
+
+ >
+ )}
);
+ReviewActions.defaultProps = {
+ isLoaded: false,
+ gradingStatus: null,
+};
ReviewActions.propTypes = {
- gradingStatus: PropTypes.string.isRequired,
+ gradingStatus: PropTypes.string,
username: PropTypes.string.isRequired,
- points: PropTypes.shape({
+ score: PropTypes.shape({
pointsEarned: PropTypes.number,
pointsPossible: PropTypes.number,
}).isRequired,
showRubric: PropTypes.bool.isRequired,
toggleShowRubric: PropTypes.func.isRequired,
+ isLoaded: PropTypes.bool,
};
export const mapStateToProps = (state) => ({
username: selectors.grading.selected.username(state),
gradingStatus: selectors.grading.selected.gradingStatus(state),
- points: selectors.grading.selected.points(state),
+ score: selectors.grading.selected.score(state),
showRubric: selectors.app.showRubric(state),
+ isLoaded: selectors.requests.isCompleted(state, { requestKey: RequestKeys.fetchSubmission }),
});
export const mapDispatchToProps = {
diff --git a/src/containers/ReviewActions/index.test.jsx b/src/containers/ReviewActions/index.test.jsx
index a5bd05d..b2e8023 100644
--- a/src/containers/ReviewActions/index.test.jsx
+++ b/src/containers/ReviewActions/index.test.jsx
@@ -2,6 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { actions, selectors } from 'data/redux';
+import { RequestKeys } from 'data/constants/requests';
import { ReviewActions, mapStateToProps, mapDispatchToProps } from '.';
@@ -11,10 +12,13 @@ jest.mock('data/redux/app/selectors', () => ({
jest.mock('data/redux/grading/selectors', () => ({
selected: {
gradingStatus: (state) => ({ gradingStatus: state }),
- points: (state) => ({ points: state }),
+ score: (state) => ({ score: state }),
username: (state) => ({ username: state }),
},
}));
+jest.mock('data/redux/requests/selectors', () => ({
+ isCompleted: (state) => ({ isCompleted: state }),
+}));
jest.mock('components/StatusBadge', () => 'StatusBadge');
jest.mock('./components/StartGradingButton', () => 'StartGradingButton');
jest.mock('./components/SubmissionNavigation', () => 'SubmissionNavigation');
@@ -25,16 +29,19 @@ describe('ReviewActions component', () => {
gradingStatus: 'grading-status',
username: 'test-username',
showRubric: false,
- points: { pointsEarned: 3, pointsPossible: 10 },
+ score: { pointsEarned: 3, pointsPossible: 10 },
};
beforeEach(() => {
props.toggleShowRubric = jest.fn().mockName('this.props.toggleShowRubric');
});
- test('snapshot: do not show rubric', () => {
+ test('snapshot: loading', () => {
expect(shallow()).toMatchSnapshot();
});
- test('snapshot: show rubric, no points', () => {
- expect(shallow()).toMatchSnapshot();
+ test('snapshot: do not show rubric', () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+ test('snapshot: show rubric, no score', () => {
+ expect(shallow()).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
@@ -43,14 +50,18 @@ describe('ReviewActions component', () => {
beforeEach(() => {
mapped = mapStateToProps(testState);
});
+ test('isLoaded loads from requests.isCompleted for fetchSubmissions', () => {
+ const requestKey = RequestKeys.fetchSubmission;
+ expect(mapped.isLoaded).toEqual(selectors.requests.isCompleted(testState, { requestKey }));
+ });
test('username loads from grading.selected.username', () => {
expect(mapped.username).toEqual(selectors.grading.selected.username(testState));
});
test('gradingStatus loads from grading.selected.gradingStatus', () => {
expect(mapped.gradingStatus).toEqual(selectors.grading.selected.gradingStatus(testState));
});
- test('points loads from grading.selected.points', () => {
- expect(mapped.points).toEqual(selectors.grading.selected.points(testState));
+ test('score loads from grading.selected.score', () => {
+ expect(mapped.score).toEqual(selectors.grading.selected.score(testState));
});
test('showRubric loads from app.showRubric', () => {
expect(mapped.showRubric).toEqual(selectors.app.showRubric(testState));
diff --git a/src/containers/ReviewModal/ReviewContent.jsx b/src/containers/ReviewModal/ReviewContent.jsx
new file mode 100644
index 0000000..6f2a8c2
--- /dev/null
+++ b/src/containers/ReviewModal/ReviewContent.jsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import { Col, Row } from '@edx/paragon';
+
+import { selectors } from 'data/redux';
+
+import ResponseDisplay from 'containers/ResponseDisplay';
+import Rubric from 'containers/Rubric';
+
+/**
+ *
+ */
+export const ReviewContent = ({ showRubric }) => (
+
+
+
+ { showRubric && }
+
+
+);
+ReviewContent.defaultProps = {
+ showRubric: false,
+};
+ReviewContent.propTypes = {
+ showRubric: PropTypes.bool,
+};
+
+export const mapStateToProps = (state) => ({
+ showRubric: selectors.app.showRubric(state),
+});
+
+export const mapDispatchToProps = {};
+
+export default connect(mapStateToProps, mapDispatchToProps)(ReviewContent);
diff --git a/src/containers/ReviewModal/ReviewContent.test.jsx b/src/containers/ReviewModal/ReviewContent.test.jsx
new file mode 100644
index 0000000..ef1e73d
--- /dev/null
+++ b/src/containers/ReviewModal/ReviewContent.test.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { selectors } from 'data/redux';
+import {
+ ReviewContent,
+ mapStateToProps,
+} from './ReviewContent';
+
+jest.mock('data/redux', () => ({
+ selectors: {
+ app: {
+ showRubric: (...args) => ({ showRubric: args }),
+ },
+ },
+}));
+jest.mock('containers/ResponseDisplay', () => 'ResponseDisplay');
+jest.mock('containers/Rubric', () => 'Rubric');
+
+jest.useFakeTimers('modern');
+
+describe('ReviewContent component', () => {
+ describe('component', () => {
+ describe('render tests', () => {
+ test('snapshot (show rubric)', () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+ test('snapshot (hide rubric)', () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+ });
+ });
+ describe('mapStateToProps', () => {
+ let mapped;
+ const testState = { some: 'test-state' };
+ beforeEach(() => {
+ mapped = mapStateToProps(testState);
+ });
+ test('showRubric loads from app.showRubric', () => {
+ expect(mapped.showRubric).toEqual(selectors.app.showRubric(testState));
+ });
+ });
+});
diff --git a/src/containers/ReviewModal/ReviewError.jsx b/src/containers/ReviewModal/ReviewError.jsx
new file mode 100644
index 0000000..d3d5503
--- /dev/null
+++ b/src/containers/ReviewModal/ReviewError.jsx
@@ -0,0 +1,51 @@
+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';
+
+/**
+ *
+ */
+export const ReviewError = ({ reload }) => (
+
+
+ ,
+ ]}
+ >
+
+
+
+
+
+
+
+);
+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);
diff --git a/src/containers/ReviewModal/ReviewError.test.jsx b/src/containers/ReviewModal/ReviewError.test.jsx
new file mode 100644
index 0000000..461fcd5
--- /dev/null
+++ b/src/containers/ReviewModal/ReviewError.test.jsx
@@ -0,0 +1,34 @@
+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();
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ it('loads reload from thunkActions.grading.reloadSubmission', () => {
+ expect(mapDispatchToProps.reload).toEqual(thunkActions.grading.loadSubmission);
+ });
+ });
+});
diff --git a/src/containers/ReviewModal/__snapshots__/ReviewContent.test.jsx.snap b/src/containers/ReviewModal/__snapshots__/ReviewContent.test.jsx.snap
new file mode 100644
index 0000000..86b6e90
--- /dev/null
+++ b/src/containers/ReviewModal/__snapshots__/ReviewContent.test.jsx.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ReviewContent component component render tests snapshot (hide rubric) 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`ReviewContent component component render tests snapshot (show rubric) 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/src/containers/ReviewModal/__snapshots__/ReviewError.test.jsx.snap b/src/containers/ReviewModal/__snapshots__/ReviewError.test.jsx.snap
new file mode 100644
index 0000000..336df69
--- /dev/null
+++ b/src/containers/ReviewModal/__snapshots__/ReviewError.test.jsx.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ReviewError component component render tests snapshot 1`] = `
+
+
+ ,
+ ]
+ }
+ variant="danger"
+>
+
+
+
+
+
+
+
+`;
diff --git a/src/containers/ReviewModal/__snapshots__/index.test.jsx.snap b/src/containers/ReviewModal/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000..a7bf68e
--- /dev/null
+++ b/src/containers/ReviewModal/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,74 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ReviewModal component component snapshots closed 1`] = `
+}
+ className="review-modal"
+ isOpen={false}
+ modalBodyClassName="review-modal-body"
+ onClose={[MockFunction this.onClose]}
+ title="test-ora-name"
+>
+
+
+`;
+
+exports[`ReviewModal component component snapshots error 1`] = `
+}
+ className="review-modal"
+ isOpen={true}
+ modalBodyClassName="review-modal-body"
+ onClose={[MockFunction this.onClose]}
+ title="test-ora-name"
+>
+
+
+
+
+`;
+
+exports[`ReviewModal component component snapshots loading 1`] = `
+}
+ className="review-modal"
+ isOpen={true}
+ modalBodyClassName="review-modal-body"
+ onClose={[MockFunction this.onClose]}
+ title="test-ora-name"
+>
+
+
+
+`;
+
+exports[`ReviewModal component component snapshots success 1`] = `
+}
+ className="review-modal"
+ isOpen={true}
+ modalBodyClassName="review-modal-body"
+ onClose={[MockFunction this.onClose]}
+ title="test-ora-name"
+>
+
+
+
+
+`;
diff --git a/src/containers/ReviewModal/index.jsx b/src/containers/ReviewModal/index.jsx
index 53fed41..192b162 100644
--- a/src/containers/ReviewModal/index.jsx
+++ b/src/containers/ReviewModal/index.jsx
@@ -2,18 +2,16 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import {
- FullscreenModal,
- Row,
- Col,
-} from '@edx/paragon';
+import { FullscreenModal } from '@edx/paragon';
import { selectors, actions } from 'data/redux';
+import { RequestKeys } from 'data/constants/requests';
-import ResponseDisplay from 'containers/ResponseDisplay';
-import Rubric from 'containers/Rubric';
-
+import LoadingMessage from 'components/LoadingMessage';
import ReviewActions from 'containers/ReviewActions';
+import ReviewError from './ReviewError';
+import ReviewContent from './ReviewContent';
+import messages from './messages';
import './ReviewModal.scss';
@@ -30,25 +28,29 @@ export class ReviewModal extends React.Component {
this.props.setShowReview(false);
}
+ get isLoading() {
+ return !(this.props.hasError || this.props.isLoaded);
+ }
+
render() {
- if (this.props.response === null) {
- return null;
- }
+ const { isOpen, isLoaded, hasError } = this.props;
return (
}
onClose={this.onClose}
className="review-modal"
modalBodyClassName="review-modal-body"
>
-
-
-
- { this.props.showRubric && }
-
-
+ {isOpen && (
+ <>
+ {isLoaded && }
+ {hasError && }
+ >
+ )}
+ {/* even if the modal is closed, in case we want to add transitions later */}
+ {!(isLoaded || hasError) && }
);
}
@@ -63,14 +65,16 @@ ReviewModal.propTypes = {
text: PropTypes.node,
}),
setShowReview: PropTypes.func.isRequired,
- showRubric: PropTypes.bool.isRequired,
+ isLoaded: PropTypes.bool.isRequired,
+ hasError: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
isOpen: selectors.app.showReview(state),
oraName: selectors.app.ora.name(state),
response: selectors.grading.selected.response(state),
- showRubric: selectors.app.showRubric(state),
+ isLoaded: selectors.requests.isCompleted(state, { requestKey: RequestKeys.fetchSubmission }),
+ hasError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchSubmission }),
});
export const mapDispatchToProps = {
diff --git a/src/containers/ReviewModal/index.test.jsx b/src/containers/ReviewModal/index.test.jsx
new file mode 100644
index 0000000..1c3f70f
--- /dev/null
+++ b/src/containers/ReviewModal/index.test.jsx
@@ -0,0 +1,108 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { selectors, actions } from 'data/redux';
+import { RequestKeys } from 'data/constants/requests';
+
+import {
+ ReviewModal,
+ mapStateToProps,
+ mapDispatchToProps,
+} from '.';
+
+let el;
+jest.useFakeTimers('modern');
+
+jest.mock('data/redux', () => ({
+ selectors: {
+ app: {
+ ora: { name: (...args) => ({ oraName: args }) },
+ showReview: (...args) => ({ showReview: args }),
+ },
+ grading: {
+ selected: { response: (...args) => ({ selectedResponse: args }) },
+ },
+ requests: {
+ isCompleted: (...args) => ({ isCompleted: args }),
+ isFailed: (...args) => ({ isFailed: args }),
+ },
+ },
+ actions: {
+ app: {
+ setShowReview: jest.fn(),
+ },
+ },
+}));
+
+jest.mock('containers/ReviewActions', () => 'ReviewActions');
+jest.mock('./ReviewError', () => 'ReviewError');
+jest.mock('./ReviewContent', () => 'ReviewContent');
+jest.mock('components/LoadingMessage', () => 'LoadingMessage');
+
+const requestKey = RequestKeys.fetchSubmission;
+
+describe('ReviewModal component', () => {
+ const props = {
+ oraName: 'test-ora-name',
+ isOpen: false,
+ response: { text: (some text
) },
+ showRubric: false,
+ isLoaded: false,
+ hasError: false,
+ };
+ describe('component', () => {
+ beforeEach(() => {
+ props.setShowReview = jest.fn();
+ });
+ describe('snapshots', () => {
+ let render;
+ beforeEach(() => {
+ el = shallow();
+ el.instance().onClose = jest.fn().mockName('this.onClose');
+ render = () => el.instance().render();
+ });
+ test('closed', () => {
+ expect(render()).toMatchSnapshot();
+ });
+ test('loading', () => {
+ el.setProps({ isOpen: true });
+ expect(render()).toMatchSnapshot();
+ });
+ test('error', () => {
+ el.setProps({ isOpen: true, hasError: true });
+ expect(render()).toMatchSnapshot();
+ });
+ test('success', () => {
+ el.setProps({ isOpen: true, isLoaded: true });
+ expect(render()).toMatchSnapshot();
+ });
+ });
+ });
+ describe('mapStateToProps', () => {
+ let mapped;
+ const testState = { some: 'test-state' };
+ beforeEach(() => {
+ mapped = mapStateToProps(testState);
+ });
+ test('oraName loads from app.ora.name', () => {
+ expect(mapped.oraName).toEqual(selectors.app.ora.name(testState));
+ });
+ test('isOpen loads from app.showReview', () => {
+ expect(mapped.isOpen).toEqual(selectors.app.showReview(testState));
+ });
+ test('response loads from grading.selected.response', () => {
+ expect(mapped.response).toEqual(selectors.grading.selected.response(testState));
+ });
+ 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 }));
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ it('loads setShowReview from thunkActions.app.setShowReview', () => {
+ expect(mapDispatchToProps.setShowReview).toEqual(actions.app.setShowReview);
+ });
+ });
+});
diff --git a/src/containers/ReviewModal/messages.js b/src/containers/ReviewModal/messages.js
new file mode 100644
index 0000000..ddc8a99
--- /dev/null
+++ b/src/containers/ReviewModal/messages.js
@@ -0,0 +1,26 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+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',
+ },
+ loadingResponse: {
+ id: 'ora-grading.ReviewModal.loadingResponse',
+ defaultMessage: 'Loading response',
+ description: 'loading text for submission response review screen',
+ },
+});
+
+export default messages;
diff --git a/src/data/redux/grading/reducer.js b/src/data/redux/grading/reducer.js
index dad03df..b6279f8 100644
--- a/src/data/redux/grading/reducer.js
+++ b/src/data/redux/grading/reducer.js
@@ -43,6 +43,7 @@ const initialState = {
activeIndex: null,
current: {
/**
+ * submissionUUID: '',
* gradeStatus: '',
* response: {
* text: '',
@@ -115,13 +116,6 @@ export const updateCriterion = (state, orderNum, data) => {
});
};
-const loadCurrentFromNeighbor = (neighbor, { lockStatus, gradeStatus, submissionUUID }) => ({
- response: neighbor.response,
- lockStatus,
- gradeStatus,
- submissionUUID,
-});
-
// eslint-disable-next-line no-unused-vars
const grading = createSlice({
name: 'grading',
@@ -130,31 +124,20 @@ const grading = createSlice({
loadSubmission: (state, { payload }) => ({
...state,
current: { ...payload },
- activeIndex: 0,
+ gradeData: {
+ ...state.gradeData,
+ [payload.submissionUUID]: payload.gradeData,
+ },
}),
- preloadNext: (state, { payload }) => ({ ...state, next: payload }),
- preloadPrev: (state, { payload }) => ({ ...state, prev: payload }),
- loadNext: (state, { payload }) => ({
+ loadNext: (state) => ({
...state,
- prev: { response: state.current.response },
- current: loadCurrentFromNeighbor(state.next, payload),
+ current: {},
activeIndex: state.activeIndex + 1,
- gradeData: {
- ...state.gradeData,
- [payload.submissionUUID]: payload.gradeData,
- },
- next: null,
}),
- loadPrev: (state, { payload }) => ({
+ loadPrev: (state) => ({
...state,
- next: { response: state.current.response },
- current: loadCurrentFromNeighbor(state.prev, payload),
- gradeData: {
- ...state.gradeData,
- [payload.submissionUUID]: payload.gradeData,
- },
+ current: {},
activeIndex: state.activeIndex - 1,
- prev: null,
}),
updateSelection: (state, { payload }) => ({
...state,
diff --git a/src/data/redux/grading/selectors.js b/src/data/redux/grading/selectors.js
index 736d966..070a577 100644
--- a/src/data/redux/grading/selectors.js
+++ b/src/data/redux/grading/selectors.js
@@ -120,7 +120,7 @@ selected.username = createSelector(
/**
* Returns the grade data for the selected submission
* @return {obj} grade data
- * { points, overallFeedback, criteria }
+ * { score, overallFeedback, criteria }
*/
selected.gradeData = createSelector(
[module.selected.submissionUUID, module.simpleSelectors.gradeData],
@@ -151,12 +151,12 @@ selected.criteriaGradeData = createSelector(
);
/**
- * Returns the points object associated with the grade
- * @return {obj} points object
+ * Returns the score object associated with the grade
+ * @return {obj} score object
*/
-selected.points = createSelector(
+selected.score = createSelector(
[module.selected.gradeData],
- (data) => ((data && data.points) ? data.points : {}),
+ (data) => ((data && data.score) ? data.score : {}),
);
/**
diff --git a/src/data/redux/requests/index.js b/src/data/redux/requests/index.js
index 27cac64..8abd5f9 100644
--- a/src/data/redux/requests/index.js
+++ b/src/data/redux/requests/index.js
@@ -1 +1,2 @@
export { actions, reducer } from './reducer';
+export { default as selectors } from './selectors';
diff --git a/src/data/redux/requests/selectors.js b/src/data/redux/requests/selectors.js
new file mode 100644
index 0000000..15f8266
--- /dev/null
+++ b/src/data/redux/requests/selectors.js
@@ -0,0 +1,29 @@
+import { StrictDict } from 'utils';
+import { RequestStates } from 'data/constants/requests';
+import * as module from './selectors';
+
+export const requestStatus = (state, { requestKey }) => state.requests[requestKey];
+
+const statusSelector = (fn) => (state, { requestKey }) => fn(state.requests[requestKey]);
+
+export const isInactive = ({ status }) => status === RequestStates.inactive;
+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 data = (request) => request.data;
+
+export const allowNavigation = ({ requests }) => (
+ !Object.keys(requests).some(requestKey => module.isPending(requests[requestKey]))
+);
+
+export default StrictDict({
+ requestStatus,
+ allowNavigation,
+ isInactive: statusSelector(isInactive),
+ isPending: statusSelector(isPending),
+ isCompleted: statusSelector(isCompleted),
+ isFailed: statusSelector(isFailed),
+ error: statusSelector(error),
+ data: statusSelector(data),
+});
diff --git a/src/data/redux/thunkActions/grading.js b/src/data/redux/thunkActions/grading.js
index 6d3afdf..17b5937 100644
--- a/src/data/redux/thunkActions/grading.js
+++ b/src/data/redux/thunkActions/grading.js
@@ -1,74 +1,18 @@
import { StrictDict } from 'utils';
-import { RequestKeys } from 'data/constants/requests';
import { actions, selectors } from 'data/redux';
import * as module from './grading';
import requests from './requests';
-/**
- * Prefetch the "next" submission in the selected queue. Only fetches the response info.
- */
-export const prefetchNext = () => (dispatch, getState) => {
- dispatch(requests.fetchSubmissionResponse({
- requestKey: RequestKeys.prefetchNext,
- submissionUUID: selectors.grading.next.submissionUUID(getState()),
- onSuccess: (response) => {
- dispatch(actions.grading.preloadNext(response));
- },
- }));
-};
-
-/**
- * Prefetch the "previous" submission in the selected queue. Only fetches the response info.
- */
-export const prefetchPrev = () => (dispatch, getState) => {
- dispatch(requests.fetchSubmissionResponse({
- requestKey: RequestKeys.prefetchPrev,
- submissionUUID: selectors.grading.prev.submissionUUID(getState()),
- onSuccess: (response) => {
- dispatch(actions.grading.preloadPrev(response));
- },
- }));
-};
-
-/**
- * Fetch the target neighbor submission's status, start grading if in progress,
- * dispatches load action with the response (injecting submissionUUID). If hasNeighbor,
- * also dispatches the prefetchAction to pre-fetch the new neighbor's response.
- * @param {string} submissionUUID - target submission id
- * @param {action} loadAction - redux action/thunkAction to load the submission status
- * @param {bool} hasNeighbor - is there a new neighbor to be pre-fetched?
- * @param {action} prefetchAction - redux action/thunkAction to prefetch the new
- * neighbor's response.
- */
-export const fetchNeighbor = ({
- submissionUUID,
- loadAction,
- hasNeighbor,
- prefetchAction,
-}) => (dispatch) => {
- dispatch(requests.fetchSubmissionStatus({
- submissionUUID,
- onSuccess: (response) => {
- dispatch(loadAction({ ...response, submissionUUID }));
- if (hasNeighbor) { dispatch(prefetchAction()); }
- },
- }));
-};
-
/**
* Fetches the current status for the "next" submission in the selected queue,
* and calls loadNext with it to update the current selection index info.
* If the new index has a next submission available, preload its response.
*/
-export const loadNext = () => (dispatch, getState) => {
- dispatch(module.fetchNeighbor({
- loadAction: actions.grading.loadNext,
- hasNeighbor: selectors.grading.next.doesExist(getState()),
- prefetchAction: module.prefetchNext,
- submissionUUID: selectors.grading.next.submissionUUID(getState()),
- }));
+export const loadNext = () => (dispatch) => {
+ dispatch(actions.grading.loadNext());
+ dispatch(module.loadSubmission());
};
/**
@@ -76,13 +20,9 @@ export const loadNext = () => (dispatch, getState) => {
* and calls loadPrev with it to update the current selection index info.
* If the new index has a previous submission available, preload its response.
*/
-export const loadPrev = () => (dispatch, getState) => {
- dispatch(module.fetchNeighbor({
- loadAction: actions.grading.loadPrev,
- hasNeighbor: selectors.grading.prev.doesExist(getState()),
- prefetchAction: module.prefetchPrev,
- submissionUUID: selectors.grading.prev.submissionUUID(getState()),
- }));
+export const loadPrev = () => (dispatch) => {
+ dispatch(actions.grading.loadPrev());
+ dispatch(module.loadSubmission());
};
/**
@@ -91,22 +31,18 @@ export const loadPrev = () => (dispatch, getState) => {
* Then loads current selection and prefetches neighbors.
* @param {string[]} submissionUUIDs - ordered list of submissionUUIDs for selected submissions
*/
-export const loadSelectionForReview = (submissionUUIDs) => (dispatch, getState) => {
+export const loadSelectionForReview = (submissionUUIDs) => (dispatch) => {
+ dispatch(actions.grading.updateSelection(submissionUUIDs));
+ dispatch(actions.app.setShowReview(true));
+ dispatch(module.loadSubmission());
+};
+
+export const loadSubmission = () => (dispatch, getState) => {
+ const submissionUUID = selectors.grading.selected.submissionUUID(getState());
dispatch(requests.fetchSubmission({
- submissionUUID: submissionUUIDs[0],
+ submissionUUID,
onSuccess: (response) => {
- dispatch(actions.grading.updateSelection(submissionUUIDs));
- dispatch(actions.grading.loadSubmission({
- ...response,
- submissionUUID: submissionUUIDs[0],
- }));
- dispatch(actions.app.setShowReview(true));
- if (selectors.grading.next.doesExist(getState())) {
- dispatch(module.prefetchNext());
- }
- if (selectors.grading.prev.doesExist(getState())) {
- dispatch(module.prefetchPrev());
- }
+ dispatch(actions.grading.loadSubmission({ ...response, submissionUUID }));
},
}));
};
@@ -182,6 +118,7 @@ export default StrictDict({
loadPrev,
startGrading,
cancelGrading,
+ loadSubmission,
stopGrading,
submitGrade,
});
diff --git a/src/data/redux/thunkActions/grading.test.js b/src/data/redux/thunkActions/grading.test.js
index 63fac4d..a13199b 100644
--- a/src/data/redux/thunkActions/grading.test.js
+++ b/src/data/redux/thunkActions/grading.test.js
@@ -1,6 +1,3 @@
-import { RequestKeys } from 'data/constants/requests';
-import { gradingStatuses } from 'data/services/lms/constants';
-
import { actions, selectors } from 'data/redux';
import * as thunkActions from './grading';
@@ -18,11 +15,9 @@ jest.mock('data/redux/app/selectors', () => ({
jest.mock('data/redux/grading/selectors', () => ({
prev: {
- submissionUUID: (state) => ({ prevsubmissionUUID: state }),
doesExist: jest.fn((state) => ({ prevDoesExist: state })),
},
next: {
- submissionUUID: (state) => ({ prevsubmissionUUID: state }),
doesExist: jest.fn((state) => ({ nextDoesExist: state })),
},
selected: {
@@ -33,8 +28,9 @@ jest.mock('data/redux/grading/selectors', () => ({
describe('grading thunkActions', () => {
const testState = { some: 'testy-state' };
- const submissionUUID = 'test-submission-id';
+ const selectedUUID = selectors.grading.selected.submissionUUID(testState);
const response = 'test-response';
+ const objResponse = { response };
let dispatch;
let dispatched;
let actionArgs;
@@ -49,210 +45,71 @@ describe('grading thunkActions', () => {
dispatch = jest.fn((action) => ({ dispatch: action }));
});
- describe('prefetchNext', () => {
+ describe('loadSubmission', () => {
beforeEach(() => {
- getDispatched(thunkActions.prefetchNext());
- actionArgs = dispatched.fetchSubmissionResponse;
+ getDispatched(thunkActions.loadSubmission());
+ actionArgs = dispatched.fetchSubmission;
});
- it('dispatches fetchSubmissionResponse with prefetchNext key and nextsubmissionUUID', () => {
+ test('dispatches fetchSubmission', () => {
expect(actionArgs).not.toEqual(undefined);
- expect(actionArgs.requestKey).toEqual(RequestKeys.prefetchNext);
- expect(actionArgs.submissionUUID).toEqual(selectors.grading.prev.submissionUUID(testState));
});
- describe('on success', () => {
- test('dispatches preloadNext', () => {
+ describe('fetchSubmissionArgs', () => {
+ test('submissionUUID: selectors.grading.selected.submisssionUUID', () => {
+ expect(actionArgs.submissionUUID).toEqual(
+ selectedUUID,
+ );
+ });
+ test('onSuccess: dispatches loadSubmission with response and submissionUUID', () => {
dispatch.mockClear();
- actionArgs.onSuccess(response);
- expect(dispatch.mock.calls).toEqual([
- [actions.grading.preloadNext(response)],
- ]);
+ actionArgs.onSuccess(objResponse);
+ expect(dispatch).toHaveBeenCalledWith(
+ actions.grading.loadSubmission({ ...objResponse, submissionUUID: selectedUUID }),
+ );
});
});
});
- describe('prefetchPrev', () => {
- beforeEach(() => {
- getDispatched(thunkActions.prefetchPrev());
- actionArgs = dispatched.fetchSubmissionResponse;
- });
- it('dispatches fetchSubmissionResponse with prefetchPrev key and next submissionUUID', () => {
- expect(actionArgs).not.toEqual(undefined);
- expect(actionArgs.requestKey).toEqual(RequestKeys.prefetchPrev);
- expect(actionArgs.submissionUUID).toEqual(selectors.grading.next.submissionUUID(testState));
- });
- describe('on success', () => {
- test('dispatches preloadPrev', () => {
- dispatch.mockClear();
- actionArgs.onSuccess(response);
- expect(dispatch.mock.calls).toEqual([
- [actions.grading.preloadPrev(response)],
- ]);
- });
- });
- });
-
- describe('fetchNeighbor', () => {
- const loadAction = actions.grading.loadNext;
- const prefetchAction = actions.grading.loadNext;
-
- const submitAction = (hasNeighbor) => {
- getDispatched(thunkActions.fetchNeighbor({
- submissionUUID,
- loadAction,
- hasNeighbor,
- prefetchAction,
- }));
- actionArgs = dispatched.fetchSubmissionStatus;
- };
-
- it('calls fetchSubmissionStatus with submissionUUID', () => {
- submitAction(false);
- expect(actionArgs).not.toEqual(undefined);
- expect(actionArgs.submissionUUID).toEqual(submissionUUID);
- });
- describe('onSuccess', () => {
- it('dispatches startGrading if lockStatus is in progress', () => {
- submitAction(false);
- dispatch.mockClear();
- actionArgs.onSuccess({ lockStatus: gradingStatuses.inProgress });
- });
- it('dispatches stopGrading if lockStatus is not in progress', () => {
- submitAction(false);
- dispatch.mockClear();
- actionArgs.onSuccess({ lockStatus: 'other status' });
- });
- });
- });
-
- describe('fetchNeigbor inheritors', () => {
- let fetchNeighbor;
+ describe('loadSubmission inheritors', () => {
+ let loadSubmission;
beforeAll(() => {
- fetchNeighbor = thunkActions.fetchNeighbor;
- thunkActions.fetchNeighbor = args => ({ fetchNeighbor: args });
+ loadSubmission = thunkActions.loadSubmission;
+ thunkActions.loadSubmission = args => ({ loadSubmission: args });
});
afterAll(() => {
- thunkActions.fetchNeighbor = fetchNeighbor;
+ thunkActions.loadSubmission = loadSubmission;
});
describe('loadNext', () => {
- beforeEach(() => {
- getDispatched(thunkActions.loadNext());
- actionArgs = dispatched.fetchNeighbor;
- });
- test('dispatches fetchNeighbor', () => {
- expect(actionArgs).not.toEqual(undefined);
- });
- describe('fetchNeighbor args', () => {
- const selGroup = selectors.grading.next;
- test('loadAction: actions.grading.loadNext', () => {
- expect(actionArgs.loadAction).toEqual(actions.grading.loadNext);
- });
- test('prefetchAction: module.prefetchNext', () => {
- expect(actionArgs.prefetchAction).toEqual(thunkActions.prefetchNext);
- });
- test('hasNeighbor: selectors.grading.next.doesExist', () => {
- expect(actionArgs.hasNeighbor).toEqual(selGroup.doesExist(testState));
- });
- test('submissionUUID: selectors.grading.next.submissionUUID', () => {
- expect(actionArgs.submissionUUID).toEqual(selGroup.submissionUUID(testState));
- });
+ test('dispatches actions.grading.loadNext and then loadSubmission', () => {
+ thunkActions.loadNext()(dispatch, getState);
+ expect(dispatch.mock.calls).toEqual([
+ [actions.grading.loadNext()],
+ [thunkActions.loadSubmission()],
+ ]);
});
});
describe('loadPrev', () => {
- beforeEach(() => {
- getDispatched(thunkActions.loadPrev());
- actionArgs = dispatched.fetchNeighbor;
- });
- test('dispatches fetchNeighbor', () => {
- expect(actionArgs).not.toEqual(undefined);
- });
- describe('fetchNeighbor args', () => {
- const selGroup = selectors.grading.prev;
- test('loadAction: actions.grading.loadPrev', () => {
- expect(actionArgs.loadAction).toEqual(actions.grading.loadPrev);
- });
- test('prefetchAction: module.prefetchPrev', () => {
- expect(actionArgs.prefetchAction).toEqual(thunkActions.prefetchPrev);
- });
- test('hasNeighbor: selectors.grading.prev.doesExist', () => {
- expect(actionArgs.hasNeighbor).toEqual(selGroup.doesExist(testState));
- });
- test('submissionUUID: selectors.grading.prev.submissionUUID', () => {
- expect(actionArgs.submissionUUID).toEqual(selGroup.submissionUUID(testState));
- });
+ test('dispatches actions.grading.loadPrev and then loadSubmission', () => {
+ thunkActions.loadPrev()(dispatch, getState);
+ expect(dispatch.mock.calls).toEqual([
+ [actions.grading.loadPrev()],
+ [thunkActions.loadSubmission()],
+ ]);
});
});
- });
-
- describe('loadSelectionForReview', () => {
- const submissionUUIDs = [
- 'submission-id-0',
- 'submission-id-1',
- 'submission-id-2',
- 'submission-id-3',
- ];
- let prefetchPrev;
- let prefetchNext;
- beforeAll(() => {
- prefetchNext = thunkActions.prefetchNext;
- prefetchPrev = thunkActions.prefetchPrev;
- thunkActions.prefetchNext = () => 'prefetch next';
- thunkActions.prefetchPrev = () => 'prefetch prev';
- });
- afterAll(() => {
- thunkActions.prefetchNext = prefetchNext;
- thunkActions.prefetchPrev = prefetchPrev;
- });
- beforeEach(() => {
- getDispatched(thunkActions.loadSelectionForReview(submissionUUIDs));
- actionArgs = dispatched.fetchSubmission;
- });
- it('dispatches fetchSubmission with first submissionUUID', () => {
- expect(actionArgs).not.toEqual(undefined);
- expect(actionArgs.submissionUUID).toEqual(submissionUUIDs[0]);
- });
- describe('onSuccess', () => {
- beforeEach(() => {
- dispatch.mockClear();
- actionArgs.onSuccess(response);
- });
- it('dispatches updateSelection with passed submissionUUIDs', () => {
- expect(dispatch.mock.calls).toContainEqual(
+ describe('loadSelectionForReview', () => {
+ const submissionUUIDs = [
+ 'submission-id-0',
+ 'submission-id-1',
+ 'submission-id-2',
+ 'submission-id-3',
+ ];
+ test('dispatches actions.grading.updateSelection, actions.app.setShowReview(true), and then loadSubmission', () => {
+ thunkActions.loadSelectionForReview(submissionUUIDs)(dispatch, getState);
+ expect(dispatch.mock.calls).toEqual([
[actions.grading.updateSelection(submissionUUIDs)],
- );
- });
- it('dispatches actions.grading.loadSubmission with response and first submission id', () => {
- expect(dispatch.mock.calls).toContainEqual(
- [actions.grading.loadSubmission({ ...response, submissionUUID: submissionUUIDs[0] })],
- );
- });
- it('dispatches app setShowReview(true)', () => {
- expect(dispatch.mock.calls).toContainEqual(
[actions.app.setShowReview(true)],
- );
- });
- it('dispatches prefetchNext iff selectors.grading.next.doesExist', () => {
- // default configured to be truthy
- expect(dispatch.mock.calls).toContainEqual(
- [thunkActions.prefetchNext()],
- );
- selectors.grading.next.doesExist.mockReturnValue(false);
- dispatch.mockClear();
- actionArgs.onSuccess(response);
- expect(dispatch.mock.calls).not.toContainEqual(
- [thunkActions.prefetchNext()],
- );
- });
- it('dispatches prefetchPrev iff selectors.grading.prev.doesExist', () => {
- // default configured to be truthy
- expect(dispatch.mock.calls).toContainEqual(
- [thunkActions.prefetchPrev()],
- );
- selectors.grading.prev.doesExist.mockReturnValue(false);
- dispatch.mockClear();
- actionArgs.onSuccess(response);
- expect(dispatch.mock.calls).not.toContainEqual(
- [thunkActions.prefetchPrev()],
- );
+ [thunkActions.loadSubmission()],
+ ]);
});
});
});
diff --git a/src/setupTest.js b/src/setupTest.js
index de3d8b3..426a51e 100755
--- a/src/setupTest.js
+++ b/src/setupTest.js
@@ -21,6 +21,9 @@ jest.mock('@edx/frontend-platform/i18n', () => {
});
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
+ Alert: {
+ Heading: 'Alert.Heading',
+ },
AlertModal: 'AlertModal',
ActionRow: 'ActionRow',
Badge: 'Badge',
@@ -28,6 +31,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Card: {
Body: 'Card.Body',
},
+ Col: 'Col',
Collapsible: {
Advanced: 'Collapsible.Advanced',
Body: 'Collapsible.Body',
@@ -57,6 +61,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
RadioSet: 'Form.RadioSet',
},
FormControlFeedback: 'FormControlFeedback',
+ FullscreenModal: 'FullscreenModal',
Hyperlink: 'Hyperlink',
Icon: 'Icon',
IconButton: 'IconButton',
@@ -65,6 +70,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Popover: {
Content: 'Popover.Content',
},
+ Row: 'Row',
TextFilter: 'TextFilter',
}));
diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx
index b041436..0f9d65e 100644
--- a/src/test/app.test.jsx
+++ b/src/test/app.test.jsx
@@ -56,7 +56,6 @@ const submissionUUIDs = [
fakeData.ids.submissionUUID(4),
];
const submissions = submissionUUIDs.map(id => fakeData.mockSubmission(id));
-const responses = submissions.map(({ response }) => response);
const statuses = submissionUUIDs.map(id => fakeData.mockSubmissionStatus(id));
const resolveFns = {};
@@ -137,40 +136,18 @@ const clickPrev = async () => {
userEvent.click(el.getByLabelText('Load previous submission'));
};
-/**
- * Wait for the prev and next values to be populated based on the current selection index
- */
-const waitForNeighbors = async (currentIndex) => {
- await waitFor(
- () => {
- const { prev, next } = getState().grading;
- expect(prev).toEqual(
- (currentIndex > 0) ? { response: responses[currentIndex - 1] } : null,
- );
- expect(next).toEqual(
- (currentIndex < 4) ? { response: responses[currentIndex + 1] } : null,
- );
- },
- );
-};
-
/**
* Wait for neighbors, and then verify prev, current, and next grading fields have the appropriate
* data. Also ensure that the app is "grading" iff the "current" response's lockStatus is inProgress.
*/
const checkLoadedResponses = async (currentIndex) => {
- await waitForNeighbors(currentIndex);
- const { prev, current, next } = state.grading;
+ await waitFor(() => expect(getState().grading.current.submissionUUID).toEqual(submissionUUIDs[currentIndex]));
const { lockStatus, gradeStatus } = statuses[currentIndex];
- expect({ prev, current, next }).toEqual({
- prev: currentIndex > 0 ? ({ response: responses[currentIndex - 1] }) : null,
- current: {
- submissionUUID: submissionUUIDs[currentIndex],
- response: submissions[currentIndex].response,
- lockStatus,
- gradeStatus,
- },
- next: currentIndex < 4 ? ({ response: responses[currentIndex + 1] }) : null,
+ expect(state.grading.current).toEqual({
+ submissionUUID: submissionUUIDs[currentIndex],
+ response: submissions[currentIndex].response,
+ lockStatus,
+ gradeStatus,
});
expect(state.app.showReview).toEqual(true);
};
@@ -219,11 +196,6 @@ describe('ESG app integration tests', () => {
...fakeData.mockSubmission(submissionUUID),
});
});
- it('loads response for next submission', () => {
- expect(state.grading.next).toEqual({
- response: fakeData.mockSubmission(submissionUUIDs[1]).response,
- });
- });
});
describe('review navigation', () => {