diff --git a/src/components/GradesView/FilterMenuToggle.jsx b/src/components/GradesView/FilterMenuToggle.jsx deleted file mode 100644 index 9261709..0000000 --- a/src/components/GradesView/FilterMenuToggle.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { Button, Icon } from '@edx/paragon'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import thunkActions from 'data/thunkActions'; - -import messages from './FilterMenuToggle.messages'; - -/** - * Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer - * as well as the search box for searching by username/email. - */ -export const FilterMenuToggle = ({ toggleFilterDrawer }) => ( - -); - -FilterMenuToggle.propTypes = { - // From Redux - toggleFilterDrawer: PropTypes.func.isRequired, -}; - -export const mapStateToProps = () => ({}); - -export const mapDispatchToProps = { - toggleFilterDrawer: thunkActions.app.filterMenu.toggle, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(FilterMenuToggle); diff --git a/src/components/GradesView/FilterMenuToggle.messages.js b/src/components/GradesView/FilterMenuToggle.messages.js deleted file mode 100644 index d028502..0000000 --- a/src/components/GradesView/FilterMenuToggle.messages.js +++ /dev/null @@ -1,11 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - editFilters: { - id: 'gradebook.GradesView.editFilterLabel', - defaultMessage: 'Edit Filters', - description: 'A labeled button in the Grades tab that opens/closes the Filters tab, allowing the grades to be filtered', - }, -}); - -export default messages; diff --git a/src/components/GradesView/FilterMenuToggle.test.jsx b/src/components/GradesView/FilterMenuToggle.test.jsx deleted file mode 100644 index 4d02fca..0000000 --- a/src/components/GradesView/FilterMenuToggle.test.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import thunkActions from 'data/thunkActions'; - -import { FilterMenuToggle, mapDispatchToProps, mapStateToProps } from './FilterMenuToggle'; - -jest.mock('@edx/paragon', () => ({ - Button: () => 'Button', - Icon: () => 'Icon', -})); -jest.mock('data/thunkActions', () => ({ - __esModule: true, - default: { - app: { - filterMenu: { toggle: jest.fn() }, - }, - }, -})); - -describe('FilterMenuToggle component', () => { - describe('snapshots', () => { - test('basic snapshot', () => { - const toggleFilterDrawer = jest.fn().mockName('this.props.toggleFilterDrawer'); - expect(shallow(( - - ))).toMatchSnapshot(); - }); - }); - describe('mapStateToProps', () => { - test('does not connect any selectors', () => { - expect(mapStateToProps({ test: 'state' })).toEqual({}); - }); - }); - describe('mapDispatchToProps', () => { - test('toggleFilterDrawer from thunkActions.app.filterMenu.toggle', () => { - expect(mapDispatchToProps.toggleFilterDrawer).toEqual( - thunkActions.app.filterMenu.toggle, - ); - }); - }); -}); diff --git a/src/components/GradesView/FilteredUsersLabel.jsx b/src/components/GradesView/FilteredUsersLabel.jsx deleted file mode 100644 index 3752dee..0000000 --- a/src/components/GradesView/FilteredUsersLabel.jsx +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import selectors from 'data/selectors'; - -/** - * - * Simple label component displaying the filtered and total users shown - */ -export const FilteredUsersLabel = ({ - filteredUsersCount, - totalUsersCount, -}) => { - if (!totalUsersCount) { - return null; - } - const bold = (val) => ({val}); - return ( - - ); -}; -FilteredUsersLabel.propTypes = { - filteredUsersCount: PropTypes.number.isRequired, - totalUsersCount: PropTypes.number.isRequired, -}; - -export const mapStateToProps = (state) => ({ - totalUsersCount: selectors.grades.totalUsersCount(state), - filteredUsersCount: selectors.grades.filteredUsersCount(state), -}); - -export default connect(mapStateToProps)(FilteredUsersLabel); diff --git a/src/components/GradesView/FilteredUsersLabel.test.jsx b/src/components/GradesView/FilteredUsersLabel.test.jsx deleted file mode 100644 index c6ec280..0000000 --- a/src/components/GradesView/FilteredUsersLabel.test.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import selectors from 'data/selectors'; -import { FilteredUsersLabel, mapStateToProps } from './FilteredUsersLabel'; - -jest.mock('@edx/paragon', () => ({ - Icon: () => 'Icon', -})); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - grades: { - filteredUsersCount: state => ({ filteredUsersCount: state }), - totalUsersCount: state => ({ totalUsersCount: state }), - }, - }, -})); - -describe('FilteredUsersLabel', () => { - describe('component', () => { - const props = { - filteredUsersCount: 23, - totalUsersCount: 140, - }; - it('does not render if totalUsersCount is falsey', () => { - expect(shallow()).toEqual({}); - }); - test('snapshot - displays label with number of filtered users out of total', () => { - expect(shallow()).toMatchSnapshot(); - }); - }); - describe('mapStateToProps', () => { - const testState = { a: 'nice', day: 'for', some: 'rain' }; - let mapped; - beforeEach(() => { - mapped = mapStateToProps(testState); - }); - test('filteredUsersCount from grades.filteredUsersCount', () => { - expect(mapped.filteredUsersCount).toEqual(selectors.grades.filteredUsersCount(testState)); - }); - test('totalUsersCount from grades.totalUsersCount', () => { - expect(mapped.totalUsersCount).toEqual(selectors.grades.totalUsersCount(testState)); - }); - }); -}); diff --git a/src/components/GradesView/ImportGradesButton/hooks.test.js b/src/components/GradesView/ImportGradesButton/hooks.test.js index 753dd09..c4439d0 100644 --- a/src/components/GradesView/ImportGradesButton/hooks.test.js +++ b/src/components/GradesView/ImportGradesButton/hooks.test.js @@ -29,7 +29,7 @@ testFormData.append('csv', testFile); const ref = { current: { click: jest.fn(), files: [testFile], value: 'test-value' }, }; -describe('useAssignmentFilterData hook', () => { +describe('useImportButtonData hook', () => { beforeEach(() => { jest.clearAllMocks(); React.useRef.mockReturnValue(ref); diff --git a/src/components/GradesView/ImportSuccessToast.jsx b/src/components/GradesView/ImportSuccessToast.jsx deleted file mode 100644 index a38a2e0..0000000 --- a/src/components/GradesView/ImportSuccessToast.jsx +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint-disable react/sort-comp, react/button-has-type */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { Toast } from '@edx/paragon'; -import { - injectIntl, - intlShape, -} from '@edx/frontend-platform/i18n'; - -import selectors from 'data/selectors'; -import actions from 'data/actions'; -import { views } from 'data/constants/app'; -import messages from './ImportSuccessToast.messages'; - -/** - * - * Toast component triggered by successful grade upload. - * Provides a link to view the Bulk Management History tab. - */ -export class ImportSuccessToast extends React.Component { - constructor(props) { - super(props); - this.onClose = this.onClose.bind(this); - this.handleShowHistoryView = this.handleShowHistoryView.bind(this); - } - - onClose() { - this.props.setShow(false); - } - - handleShowHistoryView() { - this.props.setAppView(views.bulkManagementHistory); - this.onClose(); - } - - render() { - return ( - - {this.props.intl.formatMessage(messages.description)} - - ); - } -} - -ImportSuccessToast.propTypes = { - // injected - intl: intlShape.isRequired, - // redux - show: PropTypes.bool.isRequired, - setAppView: PropTypes.func.isRequired, - setShow: PropTypes.func.isRequired, -}; - -export const mapStateToProps = (state) => ({ - show: selectors.app.showImportSuccessToast(state), -}); - -export const mapDispatchToProps = { - setAppView: actions.app.setView, - setShow: actions.app.setShowImportSuccessToast, -}; - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ImportSuccessToast)); diff --git a/src/components/GradesView/ImportSuccessToast.messages.js b/src/components/GradesView/ImportSuccessToast.messages.js deleted file mode 100644 index 90791f6..0000000 --- a/src/components/GradesView/ImportSuccessToast.messages.js +++ /dev/null @@ -1,16 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - description: { - id: 'gradebook.GradesView.ImportSuccessToast.description', - defaultMessage: 'Import Successful! Grades will be updated momentarily.', - description: 'A message congratulating a successful Import of grades', - }, - showHistoryViewBtn: { - id: 'gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn', - defaultMessage: 'View Activity Log', - description: 'The text on a button that loads a view of the Bulk Management Activity Log', - }, -}); - -export default messages; diff --git a/src/components/GradesView/ImportSuccessToast.test.jsx b/src/components/GradesView/ImportSuccessToast.test.jsx deleted file mode 100644 index be13492..0000000 --- a/src/components/GradesView/ImportSuccessToast.test.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import selectors from 'data/selectors'; -import actions from 'data/actions'; -import { views } from 'data/constants/app'; - -import { - ImportSuccessToast, - mapStateToProps, - mapDispatchToProps, -} from './ImportSuccessToast'; -import messages from './ImportSuccessToast.messages'; - -jest.mock('@edx/paragon', () => ({ - Toast: () => 'Toast', -})); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - app: { - showImportSuccessToast: (state) => ({ showImportSuccessToast: state }), - }, - }, -})); -jest.mock('data/actions', () => ({ - __esModule: true, - default: { - app: { - setView: jest.fn(), - setShow: jest.fn(), - }, - }, -})); - -describe('ImportSuccessToast component', () => { - describe('snapshots', () => { - let el; - let props = { - show: true, - }; - beforeEach(() => { - props = { - ...props, - intl: { formatMessage: (msg) => msg.defaultMessage }, - setAppView: jest.fn(), - setShow: jest.fn(), - }; - el = shallow(); - }); - test('snapshot', () => { - el.instance().handleShowHistoryView = jest.fn().mockName('handleShowHistoryView'); - el.instance().onClose = jest.fn().mockName('onClose'); - expect(el).toMatchSnapshot(); - }); - describe('Toast props', () => { - let toastProps; - beforeEach(() => { - toastProps = el.props(); - }); - test('action has translated label and onClick from this.handleShowHistoryView', () => { - expect(toastProps.action).toEqual({ - label: props.intl.formatMessage(messages.showHistoryViewBtn), - onClick: el.instance().handleShowHistoryView, - }); - }); - test('onClose from this.onClose method', () => { - expect(toastProps.onClose).toEqual(el.instance().onClose); - }); - test('show from show prop', () => { - expect(toastProps.show).toEqual(props.show); - el.setProps({ show: false }); - expect(el.props().show).toEqual(false); - }); - }); - describe('onClose', () => { - it('calls props.setShow(false)', () => { - el.instance().onClose(); - expect(props.setShow).toHaveBeenCalledWith(false); - }); - }); - describe('handleShowHistoryView', () => { - it('calls setAppView with views.bulkManagementHistory and this.onClose', () => { - el.instance().onClose = jest.fn(); - el.instance().handleShowHistoryView(); - expect(props.setAppView).toHaveBeenCalledWith(views.bulkManagementHistory); - expect(el.instance().onClose).toHaveBeenCalled(); - }); - }); - }); - describe('behavior', () => { - }); - describe('mapStateToProps', () => { - const testState = { somewhere: 'over', the: 'rainbow' }; - const mapped = mapStateToProps(testState); - test('show from app showImportSuccessToast selector', () => { - expect(mapped.show).toEqual( - selectors.app.showImportSuccessToast(testState), - ); - }); - }); - describe('mapDispatchToProps', () => { - test('setAppView from actions.app.setView', () => { - expect(mapDispatchToProps.setAppView).toEqual(actions.app.setView); - }); - test('setShow from actions.setShowImportSuccessToast', () => { - expect(mapDispatchToProps.setShow).toEqual(actions.app.setShowImportSuccessToast); - }); - }); -}); diff --git a/src/components/GradesView/InterventionsReport.jsx b/src/components/GradesView/InterventionsReport.jsx deleted file mode 100644 index 5a0402f..0000000 --- a/src/components/GradesView/InterventionsReport.jsx +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint-disable react/sort-comp, react/button-has-type */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import actions from 'data/actions'; -import selectors from 'data/selectors'; - -import NetworkButton from 'components/NetworkButton'; -import messages from './InterventionsReport.messages'; - -/** - * - * Provides download buttons for Bulk Management and Intervention reports, only if - * showBulkManagement is set in redus. - */ -export class InterventionsReport extends React.Component { - constructor(props) { - super(props); - this.handleClick = this.handleClick.bind(this); - } - - handleClick() { - this.props.downloadInterventionReport(); - window.location.assign(this.props.interventionExportUrl); - } - - render() { - return this.props.showBulkManagement && ( -
-

- -

-
-
- -
- -
-
- ); - } -} - -InterventionsReport.defaultProps = { - showBulkManagement: false, -}; - -InterventionsReport.propTypes = { - // redux - downloadInterventionReport: PropTypes.func.isRequired, - interventionExportUrl: PropTypes.string.isRequired, - showBulkManagement: PropTypes.bool, -}; - -export const mapStateToProps = (state) => ({ - interventionExportUrl: selectors.root.interventionExportUrl(state), - showBulkManagement: selectors.root.showBulkManagement(state), -}); - -export const mapDispatchToProps = { - downloadInterventionReport: actions.grades.downloadReport.intervention, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(InterventionsReport); diff --git a/src/components/GradesView/InterventionsReport.messages.js b/src/components/GradesView/InterventionsReport.messages.js deleted file mode 100644 index 373805f..0000000 --- a/src/components/GradesView/InterventionsReport.messages.js +++ /dev/null @@ -1,21 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - title: { - id: 'gradebook.GradesView.InterventionsReport.title', - defaultMessage: 'Interventions Report', - description: 'The title for the Intervention report subsection', - }, - description: { - id: 'gradebook.GradesView.InterventionsReport.description', - defaultMessage: 'Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.', - description: 'The description for the Intervention report subsection', - }, - downloadBtn: { - id: 'gradebook.GradesView.InterventionsReport.downloadBtn', - defaultMessage: 'Download Interventions', - description: 'The labeled button to download the Intervention report from the Grades View', - }, -}); - -export default messages; diff --git a/src/components/GradesView/InterventionsReport.test.jsx b/src/components/GradesView/InterventionsReport.test.jsx deleted file mode 100644 index 3dfe2b5..0000000 --- a/src/components/GradesView/InterventionsReport.test.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import selectors from 'data/selectors'; -import actions from 'data/actions'; - -import { - InterventionsReport, - mapStateToProps, - mapDispatchToProps, -} from './InterventionsReport'; - -jest.mock('@edx/paragon', () => ({ - Toast: () => 'Toast', -})); -jest.mock('components/NetworkButton', () => 'NetworkButton'); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - root: { - interventionExportUrl: (state) => ({ interventionExportUrl: state }), - showBulkManagement: (state) => ({ showBulkManagement: state }), - }, - }, -})); -jest.mock('data/actions', () => ({ - __esModule: true, - default: { - grades: { - downloadReport: { intervention: jest.fn() }, - }, - }, -})); - -describe('InterventionsReport component', () => { - let el; - let props = { - interventionExportUrl: 'url.for.exporting.interventions', - showBulkManagement: true, - }; - let location; - beforeAll(() => { - location = window.location; - }); - beforeEach(() => { - delete window.location; - window.location = Object.defineProperties( - {}, - { - ...Object.getOwnPropertyDescriptors(location), - assign: { configurable: true, value: jest.fn() }, - }, - ); - props = { - ...props, - downloadInterventionReport: jest.fn(), - }; - }); - afterAll(() => { - window.location = location; - }); - describe('snapshots', () => { - beforeEach(() => { - el = shallow(); - }); - test('snapshot', () => { - el.instance().handleClick = jest.fn().mockName('handleClick'); - expect(el.instance().render()).toMatchSnapshot(); - }); - test('returns empty if props.showBulkManagement is false', () => { - el.setProps({ showBulkManagement: false }); - expect(el.instance().render()).toEqual(false); - }); - }); - describe('behavior', () => { - beforeEach(() => { - el = shallow(); - }); - describe('handleClick', () => { - it('calls props.downloadInterventionReport and navigates to props.interventionExportUrl', () => { - el.instance().handleClick(); - expect(props.downloadInterventionReport).toHaveBeenCalled(); - }); - }); - }); - describe('mapStateToProps', () => { - const testState = { somewhere: 'over', the: 'rainbow' }; - const mapped = mapStateToProps(testState); - test('interventionExportUrl from root interventionExportUrl selector', () => { - expect(mapped.interventionExportUrl).toEqual( - selectors.root.interventionExportUrl(testState), - ); - }); - test('showBulkManagement from root showBulkManagement selector', () => { - expect(mapped.showBulkManagement).toEqual( - selectors.root.showBulkManagement(testState), - ); - }); - }); - describe('mapDispatchToProps', () => { - test('downloadInterventionReport from actions.grades.downloadReport.intervention', () => { - expect(mapDispatchToProps.downloadInterventionReport).toEqual( - actions.grades.downloadReport.intervention, - ); - }); - }); -}); diff --git a/src/components/GradesView/ScoreViewInput.jsx b/src/components/GradesView/ScoreViewInput.jsx deleted file mode 100644 index 6759573..0000000 --- a/src/components/GradesView/ScoreViewInput.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { FormControl, FormGroup, FormLabel } from '@edx/paragon'; -import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; - -import actions from 'data/actions'; -import selectors from 'data/selectors'; -import messages from './ScoreViewInput.messages'; - -/** - * - * redux-connected select control for grade format (percent vs absolute) - */ -export const ScoreViewInput = ({ format, intl, toggleFormat }) => ( - - : - - - - - -); -ScoreViewInput.defaultProps = { - format: 'percent', -}; -ScoreViewInput.propTypes = { - // injected - intl: intlShape.isRequired, - // redux - format: PropTypes.string, - toggleFormat: PropTypes.func.isRequired, -}; - -export const mapStateToProps = (state) => ({ - format: selectors.grades.gradeFormat(state), -}); - -export const mapDispatchToProps = { - toggleFormat: actions.grades.toggleGradeFormat, -}; - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ScoreViewInput)); diff --git a/src/components/GradesView/ScoreViewInput.messages.js b/src/components/GradesView/ScoreViewInput.messages.js deleted file mode 100644 index ed1dca5..0000000 --- a/src/components/GradesView/ScoreViewInput.messages.js +++ /dev/null @@ -1,21 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - scoreView: { - id: 'gradebook.GradesView.scoreViewLabel', - defaultMessage: 'Score View', - description: 'The label for the dropdown list that allows a user to select the Score format', - }, - absolute: { - id: 'gradebook.GradesView.absoluteOption', - defaultMessage: 'Absolute', - description: 'A label within the Score Format dropdown list for the Absolute Grade Score option', - }, - percent: { - id: 'gradebook.GradesView.percentOption', - defaultMessage: 'Percent', - description: 'A label within the Score Format dropdown list for the Percent Grade Score option', - }, -}); - -export default messages; diff --git a/src/components/GradesView/ScoreViewInput.test.jsx b/src/components/GradesView/ScoreViewInput.test.jsx deleted file mode 100644 index 1c73418..0000000 --- a/src/components/GradesView/ScoreViewInput.test.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import actions from 'data/actions'; -import selectors from 'data/selectors'; - -import { - ScoreViewInput, - mapDispatchToProps, - mapStateToProps, -} from './ScoreViewInput'; - -jest.mock('@edx/paragon', () => ({ - FormControl: () => 'FormControl', - FormGroup: () => 'FormGroup', - FormLabel: () => 'FormLabel', -})); - -jest.mock('data/actions', () => ({ - __esModule: true, - default: { - grades: { toggleGradeFormat: jest.fn() }, - }, -})); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - grades: { gradeFormat: (state) => ({ gradeFormat: state }) }, - }, -})); - -describe('ScoreViewInput', () => { - describe('component', () => { - const props = { format: 'percent' }; - let el; - beforeEach(() => { - props.toggleFormat = jest.fn(); - props.intl = { formatMessage: (msg) => msg.defaultMessage }; - el = shallow(); - }); - const assertions = [ - 'select box with percent and absolute options', - 'onClick from props.toggleFormat', - ]; - test(`snapshot - ${assertions.join(' and ')}`, () => { - expect(el).toMatchSnapshot(); - }); - }); - describe('mapStateToProps', () => { - test('format from grades.gradeFormat', () => { - const testState = { some: 'state' }; - expect(mapStateToProps(testState).format).toEqual(selectors.grades.gradeFormat(testState)); - }); - }); - describe('mapDispatchToProps', () => { - test('toggleFormat from actions.grades.toggleGradeFormat', () => { - expect(mapDispatchToProps.toggleFormat).toEqual(actions.grades.toggleGradeFormat); - }); - }); -}); diff --git a/src/components/GradesView/SearchControls.jsx b/src/components/GradesView/SearchControls.jsx deleted file mode 100644 index 08124fe..0000000 --- a/src/components/GradesView/SearchControls.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { SearchField } from '@edx/paragon'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import actions from 'data/actions'; -import selectors from 'data/selectors'; -import thunkActions from 'data/thunkActions'; - -import messages from './SearchControls.messages'; - -/** - * Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer - * as well as the search box for searching by username/email. - */ -export class SearchControls extends React.Component { - constructor(props) { - super(props); - - this.onBlur = this.onBlur.bind(this); - this.onClear = this.onClear.bind(this); - this.onSubmit = this.onSubmit.bind(this); - } - - onBlur(e) { - this.props.setSearchValue(e.target.value); - } - - onClear() { - this.props.setSearchValue(''); - this.props.fetchGrades(); - } - - onSubmit(searchValue) { - this.props.setSearchValue(searchValue); - this.props.fetchGrades(); - } - - render() { - return ( -
- } - onBlur={this.onBlur} - onClear={this.onClear} - value={this.props.searchValue} - /> - - - -
- ); - } -} - -SearchControls.propTypes = { - // From Redux - fetchGrades: PropTypes.func.isRequired, - searchValue: PropTypes.string.isRequired, - setSearchValue: PropTypes.func.isRequired, -}; - -export const mapStateToProps = (state) => ({ - searchValue: selectors.app.searchValue(state), -}); - -export const mapDispatchToProps = { - fetchGrades: thunkActions.grades.fetchGrades, - setSearchValue: actions.app.setSearchValue, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(SearchControls); diff --git a/src/components/GradesView/SearchControls.messages.js b/src/components/GradesView/SearchControls.messages.js deleted file mode 100644 index 79b117f..0000000 --- a/src/components/GradesView/SearchControls.messages.js +++ /dev/null @@ -1,16 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - label: { - id: 'gradebook.GradesView.search.label', - defaultMessage: 'Search for a learner', - description: 'Text prompting a user to use this functionality to search for a learner', - }, - hint: { - id: 'gradebook.GradesView.search.hint', - defaultMessage: 'Search by username, email, or student key', - description: 'A hint explaining the ways a user can search', - }, -}); - -export default messages; diff --git a/src/components/GradesView/SearchControls.test.jsx b/src/components/GradesView/SearchControls.test.jsx deleted file mode 100644 index b181d91..0000000 --- a/src/components/GradesView/SearchControls.test.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import selectors from 'data/selectors'; -import actions from 'data/actions'; -import thunkActions from 'data/thunkActions'; -import { - mapDispatchToProps, - mapStateToProps, - SearchControls, -} from './SearchControls'; - -jest.mock('@edx/paragon', () => ({ - Icon: 'Icon', - Button: 'Button', - SearchField: 'SearchField', -})); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - app: { - searchValue: jest.fn((state) => ({ searchValue: state })), - }, - }, -})); -jest.mock('data/thunkActions', () => ({ - __esModule: true, - default: { - grades: { - fetchGrades: jest.fn().mockName('thunkActions.grades.fetchGrades'), - }, - app: { - filterMenu: { toggle: jest.fn().mockName('thunkActions.app.filterMenu') }, - }, - }, -})); - -describe('SearchControls', () => { - let props; - - beforeEach(() => { - jest.resetAllMocks(); - props = { - searchValue: 'alice', - setSearchValue: jest.fn(), - fetchGrades: jest.fn().mockName('fetchGrades'), - }; - }); - - const searchControls = (overriddenProps) => { - props = { ...props, ...overriddenProps }; - return shallow(); - }; - - describe('Component', () => { - describe('Snapshots', () => { - test('basic snapshot', () => { - const wrapper = searchControls(); - wrapper.instance().onBlur = jest.fn().mockName('onBlur'); - wrapper.instance().onClear = jest.fn().mockName('onClear'); - wrapper.instance().onSubmit = jest.fn().mockName('onSubmit'); - expect(wrapper.instance().render()).toMatchSnapshot(); - }); - }); - - describe('Behavior', () => { - describe('onBlur', () => { - it('saves the search value to Gradebook state but do not fetch grade', () => { - const wrapper = searchControls(); - const event = { - target: { - value: 'bob', - }, - }; - wrapper.instance().onBlur(event); - expect(props.setSearchValue).toHaveBeenCalledWith('bob'); - expect(props.fetchGrades).not.toHaveBeenCalled(); - }); - }); - - describe('onClear', () => { - it('sets search value to empty string and calls fetchGrades', () => { - const wrapper = searchControls(); - wrapper.instance().onClear(); - expect(props.setSearchValue).toHaveBeenCalledWith(''); - expect(props.fetchGrades).toHaveBeenCalled(); - }); - }); - - describe('onSubmit', () => { - it('sets search value to input and calls fetchGrades', () => { - const wrapper = searchControls(); - - wrapper.instance().onSubmit('John'); - expect(props.setSearchValue).toHaveBeenCalledWith('John'); - expect(props.fetchGrades).toHaveBeenCalled(); - }); - }); - }); - - describe('mapStateToProps', () => { - const testState = { never: 'gonna', give: 'you up' }; - test('searchValue from app.searchValue', () => { - expect( - mapStateToProps(testState).searchValue, - ).toEqual(selectors.app.searchValue(testState)); - }); - }); - describe('mapDispatchToProps', () => { - test('fetchGrades from thunkActions.grades.fetchGrades', () => { - expect(mapDispatchToProps.fetchGrades).toEqual(thunkActions.grades.fetchGrades); - }); - - test('setSearchValue from actions.app.setSearchValue', () => { - expect(mapDispatchToProps.setSearchValue).toEqual(actions.app.setSearchValue); - }); - }); - }); -}); diff --git a/src/components/GradesView/SpinnerIcon.jsx b/src/components/GradesView/SpinnerIcon.jsx index 398201c..da044d6 100644 --- a/src/components/GradesView/SpinnerIcon.jsx +++ b/src/components/GradesView/SpinnerIcon.jsx @@ -1,31 +1,22 @@ -/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */ import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; import { Icon } from '@edx/paragon'; -import selectors from 'data/selectors'; +import { selectors } from 'data/redux/hooks'; /** * * Simmple redux-connected icon component that shows a spinner overlay only if * redux state says it should. */ -export const SpinnerIcon = ({ show }) => show && ( -
- -
-); -SpinnerIcon.defaultProps = { - show: false, -}; -SpinnerIcon.propTypes = { - show: PropTypes.bool, +export const SpinnerIcon = () => { + const show = selectors.root.useShouldShowSpinner(); + return show && ( +
+ +
+ ); }; +SpinnerIcon.propTypes = {}; -export const mapStateToProps = (state) => ({ - show: selectors.root.shouldShowSpinner(state), -}); - -export default connect(mapStateToProps)(SpinnerIcon); +export default SpinnerIcon; diff --git a/src/components/GradesView/SpinnerIcon.test.jsx b/src/components/GradesView/SpinnerIcon.test.jsx index 0ffd088..031cf1b 100644 --- a/src/components/GradesView/SpinnerIcon.test.jsx +++ b/src/components/GradesView/SpinnerIcon.test.jsx @@ -1,32 +1,35 @@ import React from 'react'; import { shallow } from 'enzyme'; -import selectors from 'data/selectors'; -import { SpinnerIcon, mapStateToProps } from './SpinnerIcon'; +import { selectors } from 'data/redux/hooks'; +import SpinnerIcon from './SpinnerIcon'; -jest.mock('@edx/paragon', () => ({ - Icon: () => 'Icon', -})); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - root: { shouldShowSpinner: state => ({ shouldShowSpinner: state }) }, +jest.mock('data/redux/hooks', () => ({ + selectors: { + root: { useShouldShowSpinner: jest.fn() }, }, })); +selectors.root.useShouldShowSpinner.mockReturnValue(true); +let el; describe('SpinnerIcon', () => { - describe('component', () => { - it('snapshot - does not render if show: false', () => { - expect(shallow()).toMatchSnapshot(); - }); - test('snapshot - displays spinner overlay with spinner icon', () => { - expect(shallow()).toMatchSnapshot(); + beforeEach(() => { + jest.clearAllMocks(); + el = shallow(); + }); + describe('behavior', () => { + it('initializes redux hook', () => { + expect(selectors.root.useShouldShowSpinner).toHaveBeenCalled(); }); }); - describe('mapStateToProps', () => { - const testState = { a: 'nice', day: 'for', some: 'sun' }; - test('show from root.shouldShowSpinner', () => { - expect(mapStateToProps(testState).show).toEqual(selectors.root.shouldShowSpinner(testState)); + describe('component', () => { + it('does not render if show: false', () => { + selectors.root.useShouldShowSpinner.mockReturnValueOnce(false); + el = shallow(); + expect(el.isEmptyRender()).toEqual(true); + }); + test('snapshot - displays spinner overlay with spinner icon', () => { + expect(el).toMatchSnapshot(); }); }); }); diff --git a/src/components/GradesView/StatusAlerts.jsx b/src/components/GradesView/StatusAlerts.jsx deleted file mode 100644 index 74f60fb..0000000 --- a/src/components/GradesView/StatusAlerts.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { Alert } from '@edx/paragon'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import selectors from 'data/selectors'; -import actions from 'data/actions'; -import messages from './StatusAlerts.messages'; - -export class StatusAlerts extends React.Component { - get isCourseGradeFilterAlertOpen() { - return ( - !this.props.limitValidity.isMinValid - || !this.props.limitValidity.isMaxValid - ); - } - - get minValidityMessage() { - return (this.props.limitValidity.isMinValid) - ? '' - : ; - } - - get maxValidityMessage() { - return (this.props.limitValidity.isMaxValid) - ? '' - : ; - } - - get courseGradeFilterAlertDialogText() { - return ( - <> - {this.minValidityMessage}{this.maxValidityMessage} - - ); - } - - render() { - return ( - <> - - - - - {this.courseGradeFilterAlertDialogText} - - - ); - } -} - -StatusAlerts.defaultProps = { -}; - -StatusAlerts.propTypes = { - // redux - handleCloseSuccessBanner: PropTypes.func.isRequired, - limitValidity: PropTypes.shape({ - isMaxValid: PropTypes.bool, - isMinValid: PropTypes.bool, - }).isRequired, - showSuccessBanner: PropTypes.bool.isRequired, -}; - -export const mapStateToProps = (state) => ({ - limitValidity: selectors.app.courseGradeFilterValidity(state), - showSuccessBanner: selectors.grades.showSuccess(state), -}); - -export const mapDispatchToProps = { - handleCloseSuccessBanner: actions.grades.banner.close, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(StatusAlerts); diff --git a/src/components/GradesView/StatusAlerts.test.jsx b/src/components/GradesView/StatusAlerts.test.jsx deleted file mode 100644 index ba2d799..0000000 --- a/src/components/GradesView/StatusAlerts.test.jsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import actions from 'data/actions'; -import selectors from 'data/selectors'; -import messages from './StatusAlerts.messages'; -import { - StatusAlerts, - mapDispatchToProps, - mapStateToProps, -} from './StatusAlerts'; - -jest.mock('@edx/paragon', () => ({ - Alert: 'Alert', -})); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - app: { - courseGradeFilterValidity: (state) => ({ courseGradeFilterValidity: state }), - }, - grades: { - showSuccess: (state) => ({ showSuccess: state }), - }, - }, -})); - -describe('StatusAlerts', () => { - let props = { - showSuccessBanner: true, - limitValidity: { - isMaxValid: true, - isMinValid: true, - }, - }; - - beforeEach(() => { - props = { - ...props, - handleCloseSuccessBanner: jest.fn().mockName('handleCloseSuccessBanner'), - }; - }); - - describe('snapshots', () => { - let el; - it('basic snapshot', () => { - el = shallow(); - const courseGradeFilterAlertDialogText = 'the quiCk brown does somEthing or other'; - jest.spyOn( - el.instance(), - 'courseGradeFilterAlertDialogText', - 'get', - ).mockReturnValue(courseGradeFilterAlertDialogText); - expect(el.instance().render()).toMatchSnapshot(); - }); - }); - - describe('behavior', () => { - it.each([ - [false, false], - [false, true], - [true, false], - [true, true], - ])('min + max course grade validity', (isMinValid, isMaxValid) => { - props = { - ...props, - limitValidity: { - isMinValid, - isMaxValid, - }, - }; - const el = shallow(); - expect( - el.instance().isCourseGradeFilterAlertOpen, - ).toEqual( - !isMinValid || !isMaxValid, - ); - if (!isMaxValid) { - if (!isMinValid) { - expect(el.instance().courseGradeFilterAlertDialogText).toEqual( - <> - - - , - ); - } else { - expect( - el.instance().courseGradeFilterAlertDialogText, - // eslint-disable-next-line react/jsx-curly-brace-presence - ).toEqual(<>{''}); - } - } else if (!isMinValid) { - expect( - el.instance().courseGradeFilterAlertDialogText, - // eslint-disable-next-line react/jsx-curly-brace-presence - ).toEqual(<>{''}); - } - }); - }); - - describe('mapStateToProps', () => { - const testState = { A: 'pple', B: 'anana', C: 'ucumber' }; - let mapped; - beforeEach(() => { - mapped = mapStateToProps(testState); - }); - test('limitValidity from app.courseGradeFitlerValidity', () => { - expect(mapped.limitValidity).toEqual(selectors.app.courseGradeFilterValidity(testState)); - }); - test('showSuccessBanner from grades.showSuccess', () => { - expect(mapped.showSuccessBanner).toEqual(selectors.grades.showSuccess(testState)); - }); - }); - describe('mapDispatchToProps', () => { - test('handleCloseSuccessBanner from actions.grades.banner.close', () => { - expect( - mapDispatchToProps.handleCloseSuccessBanner, - ).toEqual(actions.grades.banner.close); - }); - }); -}); diff --git a/src/components/GradesView/StatusAlerts/__snapshots__/index.test.jsx.snap b/src/components/GradesView/StatusAlerts/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..a842973 --- /dev/null +++ b/src/components/GradesView/StatusAlerts/__snapshots__/index.test.jsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StatusAlerts component render snapshot 1`] = ` + + + hooks.success-banner-text + + + hooks.grade-filter-text + + +`; diff --git a/src/components/GradesView/StatusAlerts/hooks.js b/src/components/GradesView/StatusAlerts/hooks.js new file mode 100644 index 0000000..efcd8a5 --- /dev/null +++ b/src/components/GradesView/StatusAlerts/hooks.js @@ -0,0 +1,32 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { actions, selectors } from 'data/redux/hooks'; +import messages from './messages'; + +export const useStatusAlertsData = () => { + const { formatMessage } = useIntl(); + + const limitValidity = selectors.app.useCourseGradeFilterValidity(); + const showSuccessBanner = selectors.grades.useShowSuccess(); + const handleCloseSuccessBanner = actions.grades.useCloseBanner(); + + const isCourseGradeFilterAlertOpen = !limitValidity.isMinValid || !limitValidity.isMaxValid; + + const validityMessages = { + min: limitValidity.isMinValid ? '' : formatMessage(messages.minGradeInvalid), + max: limitValidity.isMaxValid ? '' : formatMessage(messages.maxGradeInvalid), + }; + + return { + successBanner: { + onClose: handleCloseSuccessBanner, + show: showSuccessBanner, + text: formatMessage(messages.editSuccessAlert), + }, + gradeFilter: { + show: isCourseGradeFilterAlertOpen, + text: `${validityMessages.min}${validityMessages.max}`, + }, + }; +}; +export default useStatusAlertsData; diff --git a/src/components/GradesView/StatusAlerts/hooks.test.js b/src/components/GradesView/StatusAlerts/hooks.test.js new file mode 100644 index 0000000..4b0e11f --- /dev/null +++ b/src/components/GradesView/StatusAlerts/hooks.test.js @@ -0,0 +1,110 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { formatMessage } from 'testUtils'; +import { actions, selectors } from 'data/redux/hooks'; + +import useStatusAlertsData from './hooks'; +import messages from './messages'; + +jest.mock('data/redux/hooks', () => ({ + actions: { + grades: { useCloseBanner: jest.fn() }, + }, + selectors: { + app: { useCourseGradeFilterValidity: jest.fn() }, + grades: { useShowSuccess: jest.fn() }, + }, +})); + +const validity = { + isMinValid: true, + isMaxValid: true, +}; +selectors.app.useCourseGradeFilterValidity.mockReturnValue(validity); +const showSuccess = 'test-show-success'; +selectors.grades.useShowSuccess.mockReturnValue(showSuccess); +const closeBanner = jest.fn().mockName('hooks.closeBanner'); +actions.grades.useCloseBanner.mockReturnValue(closeBanner); + +let out; +describe('useStatusAlertsData', () => { + beforeEach(() => { + jest.clearAllMocks(); + out = useStatusAlertsData(); + }); + describe('behavior', () => { + it('initializes intl hook', () => { + expect(useIntl).toHaveBeenCalled(); + }); + it('initializes redux hooks', () => { + expect(actions.grades.useCloseBanner).toHaveBeenCalled(); + expect(selectors.app.useCourseGradeFilterValidity).toHaveBeenCalled(); + expect(selectors.grades.useShowSuccess).toHaveBeenCalled(); + }); + }); + describe('output', () => { + describe('successBanner', () => { + test('onClose and show from redux', () => { + expect(out.successBanner.onClose).toEqual(closeBanner); + expect(out.successBanner.show).toEqual(showSuccess); + }); + test('message', () => { + expect(out.successBanner.text).toEqual(formatMessage(messages.editSuccessAlert)); + }); + }); + describe('gradeFilter', () => { + describe('both filters are valid', () => { + test('do not show', () => { + expect(out.gradeFilter.show).toEqual(false); + }); + }); + describe('min filter is invalid', () => { + beforeEach(() => { + selectors.app.useCourseGradeFilterValidity.mockReturnValue({ + isMinValid: false, + isMaxValid: true, + }); + out = useStatusAlertsData(); + }); + test('show grade filter banner', () => { + expect(out.gradeFilter.show).toEqual(true); + }); + test('filter message', () => { + expect(out.gradeFilter.text).toEqual(formatMessage(messages.minGradeInvalid)); + }); + }); + describe('max filter is invalid', () => { + beforeEach(() => { + selectors.app.useCourseGradeFilterValidity.mockReturnValue({ + isMinValid: true, + isMaxValid: false, + }); + out = useStatusAlertsData(); + }); + test('show grade filter banner', () => { + expect(out.gradeFilter.show).toEqual(true); + }); + test('filter message', () => { + expect(out.gradeFilter.text).toEqual(formatMessage(messages.maxGradeInvalid)); + }); + }); + describe('both filters are invalid', () => { + beforeEach(() => { + selectors.app.useCourseGradeFilterValidity.mockReturnValue({ + isMinValid: false, + isMaxValid: false, + }); + out = useStatusAlertsData(); + }); + test('show grade filter banner', () => { + expect(out.gradeFilter.show).toEqual(true); + }); + test('filter message', () => { + expect(out.gradeFilter.text).toEqual( + `${formatMessage(messages.minGradeInvalid)}${formatMessage(messages.maxGradeInvalid)}`, + ); + }); + }); + }); + }); +}); diff --git a/src/components/GradesView/StatusAlerts/index.jsx b/src/components/GradesView/StatusAlerts/index.jsx new file mode 100644 index 0000000..f7883ff --- /dev/null +++ b/src/components/GradesView/StatusAlerts/index.jsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { Alert } from '@edx/paragon'; + +import useStatusAlertsData from './hooks'; + +export const StatusAlerts = () => { + const { + successBanner, + gradeFilter, + } = useStatusAlertsData(); + + return ( + <> + + {successBanner.text} + + + {gradeFilter.text} + + + ); +}; + +StatusAlerts.propTypes = {}; + +export default StatusAlerts; diff --git a/src/components/GradesView/StatusAlerts/index.test.jsx b/src/components/GradesView/StatusAlerts/index.test.jsx new file mode 100644 index 0000000..408044d --- /dev/null +++ b/src/components/GradesView/StatusAlerts/index.test.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Alert } from '@edx/paragon'; + +import useStatusAlertsData from './hooks'; +import StatusAlerts from '.'; + +jest.mock('./hooks', () => jest.fn()); + +const hookProps = { + successBanner: { + onClose: jest.fn().mockName('hooks.successBanner.onClose'), + show: 'hooks.show-success-banner', + text: 'hooks.success-banner-text', + }, + gradeFilter: { + show: 'hooks.show-grade-filter', + text: 'hooks.grade-filter-text', + }, +}; +useStatusAlertsData.mockReturnValue(hookProps); + +let el; +describe('StatusAlerts component', () => { + beforeEach(() => { + jest.clearAllMocks(); + el = shallow(); + }); + describe('behavior', () => { + it('initializes component hooks', () => { + expect(useStatusAlertsData).toHaveBeenCalled(); + }); + }); + describe('render', () => { + test('snapshot', () => { + expect(el).toMatchSnapshot(); + }); + test('success banner', () => { + const alert = el.find(Alert).at(0); + const props = alert.props(); + expect(props.onClose).toEqual(hookProps.successBanner.onClose); + expect(props.show).toEqual(hookProps.successBanner.show); + expect(alert.text()).toEqual(hookProps.successBanner.text); + }); + test('grade filter banner', () => { + const alert = el.find(Alert).at(1); + const props = alert.props(); + expect(props.show).toEqual(hookProps.gradeFilter.show); + expect(alert.text()).toEqual(hookProps.gradeFilter.text); + }); + }); +}); diff --git a/src/components/GradesView/StatusAlerts.messages.js b/src/components/GradesView/StatusAlerts/messages.js similarity index 100% rename from src/components/GradesView/StatusAlerts.messages.js rename to src/components/GradesView/StatusAlerts/messages.js diff --git a/src/components/GradesView/UsersLabel.jsx b/src/components/GradesView/UsersLabel.jsx deleted file mode 100644 index ac6df0f..0000000 --- a/src/components/GradesView/UsersLabel.jsx +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import selectors from 'data/selectors'; - -/** - * - * Simple label component displaying the filtered and total users shown - */ -export const UsersLabel = ({ - filteredUsersCount, - totalUsersCount, -}) => { - if (!totalUsersCount) { - return null; - } - const bold = (val) => ({val}); - return ( - - ); -}; -UsersLabel.propTypes = { - filteredUsersCount: PropTypes.number.isRequired, - totalUsersCount: PropTypes.number.isRequired, -}; - -export const mapStateToProps = (state) => ({ - totalUsersCount: selectors.grades.totalUsersCount(state), - filteredUsersCount: selectors.grades.filteredUsersCount(state), -}); - -export default connect(mapStateToProps)(UsersLabel); diff --git a/src/components/GradesView/UsersLabel.test.jsx b/src/components/GradesView/UsersLabel.test.jsx deleted file mode 100644 index 43a4335..0000000 --- a/src/components/GradesView/UsersLabel.test.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import selectors from 'data/selectors'; -import { UsersLabel, mapStateToProps } from './UsersLabel'; - -jest.mock('@edx/paragon', () => ({ - Icon: () => 'Icon', -})); -jest.mock('data/selectors', () => ({ - __esModule: true, - default: { - grades: { - filteredUsersCount: state => ({ filteredUsersCount: state }), - totalUsersCount: state => ({ totalUsersCount: state }), - }, - }, -})); - -describe('UsersLabel', () => { - describe('component', () => { - const props = { - filteredUsersCount: 23, - totalUsersCount: 140, - }; - it('does not render if totalUsersCount is falsey', () => { - expect(shallow()).toEqual({}); - }); - test('snapshot - displays label with number of filtered users out of total', () => { - expect(shallow()).toMatchSnapshot(); - }); - }); - describe('mapStateToProps', () => { - const testState = { a: 'nice', day: 'for', some: 'rain' }; - let mapped; - beforeEach(() => { - mapped = mapStateToProps(testState); - }); - test('filteredUsersCount from grades.filteredUsersCount', () => { - expect(mapped.filteredUsersCount).toEqual(selectors.grades.filteredUsersCount(testState)); - }); - test('totalUsersCount from grades.totalUsersCount', () => { - expect(mapped.totalUsersCount).toEqual(selectors.grades.totalUsersCount(testState)); - }); - }); -}); diff --git a/src/components/GradesView/__snapshots__/FilterMenuToggle.test.jsx.snap b/src/components/GradesView/__snapshots__/FilterMenuToggle.test.jsx.snap deleted file mode 100644 index 9b66b38..0000000 --- a/src/components/GradesView/__snapshots__/FilterMenuToggle.test.jsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FilterMenuToggle component snapshots basic snapshot 1`] = ` - -`; diff --git a/src/components/GradesView/__snapshots__/FilteredUsersLabel.test.jsx.snap b/src/components/GradesView/__snapshots__/FilteredUsersLabel.test.jsx.snap deleted file mode 100644 index 2086de8..0000000 --- a/src/components/GradesView/__snapshots__/FilteredUsersLabel.test.jsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FilteredUsersLabel component snapshot - displays label with number of filtered users out of total 1`] = ` - - 23 - , - "totalUsers": - 140 - , - } - } -/> -`; diff --git a/src/components/GradesView/__snapshots__/ImportSuccessToast.test.jsx.snap b/src/components/GradesView/__snapshots__/ImportSuccessToast.test.jsx.snap deleted file mode 100644 index b42d994..0000000 --- a/src/components/GradesView/__snapshots__/ImportSuccessToast.test.jsx.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ImportSuccessToast component snapshots snapshot 1`] = ` - - Import Successful! Grades will be updated momentarily. - -`; diff --git a/src/components/GradesView/__snapshots__/InterventionsReport.test.jsx.snap b/src/components/GradesView/__snapshots__/InterventionsReport.test.jsx.snap deleted file mode 100644 index 44210b1..0000000 --- a/src/components/GradesView/__snapshots__/InterventionsReport.test.jsx.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`InterventionsReport component snapshots snapshot 1`] = ` -
-

- -

-
-
- -
- -
-
-`; diff --git a/src/components/GradesView/__snapshots__/ScoreViewInput.test.jsx.snap b/src/components/GradesView/__snapshots__/ScoreViewInput.test.jsx.snap deleted file mode 100644 index 44b805a..0000000 --- a/src/components/GradesView/__snapshots__/ScoreViewInput.test.jsx.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScoreViewInput component snapshot - select box with percent and absolute options and onClick from props.toggleFormat 1`] = ` - - - - : - - - - - - -`; diff --git a/src/components/GradesView/__snapshots__/SearchControls.test.jsx.snap b/src/components/GradesView/__snapshots__/SearchControls.test.jsx.snap deleted file mode 100644 index 43e3bb7..0000000 --- a/src/components/GradesView/__snapshots__/SearchControls.test.jsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SearchControls Component Snapshots basic snapshot 1`] = ` -
- - } - onBlur={[MockFunction onBlur]} - onClear={[MockFunction onClear]} - onSubmit={[MockFunction onSubmit]} - value="alice" - /> - - - -
-`; diff --git a/src/components/GradesView/__snapshots__/SpinnerIcon.test.jsx.snap b/src/components/GradesView/__snapshots__/SpinnerIcon.test.jsx.snap index 6a99def..c530fb3 100644 --- a/src/components/GradesView/__snapshots__/SpinnerIcon.test.jsx.snap +++ b/src/components/GradesView/__snapshots__/SpinnerIcon.test.jsx.snap @@ -9,5 +9,3 @@ exports[`SpinnerIcon component snapshot - displays spinner overlay with spinner /> `; - -exports[`SpinnerIcon component snapshot - does not render if show: false 1`] = `""`; diff --git a/src/components/GradesView/__snapshots__/StatusAlerts.test.jsx.snap b/src/components/GradesView/__snapshots__/StatusAlerts.test.jsx.snap deleted file mode 100644 index 7edc860..0000000 --- a/src/components/GradesView/__snapshots__/StatusAlerts.test.jsx.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`StatusAlerts snapshots basic snapshot 1`] = ` - - - - - - the quiCk brown does somEthing or other - - -`; diff --git a/src/components/GradesView/__snapshots__/UsersLabel.test.jsx.snap b/src/components/GradesView/__snapshots__/UsersLabel.test.jsx.snap deleted file mode 100644 index 9263159..0000000 --- a/src/components/GradesView/__snapshots__/UsersLabel.test.jsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UsersLabel component snapshot - displays label with number of filtered users out of total 1`] = ` - - 23 - , - "totalUsers": - 140 - , - } - } -/> -`; diff --git a/src/components/GradesView/__snapshots__/index.test.jsx.snap b/src/components/GradesView/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..cc927fa --- /dev/null +++ b/src/components/GradesView/__snapshots__/index.test.jsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GradesView component render snapshot 1`] = ` + + + +

+ filter-step-heading +

+
+ + +
+ + +

+ gradebook-step-heading +

+
+ + +
+ + + +

+ * + test-masters-hint +

+ + +
+`; diff --git a/src/components/GradesView/__snapshots__/test.jsx.snap b/src/components/GradesView/__snapshots__/test.jsx.snap deleted file mode 100644 index d208f4b..0000000 --- a/src/components/GradesView/__snapshots__/test.jsx.snap +++ /dev/null @@ -1,53 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GradesView Component snapshots basic snapshot 1`] = ` - - - -

- -

-
- - -
- - -

- -

-
- - -
- - - -

- * - -

- - -
-`; diff --git a/src/components/GradesView/hooks.js b/src/components/GradesView/hooks.js new file mode 100644 index 0000000..b8f6504 --- /dev/null +++ b/src/components/GradesView/hooks.js @@ -0,0 +1,30 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { actions, thunkActions } from 'data/redux/hooks'; +import messages from './messages'; + +export const useGradesViewData = ({ updateQueryParams }) => { + const { formatMessage } = useIntl(); + const fetchGrades = thunkActions.grades.useFetchGrades(); + const resetFilters = actions.filters.useResetFilters(); + + const handleFilterBadgeClose = (filterNames) => () => { + resetFilters(filterNames); + updateQueryParams(filterNames.reduce( + (obj, filterName) => ({ ...obj, [filterName]: false }), + {}, + )); + fetchGrades(); + }; + + return { + stepHeadings: { + filter: formatMessage(messages.filterStepHeading), + gradebook: formatMessage(messages.gradebookStepHeading), + }, + handleFilterBadgeClose, + mastersHint: formatMessage(messages.mastersHint), + }; +}; + +export default useGradesViewData; diff --git a/src/components/GradesView/hooks.test.js b/src/components/GradesView/hooks.test.js new file mode 100644 index 0000000..7e444e0 --- /dev/null +++ b/src/components/GradesView/hooks.test.js @@ -0,0 +1,62 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { formatMessage } from 'testUtils'; +import { actions, thunkActions } from 'data/redux/hooks'; + +import useGradesViewData from './hooks'; +import messages from './messages'; + +jest.mock('data/redux/hooks', () => ({ + actions: { + filters: { useResetFilters: jest.fn() }, + }, + thunkActions: { + grades: { useFetchGrades: jest.fn() }, + }, +})); + +const fetchGrades = jest.fn(); +thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades); +const resetFilters = jest.fn(); +actions.filters.useResetFilters.mockReturnValue(resetFilters); + +const updateQueryParams = jest.fn(); + +let out; +describe('useGradesViewData', () => { + beforeEach(() => { + jest.clearAllMocks(); + out = useGradesViewData({ updateQueryParams }); + }); + describe('behavior', () => { + it('initializes intl hook', () => { + expect(useIntl).toHaveBeenCalled(); + }); + it('initializes redux hooks', () => { + expect(thunkActions.grades.useFetchGrades).toHaveBeenCalled(); + expect(actions.filters.useResetFilters).toHaveBeenCalled(); + }); + }); + describe('output', () => { + test('stepHeadings', () => { + expect(out.stepHeadings.filter).toEqual(formatMessage(messages.filterStepHeading)); + expect(out.stepHeadings.gradebook).toEqual(formatMessage(messages.gradebookStepHeading)); + }); + test('mastersHint', () => { + expect(out.mastersHint).toEqual(formatMessage(messages.mastersHint)); + }); + describe('handleFilterBadgeClose', () => { + it('resets filters locally and in query params, and fetches grades', () => { + const filters = ['some', 'filter', 'names']; + out.handleFilterBadgeClose(filters)(); + expect(resetFilters).toHaveBeenCalledWith(filters); + expect(updateQueryParams).toHaveBeenCalledWith({ + some: false, + filter: false, + names: false, + }); + expect(fetchGrades).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/components/GradesView/index.jsx b/src/components/GradesView/index.jsx index 47373b1..5c4ddee 100644 --- a/src/components/GradesView/index.jsx +++ b/src/components/GradesView/index.jsx @@ -1,12 +1,6 @@ /* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import actions from 'data/actions'; -import thunkActions from 'data/thunkActions'; import BulkManagementControls from './BulkManagementControls'; import EditModal from './EditModal'; @@ -21,79 +15,55 @@ import ScoreViewInput from './ScoreViewInput'; import SearchControls from './SearchControls'; import SpinnerIcon from './SpinnerIcon'; import StatusAlerts from './StatusAlerts'; -import messages from './messages'; -export class GradesView extends React.Component { - constructor(props) { - super(props); - this.handleFilterBadgeClose = this.handleFilterBadgeClose.bind(this); - } +import useGradesViewData from './hooks'; - handleFilterBadgeClose(filterNames) { - return () => { - this.props.resetFilters(filterNames); - this.props.updateQueryParams(filterNames.reduce( - (obj, filterName) => ({ ...obj, [filterName]: false }), - {}, - )); - this.props.fetchGrades(); - }; - } +export const GradesView = ({ updateQueryParams }) => { + const { + stepHeadings, + handleFilterBadgeClose, + mastersHint, + } = useGradesViewData({ updateQueryParams }); - render() { - return ( - <> - + return ( + <> + - -

- -

+ +

+ {stepHeadings.filter} +

-
- - -
+
+ + +
- - + + -

+

{stepHeadings.gradebook}

-
- - -
+
+ + +
- + - + - -

*

- + +

* {mastersHint}

+ - - - ); - } -} - -GradesView.defaultProps = {}; + + + ); +}; GradesView.propTypes = { updateQueryParams: PropTypes.func.isRequired, - - // redux - fetchGrades: PropTypes.func.isRequired, - resetFilters: PropTypes.func.isRequired, }; -export const mapStateToProps = () => ({}); - -export const mapDispatchToProps = { - fetchGrades: thunkActions.grades.fetchGrades, - resetFilters: actions.filters.reset, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(GradesView); +export default GradesView; diff --git a/src/components/GradesView/index.test.jsx b/src/components/GradesView/index.test.jsx new file mode 100644 index 0000000..2279071 --- /dev/null +++ b/src/components/GradesView/index.test.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import FilterBadges from './FilterBadges'; + +import useGradesViewData from './hooks'; +import GradesView from '.'; + +jest.mock('./BulkManagementControls', () => 'BulkManagementControls'); +jest.mock('./EditModal', () => 'EditModal'); +jest.mock('./FilterBadges', () => 'FilterBadges'); +jest.mock('./FilteredUsersLabel', () => 'FilteredUsersLabel'); +jest.mock('./FilterMenuToggle', () => 'FilterMenuToggle'); +jest.mock('./GradebookTable', () => 'GradebookTable'); +jest.mock('./ImportSuccessToast', () => 'ImportSuccessToast'); +jest.mock('./InterventionsReport', () => 'InterventionsReport'); +jest.mock('./PageButtons', () => 'PageButtons'); +jest.mock('./ScoreViewInput', () => 'ScoreViewInput'); +jest.mock('./SearchControls', () => 'SearchControls'); +jest.mock('./SpinnerIcon', () => 'SpinnerIcon'); +jest.mock('./StatusAlerts', () => 'StatusAlerts'); +jest.mock('./hooks', () => jest.fn()); + +const hookProps = { + stepHeadings: { + filter: 'filter-step-heading', + gradebook: 'gradebook-step-heading', + }, + handleFilterBadgeClose: jest.fn().mockName('hooks.handleFilterBadgeClose'), + mastersHint: 'test-masters-hint', +}; +useGradesViewData.mockReturnValue(hookProps); + +const updateQueryParams = jest.fn().mockName('props.updateQueryParams'); + +let el; +describe('GradesView component', () => { + beforeEach(() => { + jest.clearAllMocks(); + el = shallow(); + }); + describe('behavior', () => { + it('initializes component hooks', () => { + expect(useGradesViewData).toHaveBeenCalled(); + }); + }); + describe('render', () => { + test('snapshot', () => { + expect(el).toMatchSnapshot(); + }); + test('filterBadges load close behavior from hook', () => { + expect(el.find(FilterBadges).props().handleClose).toEqual( + hookProps.handleFilterBadgeClose, + ); + }); + }); +}); diff --git a/src/components/GradesView/test.jsx b/src/components/GradesView/test.jsx deleted file mode 100644 index 847e989..0000000 --- a/src/components/GradesView/test.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import actions from 'data/actions'; -import thunkActions from 'data/thunkActions'; - -import { - GradesView, - mapStateToProps, - mapDispatchToProps, -} from '.'; - -jest.mock('data/actions', () => ({ - __esModule: true, - default: { - app: { setView: jest.fn() }, - filters: { resetFilters: jest.fn() }, - }, -})); -jest.mock('data/thunkActions', () => ({ - __esModule: true, - default: { - grades: { fetchGrades: jest.fn() }, - }, -})); - -jest.mock('./BulkManagementControls', () => 'BulkManagementControls'); -jest.mock('./EditModal', () => 'EditModal'); -jest.mock('./FilterBadges', () => 'FilterBadges'); -jest.mock('./FilteredUsersLabel', () => 'FilteredUsersLabel'); -jest.mock('./FilterMenuToggle', () => 'FilterMenuToggle'); -jest.mock('./GradebookTable', () => 'GradebookTable'); -jest.mock('./ImportSuccessToast', () => 'ImportSuccessToast'); -jest.mock('./InterventionsReport', () => 'InterventionsReport'); -jest.mock('./PageButtons', () => 'PageButtons'); -jest.mock('./ScoreViewInput', () => 'ScoreViewInput'); -jest.mock('./SearchControls', () => 'SearchControls'); -jest.mock('./SpinnerIcon', () => 'SpinnerIcon'); -jest.mock('./StatusAlerts', () => 'StatusAlerts'); - -describe('GradesView', () => { - let props; - beforeEach(() => { - props = { - updateQueryParams: jest.fn(), - fetchGrades: jest.fn(), - resetFilters: jest.fn(), - }; - }); - - describe('Component', () => { - const filterNames = ['duck', 'Duck', 'Duuuuuck', 'GOOOOSE!']; - describe('behavior', () => { - let el; - beforeEach(() => { - el = shallow(); - }); - describe('handleFilterBadgeClose', () => { - beforeEach(() => { - el.instance().handleFilterBadgeClose(filterNames)(); - }); - it('calls props.resetFilters with the filters', () => { - expect(props.resetFilters).toHaveBeenCalledWith(filterNames); - }); - it('calls props.updateQueryParams with a reset-filters obj', () => { - expect(props.updateQueryParams).toHaveBeenCalledWith({ - [filterNames[0]]: false, - [filterNames[1]]: false, - [filterNames[2]]: false, - [filterNames[3]]: false, - }); - }); - it('calls fetchGrades', () => { - expect(props.fetchGrades).toHaveBeenCalledWith(); - }); - }); - }); - describe('snapshots', () => { - test('basic snapshot', () => { - const el = shallow(); - el.instance().handleFilterBadgeClose = jest.fn().mockName('this.handleFilterBadgeClose'); - expect(el.instance().render()).toMatchSnapshot(); - }); - }); - }); - test('mapStateToProps is empty', () => { - expect(mapStateToProps({ some: 'state' })).toEqual({}); - }); - describe('mapDispatchToProps', () => { - describe('fetchGrades', () => { - test('from thunkActions.grades.fetchGrades', () => { - expect(mapDispatchToProps.fetchGrades).toEqual( - thunkActions.grades.fetchGrades, - ); - }); - }); - describe('resetFilters', () => { - test('from actions.filters.reset', () => { - expect(mapDispatchToProps.resetFilters).toEqual( - actions.filters.reset, - ); - }); - }); - }); -});