From 5f58a6b80959472dd619c614bf7c9068c3297028 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Fri, 8 Oct 2021 11:33:08 -0400 Subject: [PATCH 1/3] feat: ListView tests --- .../ListView/ListViewBreadcrumb.jsx | 35 +-- .../ListView/ListViewBreadcrumb.test.jsx | 79 ++++++ src/containers/ListView/TableControls.jsx | 20 ++ .../ListView/TableControls.test.jsx | 21 ++ .../ListViewBreadcrumb.test.jsx.snap | 24 ++ .../__snapshots__/TableControls.test.jsx.snap | 12 + .../__snapshots__/index.test.jsx.snap | 123 +++++++++ src/containers/ListView/index.jsx | 8 +- src/containers/ListView/index.test.jsx | 256 ++++++++++++++++++ src/data/selectors/app.test.js | 1 - src/data/services/lms/urls.js | 9 + 11 files changed, 562 insertions(+), 26 deletions(-) create mode 100644 src/containers/ListView/ListViewBreadcrumb.test.jsx create mode 100644 src/containers/ListView/TableControls.jsx create mode 100644 src/containers/ListView/TableControls.test.jsx create mode 100644 src/containers/ListView/__snapshots__/ListViewBreadcrumb.test.jsx.snap create mode 100644 src/containers/ListView/__snapshots__/TableControls.test.jsx.snap create mode 100644 src/containers/ListView/__snapshots__/index.test.jsx.snap create mode 100644 src/containers/ListView/index.test.jsx 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..73c6e6f --- /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: { + TableController: () => 'DataTable.TableController', + 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..f0684b7 --- /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, }); From f14933587ddbe972c6b38a1880891726761dc0eb Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 18 Oct 2021 16:35:45 -0400 Subject: [PATCH 2/3] fix: Update src/containers/ListView/TableControls.test.jsx Co-authored-by: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com> --- src/containers/ListView/TableControls.test.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/ListView/TableControls.test.jsx b/src/containers/ListView/TableControls.test.jsx index 73c6e6f..dd552e4 100644 --- a/src/containers/ListView/TableControls.test.jsx +++ b/src/containers/ListView/TableControls.test.jsx @@ -5,7 +5,7 @@ import TableControls from './TableControls'; jest.mock('@edx/paragon', () => ({ DataTable: { - TableController: () => 'DataTable.TableController', + TableControlBar: () => 'DataTable.TableControlBar', Table: () => 'DataTable.Table', EmptyTable: () => 'DataTable.EmptyTable', TableFooter: () => 'DataTable.TableFooter', From 7ae740bf63c8c347e36a7fb7b6224ff40a6f5fed Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Tue, 19 Oct 2021 09:10:32 -0400 Subject: [PATCH 3/3] fix: update snapshot --- .../ListView/__snapshots__/TableControls.test.jsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/ListView/__snapshots__/TableControls.test.jsx.snap b/src/containers/ListView/__snapshots__/TableControls.test.jsx.snap index f0684b7..e5d9af4 100644 --- a/src/containers/ListView/__snapshots__/TableControls.test.jsx.snap +++ b/src/containers/ListView/__snapshots__/TableControls.test.jsx.snap @@ -2,7 +2,7 @@ exports[`ListView TableControls component component snapshot 1`] = ` - +