diff --git a/src/containers/ListView/ListViewBreadcrumb.jsx b/src/containers/ListView/ListViewBreadcrumb.jsx
index d646f17..025135c 100644
--- a/src/containers/ListView/ListViewBreadcrumb.jsx
+++ b/src/containers/ListView/ListViewBreadcrumb.jsx
@@ -6,28 +6,24 @@ import { ArrowBack } from '@edx/paragon/icons';
import { Hyperlink } from '@edx/paragon';
import selectors from 'data/selectors';
-import { locationId } from '../../data/constants/app';
+import { locationId } from 'data/constants/app';
+import urls from 'data/services/lms/urls';
/**
*
*/
-export const ListViewBreadcrumb = ({ courseId, oraName }) => {
- const openResponseUrl = `${process.env.LMS_BASE_URL}/courses/${courseId}/instructor#view-open_response_assessment`;
- const oraUrl = `${process.env.LMS_BASE_URL}/courses/${courseId}/jump_to/${locationId}`;
- return (
- <>
-
-
- Back to all open responses
-
-
{oraName}
-
- >
- );
-};
+export const ListViewBreadcrumb = ({ courseId, oraName }) => (
+ <>
+
+
+ Back to all open responses
+
+
+ {oraName}
+
+
+ >
+);
ListViewBreadcrumb.defaultProps = {
courseId: '',
oraName: '',
@@ -42,7 +38,6 @@ export const mapStateToProps = (state) => ({
oraName: selectors.app.ora.name(state),
});
-export const mapDispatchToProps = {
-};
+export const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(ListViewBreadcrumb);
diff --git a/src/containers/ListView/ListViewBreadcrumb.test.jsx b/src/containers/ListView/ListViewBreadcrumb.test.jsx
new file mode 100644
index 0000000..0b8ca97
--- /dev/null
+++ b/src/containers/ListView/ListViewBreadcrumb.test.jsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import {
+ Hyperlink,
+} from '@edx/paragon';
+
+import * as constants from 'data/constants/app';
+import urls from 'data/services/lms/urls';
+import selectors from 'data/selectors';
+
+import {
+ ListViewBreadcrumb,
+ mapStateToProps,
+} from './ListViewBreadcrumb';
+
+jest.mock('@edx/paragon', () => ({
+ Hyperlink: () => 'Hyperlink',
+}));
+
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ courseId: (...args) => ({ courseId: args }),
+ ora: {
+ name: (...args) => ({ oraName: args }),
+ },
+ },
+ },
+}));
+
+jest.mock('data/constants/app', () => ({
+ locationId: 'fake-location-id',
+}));
+jest.mock('data/services/lms/urls', () => ({
+ openResponse: (courseId) => `openResponseUrl(${courseId})`,
+ ora: (courseId, locationId) => `oraUrl(${courseId}, ${locationId})`,
+}));
+
+let el;
+
+describe('ListViewBreadcrumb component', () => {
+ describe('component', () => {
+ const props = {
+ courseId: 'test-course-id',
+ oraName: 'fake-ora-name',
+ };
+ beforeEach(() => {
+ el = shallow();
+ });
+ test('snapshot: empty (no list data)', () => {
+ expect(el).toMatchSnapshot();
+ });
+ test('openResponse destination', () => {
+ expect(
+ el.find(Hyperlink).at(0).props().destination,
+ ).toEqual(urls.openResponse(props.courseId));
+ });
+ test('ora destination', () => {
+ expect(
+ el.find(Hyperlink).at(1).props().destination,
+ ).toEqual(urls.ora(props.courseId, constants.locationId));
+ });
+ });
+ 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));
+ });
+ test('oraName loads from app.ora.name', () => {
+ expect(mapped.oraName).toEqual(selectors.app.ora.name(testState));
+ });
+ });
+});
diff --git a/src/containers/ListView/TableControls.jsx b/src/containers/ListView/TableControls.jsx
new file mode 100644
index 0000000..8ce2179
--- /dev/null
+++ b/src/containers/ListView/TableControls.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import {
+ DataTable,
+} from '@edx/paragon';
+
+/**
+ *
+ */
+export const TableControls = () => (
+ <>
+
+
+
+
+ >
+);
+TableControls.propTypes = {};
+
+export default TableControls;
diff --git a/src/containers/ListView/TableControls.test.jsx b/src/containers/ListView/TableControls.test.jsx
new file mode 100644
index 0000000..dd552e4
--- /dev/null
+++ b/src/containers/ListView/TableControls.test.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import TableControls from './TableControls';
+
+jest.mock('@edx/paragon', () => ({
+ DataTable: {
+ TableControlBar: () => 'DataTable.TableControlBar',
+ Table: () => 'DataTable.Table',
+ EmptyTable: () => 'DataTable.EmptyTable',
+ TableFooter: () => 'DataTable.TableFooter',
+ },
+}));
+
+describe('ListView TableControls component', () => {
+ describe('component', () => {
+ test('snapshot', () => {
+ expect(shallow()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap b/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap
new file mode 100644
index 0000000..030b5f0
--- /dev/null
+++ b/src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ListViewBreadcrumb component component snapshot: empty (no list data) 1`] = `
+
+
+
+ Back to all open responses
+
+
+ fake-ora-name
+
+
+
+`;
diff --git a/src/containers/ListView/__snapshots__/TableControls.test.jsx.snap b/src/containers/ListView/__snapshots__/TableControls.test.jsx.snap
new file mode 100644
index 0000000..e5d9af4
--- /dev/null
+++ b/src/containers/ListView/__snapshots__/TableControls.test.jsx.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ListView TableControls component component snapshot 1`] = `
+
+
+
+
+
+
+`;
diff --git a/src/containers/ListView/__snapshots__/index.test.jsx.snap b/src/containers/ListView/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000..4cfdd76
--- /dev/null
+++ b/src/containers/ListView/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,123 @@
+// 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`] = `
+
+
+
+
+
+
+
+`;
diff --git a/src/containers/ListView/index.jsx b/src/containers/ListView/index.jsx
index 97e91ec..3328316 100644
--- a/src/containers/ListView/index.jsx
+++ b/src/containers/ListView/index.jsx
@@ -19,9 +19,10 @@ import thunkActions from 'data/thunkActions';
import StatusBadge from 'components/StatusBadge';
import ReviewModal from 'containers/ReviewModal';
import ListViewBreadcrumb from './ListViewBreadcrumb';
+import TableControls from './TableControls';
import './ListView.scss';
-const gradeStatusOptions = Object.keys(gradingStatusDisplay).map(key => ({
+export const gradeStatusOptions = Object.keys(gradingStatusDisplay).map(key => ({
name: gradingStatusDisplay[key],
value: key,
}));
@@ -118,10 +119,7 @@ export class ListView extends React.Component {
},
]}
>
-
-
-
-
+
diff --git a/src/containers/ListView/index.test.jsx b/src/containers/ListView/index.test.jsx
new file mode 100644
index 0000000..58c3496
--- /dev/null
+++ b/src/containers/ListView/index.test.jsx
@@ -0,0 +1,256 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import {
+ DataTable,
+ MultiSelectDropdownFilter,
+ TextFilter,
+} from '@edx/paragon';
+
+import selectors from 'data/selectors';
+import thunkActions from 'data/thunkActions';
+import { gradingStatuses as statuses } from 'data/services/lms/constants';
+
+import StatusBadge from 'components/StatusBadge';
+import {
+ ListView,
+ mapStateToProps,
+ mapDispatchToProps,
+ gradeStatusOptions,
+} from '.';
+
+jest.mock('@edx/paragon', () => ({
+ DataTable: () => 'DataTable',
+ TextFilter: 'TextFilter',
+ MultiSelectDropdownFilter: 'MultiSelectDropdownFilter',
+ Container: () => 'Container',
+}));
+jest.mock('components/StatusBadge', () => 'StatusBadge');
+jest.mock('containers/ReviewModal', () => 'ReviewModal');
+jest.mock('./ListViewBreadcrumb', () => 'ListViewBreadcrumb');
+jest.mock('./TableControls', () => 'TableControls');
+
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ submissions: {
+ listData: (...args) => ({ listData: args }),
+ },
+ },
+}));
+
+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,
+ },
+ },
+ ],
+ };
+ beforeEach(() => {
+ props.initializeApp = jest.fn();
+ props.loadSelectionForReview = jest.fn();
+ });
+ 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: 'Username',
+ accessor: 'username',
+ });
+ });
+ test('submission date column', () => {
+ expect(columns[1]).toEqual({
+ Header: 'Learner submission date',
+ accessor: 'dateSubmitted',
+ Cell: el.instance().formatDate,
+ disableFilters: true,
+ });
+ });
+ test('grade column', () => {
+ expect(columns[2]).toEqual({
+ Header: 'Grade',
+ accessor: 'score',
+ Cell: el.instance().formatGrade,
+ disableFilters: true,
+ });
+ });
+ test('grading status column', () => {
+ expect(columns[3]).toEqual({
+ Header: 'Grading Status',
+ accessor: 'gradingStatus',
+ Cell: el.instance().formatStatus,
+ Filter: MultiSelectDropdownFilter,
+ filter: 'includesValue',
+ filterChoices: 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 submissionId from all rows if there are no selectedRows', () => {
+ const data = {
+ selectedRows: [
+ ],
+ tableInstance: {
+ rows: [
+ { original: { submissionId: '123' } },
+ { original: { submissionId: '456' } },
+ { original: { submissionId: '789' } },
+ ],
+ },
+ };
+ el.instance().handleViewAllResponsesClick(data);
+ expect(el.instance().props.loadSelectionForReview).toHaveBeenCalledWith(['123', '456', '789']);
+ });
+ it('calls loadSelectionForReview with submissionId from selected rows if there are any', () => {
+ const data = {
+ selectedRows: [
+ { original: { submissionId: '123' } },
+ { original: { submissionId: '456' } },
+ { original: { submissionId: '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.includes(rows.length)).toEqual(true);
+ 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 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/data/selectors/app.test.js b/src/data/selectors/app.test.js
index 2cb92ab..20db8fc 100644
--- a/src/data/selectors/app.test.js
+++ b/src/data/selectors/app.test.js
@@ -1,4 +1,3 @@
-
import { feedbackRequirement } from 'data/services/lms/constants';
// import * in order to mock in-file references
diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js
index 678fd21..6905aa9 100644
--- a/src/data/services/lms/urls.js
+++ b/src/data/services/lms/urls.js
@@ -4,8 +4,17 @@ import { configuration } from 'config';
const baseUrl = `${configuration.LMS_BASE_URL}`;
const api = `${baseUrl}/api/`;
+const course = (courseId) => `${baseUrl}/courses/${courseId}`;
+
+const openResponse = (courseId) => (
+ `${course(courseId)}/instructor#view-open_response_assessment`
+);
+const ora = (courseId, locationId) => `${course(courseId)}/jump_to/${locationId}`;
export default StrictDict({
api,
baseUrl,
+ course,
+ openResponse,
+ ora,
});