diff --git a/package.json b/package.json index 6ad20c5..99ef8ad 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@edx/frontend-app-gradebook", - "version": "1.4.37", + "version": "1.4.38", "description": "edx editable gradebook-ui to manipulate grade overrides on subsections", "repository": { "type": "git", diff --git a/src/components/GradesTab/BulkManagementControls.jsx b/src/components/GradesTab/BulkManagementControls.jsx index c0bf894..633ea25 100644 --- a/src/components/GradesTab/BulkManagementControls.jsx +++ b/src/components/GradesTab/BulkManagementControls.jsx @@ -3,59 +3,72 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { StatefulButton } from '@edx/paragon'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faDownload, faSpinner } from '@fortawesome/free-solid-svg-icons'; +import { StatefulButton, Icon } from '@edx/paragon'; +import { StrictDict } from 'utils'; import actions from 'data/actions'; import selectors from 'data/selectors'; +export const basicButtonProps = () => ({ + variant: 'outline-primary', + icons: { + default: , + pending: , + }, + disabledStates: ['pending'], + className: 'ml-2', +}); + +export const buttonStates = StrictDict({ + pending: 'pending', + default: 'default', +}); + +/** + * + * Provides download buttons for Bulk Management and Intervention reports, only if + * showBulkManagement is set in redus. + */ export class BulkManagementControls extends React.Component { - handleClickDownloadInterventions = () => { - this.props.downloadInterventionReport(this.props.courseId); - window.location = this.props.interventionExportUrl; - }; + constructor(props) { + super(props); + this.buttonProps = this.buttonProps.bind(this); + this.handleClickDownloadInterventions = this.handleClickDownloadInterventions.bind(this); + this.handleClickExportGrades = this.handleClickExportGrades.bind(this); + } + + buttonProps(label) { + return { + labels: { default: label, pending: label }, + state: this.props.showSpinner ? 'pending' : 'default', + ...basicButtonProps(), + }; + } + + handleClickDownloadInterventions() { + this.props.downloadInterventionReport(); + window.location.assign(this.props.interventionExportUrl); + } // At present, we don't store label and value in google analytics. By setting the label // property of the below events, I want to verify that we can set the label of google anlatyics // The following properties of a google analytics event are: - // category (used), name(used), lavel(not used), value(not used) - handleClickExportGrades = () => { - this.props.downloadBulkGradesReport(this.props.courseId); - window.location = this.props.gradeExportUrl; - }; + // category (used), name(used), label(not used), value(not used) + handleClickExportGrades() { + this.props.downloadBulkGradesReport(); + window.location.assign(this.props.gradeExportUrl); + } render() { return this.props.showBulkManagement && (
, - pending: , - }} - disabledStates={['pending']} /> , - pending: , - }} - disabledStates={['pending']} />
); @@ -63,14 +76,11 @@ export class BulkManagementControls extends React.Component { } BulkManagementControls.defaultProps = { - courseId: '', showBulkManagement: false, showSpinner: false, }; BulkManagementControls.propTypes = { - courseId: PropTypes.string, - // redux downloadBulkGradesReport: PropTypes.func.isRequired, downloadInterventionReport: PropTypes.func.isRequired, @@ -80,13 +90,10 @@ BulkManagementControls.propTypes = { showBulkManagement: PropTypes.bool, }; -export const mapStateToProps = (state, ownProps) => ({ - gradeExportUrl: selectors.root.gradeExportUrl(state, { courseId: ownProps.courseId }), - interventionExportUrl: selectors.root.interventionExportUrl( - state, - { courseId: ownProps.courseId }, - ), - showBulkManagement: selectors.root.showBulkManagement(state, { courseId: ownProps.courseId }), +export const mapStateToProps = (state) => ({ + gradeExportUrl: selectors.root.gradeExportUrl(state), + interventionExportUrl: selectors.root.interventionExportUrl(state), + showBulkManagement: selectors.root.showBulkManagement(state), showSpinner: selectors.root.shouldShowSpinner(state), }); diff --git a/src/components/GradesTab/BulkManagementControls.test.jsx b/src/components/GradesTab/BulkManagementControls.test.jsx new file mode 100644 index 0000000..e48a459 --- /dev/null +++ b/src/components/GradesTab/BulkManagementControls.test.jsx @@ -0,0 +1,168 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import actions from 'data/actions'; +import selectors from 'data/selectors'; + +import { + BulkManagementControls, + basicButtonProps, + buttonStates, + mapStateToProps, + mapDispatchToProps, +} from './BulkManagementControls'; + +jest.mock('data/selectors', () => ({ + __esModule: true, + default: { + root: { + gradeExportUrl: (state) => ({ gradeExportUrl: state }), + interventionExportUrl: (state) => ({ interventionExportUrl: state }), + showBulkManagement: (state) => ({ showBulkManagement: state }), + shouldShowSpinner: (state) => ({ showSpinner: state }), + }, + }, +})); +jest.mock('data/actions', () => ({ + __esModule: true, + default: { + grades: { + downloadReport: { + bulkGrades: jest.fn(), + intervention: jest.fn(), + }, + }, + }, +})); + +describe('BulkManagementControls', () => { + describe('component', () => { + let el; + let props = { + gradeExportUrl: 'gradesGoHere', + interventionExportUrl: 'interventionsGoHere', + }; + beforeEach(() => { + props = { + ...props, + downloadBulkGradesReport: jest.fn(), + downloadInterventionReport: jest.fn(), + }; + }); + test('snapshot - empty if showBulkManagement is not truthy', () => { + expect(shallow()).toEqual({}); + }); + test('snapshot - buttonProps for each button ("Bulk Management" and "Interventions")', () => { + el = shallow(); + jest.spyOn(el.instance(), 'buttonProps').mockImplementation( + value => ({ buttonProps: value }), + ); + jest.spyOn(el.instance(), 'handleClickExportGrades').mockName('this.handleClickExportGrades'); + jest.spyOn( + el.instance(), + 'handleClickDownloadInterventions', + ).mockName('this.handleClickDownloadInterventions'); + }); + describe('behavior', () => { + const oldWindowLocation = window.location; + + beforeAll(() => { + delete window.location; + window.location = Object.defineProperties( + {}, + { + ...Object.getOwnPropertyDescriptors(oldWindowLocation), + assign: { + configurable: true, + value: jest.fn(), + }, + }, + ); + }); + beforeEach(() => { + window.location.assign.mockReset(); + el = shallow(); + }); + afterAll(() => { + // restore `window.location` to the `jsdom` `Location` object + window.location = oldWindowLocation; + }); + + describe('buttonProps', () => { + test('loads default and pending labels based on passed string', () => { + const label = 'Fake Label'; + const { labels, state, ...rest } = el.instance().buttonProps(label); + expect(rest).toEqual(basicButtonProps()); + expect(labels).toEqual({ default: label, pending: label }); + }); + test('loads pending state if props.showSpinner', () => { + const label = 'Fake Label'; + el.setProps({ showSpinner: true }); + const { labels, state, ...rest } = el.instance().buttonProps(label); + expect(state).toEqual(buttonStates.pending); + expect(rest).toEqual(basicButtonProps()); + }); + test('loads default state if not props.showSpinner', () => { + const label = 'Fake Label'; + const { labels, state, ...rest } = el.instance().buttonProps(label); + expect(state).toEqual(buttonStates.default); + expect(rest).toEqual(basicButtonProps()); + }); + }); + describe('handleClickDownloadInterventions', () => { + const assertions = [ + 'calls props.downloadInterventionReport', + 'sets location to props.interventionsExportUrl', + ]; + it(assertions.join(' and '), () => { + el.instance().handleClickDownloadInterventions(); + expect(props.downloadInterventionReport).toHaveBeenCalled(); + expect(window.location.assign).toHaveBeenCalledWith(props.interventionExportUrl); + }); + }); + describe('handleClickExportGrades', () => { + const assertions = [ + 'calls props.downloadBulkGradesReport', + 'sets location to props.gradeExportUrl', + ]; + it(assertions.join(' and '), () => { + el.instance().handleClickExportGrades(); + expect(props.downloadBulkGradesReport).toHaveBeenCalled(); + expect(window.location.assign).toHaveBeenCalledWith(props.gradeExportUrl); + }); + }); + }); + }); + + describe('mapStateToProps', () => { + let mapped; + const testState = { do: 'not', test: 'me' }; + beforeEach(() => { + mapped = mapStateToProps(testState); + }); + test('gradeExportUrl from root.gradeExportUrl', () => { + expect(mapped.gradeExportUrl).toEqual(selectors.root.gradeExportUrl(testState)); + }); + test('interventionExportUrl from root.interventionExportUrl', () => { + expect(mapped.interventionExportUrl).toEqual(selectors.root.interventionExportUrl(testState)); + }); + test('showBulkManagement from root.showBulkManagement', () => { + expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState)); + }); + test('showSpinner from root.shouldShowSpinner', () => { + expect(mapped.showSpinner).toEqual(selectors.root.shouldShowSpinner(testState)); + }); + }); + describe('mapDispatchToProps', () => { + test('downloadBulkGradesReport from actions.grades.downloadReport.bulkGrades', () => { + expect( + mapDispatchToProps.downloadBulkGradesReport, + ).toEqual(actions.grades.downloadReport.bulkGrades); + }); + test('downloadInterventionReport from actions.grades.downloadReport.invervention', () => { + expect( + mapDispatchToProps.downloadInterventionReport, + ).toEqual(actions.grades.downloadReport.intervention); + }); + }); +}); diff --git a/src/components/GradesTab/ScoreViewInput.jsx b/src/components/GradesTab/ScoreViewInput.jsx index 7131678..e3638aa 100644 --- a/src/components/GradesTab/ScoreViewInput.jsx +++ b/src/components/GradesTab/ScoreViewInput.jsx @@ -2,18 +2,26 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { InputSelect } from '@edx/paragon'; +import { FormControl, FormGroup, FormLabel } from '@edx/paragon'; import actions from 'data/actions'; -const ScoreViewInput = ({ toggleFormat }) => ( - +/** + * + * redux-connected select control for grade format (percent vs absolute) + */ +export const ScoreViewInput = ({ toggleFormat }) => ( + + Score View: + + + + + ); ScoreViewInput.propTypes = { toggleFormat: PropTypes.func.isRequired, diff --git a/src/components/GradesTab/ScoreViewInput.test.jsx b/src/components/GradesTab/ScoreViewInput.test.jsx new file mode 100644 index 0000000..de22ecf --- /dev/null +++ b/src/components/GradesTab/ScoreViewInput.test.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import actions from 'data/actions'; + +import { ScoreViewInput, mapDispatchToProps } from './ScoreViewInput'; + +jest.mock('@edx/paragon', () => ({ + FormControl: () => 'FormControl', + FormGroup: () => 'FormGroup', + FormLabel: () => 'FormLabel', +})); + +jest.mock('data/actions', () => ({ + __esModule: true, + default: { + grades: { toggleGradeFormat: jest.fn() }, + }, +})); + +describe('ScoreViewInput', () => { + describe('component', () => { + let props; + let el; + beforeEach(() => { + props = { toggleFormat: jest.fn() }; + el = shallow(); + }); + const assertions = [ + 'select box with percent and absolute options', + 'onClick from props.toggleFormat', + ]; + test(`snapshot - ${assertions.join(' and ')}`, () => { + expect(el).toMatchSnapshot(); + }); + }); + describe('mapDispatchToProps', () => { + test('toggleFormat from actions.grades.toggleGradeFormat', () => { + expect(mapDispatchToProps.toggleFormat).toEqual(actions.grades.toggleGradeFormat); + }); + }); +}); diff --git a/src/components/GradesTab/SpinnerIcon.jsx b/src/components/GradesTab/SpinnerIcon.jsx index 92e9ed3..398201c 100644 --- a/src/components/GradesTab/SpinnerIcon.jsx +++ b/src/components/GradesTab/SpinnerIcon.jsx @@ -7,19 +7,21 @@ import { Icon } from '@edx/paragon'; import selectors from 'data/selectors'; -const SpinnerIcon = ({ show }) => { - if (!show) { - return null; - } - return ( -
- -
- ); +/** + * + * 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.isRequired, + show: PropTypes.bool, }; export const mapStateToProps = (state) => ({ diff --git a/src/components/GradesTab/SpinnerIcon.test.jsx b/src/components/GradesTab/SpinnerIcon.test.jsx new file mode 100644 index 0000000..0ffd088 --- /dev/null +++ b/src/components/GradesTab/SpinnerIcon.test.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import selectors from 'data/selectors'; +import { SpinnerIcon, mapStateToProps } from './SpinnerIcon'; + +jest.mock('@edx/paragon', () => ({ + Icon: () => 'Icon', +})); +jest.mock('data/selectors', () => ({ + __esModule: true, + default: { + root: { shouldShowSpinner: state => ({ shouldShowSpinner: state }) }, + }, +})); + +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(); + }); + }); + describe('mapStateToProps', () => { + const testState = { a: 'nice', day: 'for', some: 'sun' }; + test('show from root.shouldShowSpinner', () => { + expect(mapStateToProps(testState).show).toEqual(selectors.root.shouldShowSpinner(testState)); + }); + }); +}); diff --git a/src/components/GradesTab/UsersLabel.jsx b/src/components/GradesTab/UsersLabel.jsx index c59f392..9a2999c 100644 --- a/src/components/GradesTab/UsersLabel.jsx +++ b/src/components/GradesTab/UsersLabel.jsx @@ -5,7 +5,11 @@ import { connect } from 'react-redux'; import selectors from 'data/selectors'; -const UsersLabel = ({ +/** + * + * Simple label component displaying the filtered and total users shown + */ +export const UsersLabel = ({ filteredUsersCount, totalUsersCount, }) => { diff --git a/src/components/GradesTab/UsersLabel.test.jsx b/src/components/GradesTab/UsersLabel.test.jsx new file mode 100644 index 0000000..43a4335 --- /dev/null +++ b/src/components/GradesTab/UsersLabel.test.jsx @@ -0,0 +1,46 @@ +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/GradesTab/__snapshots__/ScoreViewInput.test.jsx.snap b/src/components/GradesTab/__snapshots__/ScoreViewInput.test.jsx.snap new file mode 100644 index 0000000..df8a230 --- /dev/null +++ b/src/components/GradesTab/__snapshots__/ScoreViewInput.test.jsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScoreViewInput component snapshot - select box with percent and absolute options and onClick from props.toggleFormat 1`] = ` + + + Score View: + + + + + + +`; diff --git a/src/components/GradesTab/__snapshots__/SpinnerIcon.test.jsx.snap b/src/components/GradesTab/__snapshots__/SpinnerIcon.test.jsx.snap new file mode 100644 index 0000000..6a99def --- /dev/null +++ b/src/components/GradesTab/__snapshots__/SpinnerIcon.test.jsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SpinnerIcon component snapshot - displays spinner overlay with spinner icon 1`] = ` +
+ +
+`; + +exports[`SpinnerIcon component snapshot - does not render if show: false 1`] = `""`; diff --git a/src/components/GradesTab/__snapshots__/UsersLabel.test.jsx.snap b/src/components/GradesTab/__snapshots__/UsersLabel.test.jsx.snap new file mode 100644 index 0000000..b60d7fe --- /dev/null +++ b/src/components/GradesTab/__snapshots__/UsersLabel.test.jsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UsersLabel component snapshot - displays label with number of filtered users out of total 1`] = ` + + Showing + + 23 + + of + + 140 + + total learners + +`; diff --git a/src/components/Header/__snapshots__/test.jsx.snap b/src/components/Header/__snapshots__/test.jsx.snap new file mode 100644 index 0000000..77cb97f --- /dev/null +++ b/src/components/Header/__snapshots__/test.jsx.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header snapshot - has edx link with logo url 1`] = ` +
+
+ + edX logo + +
+
+
+`; diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index 2eb8c91..0b669f3 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -2,23 +2,20 @@ import React from 'react'; import { Hyperlink } from '@edx/paragon'; import { getConfig } from '@edx/frontend-platform'; -export default class Header extends React.Component { - renderLogo() { - return ( - edX logo - ); - } +/** + *
+ * Gradebook MFE app header. + * Displays edx logo, linked to lms dashboard + */ +const Header = () => ( +
+
+ + edX logo + +
+
+
+); - render() { - return ( -
-
- - {this.renderLogo()} - -
-
-
- ); - } -} +export default Header; diff --git a/src/components/Header/test.jsx b/src/components/Header/test.jsx new file mode 100644 index 0000000..75ac944 --- /dev/null +++ b/src/components/Header/test.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { getConfig } from '@edx/frontend-platform'; + +import Header from '.'; + +jest.mock('@edx/paragon', () => ({ + Hyperlink: () => 'Hyperlink', +})); +jest.mock('@edx/frontend-platform', () => ({ + getConfig: jest.fn(), +})); + +describe('Header', () => { + test('snapshot - has edx link with logo url', () => { + const url = 'www.ourLogo.url'; + getConfig.mockReturnValue({ LOGO_URL: url }); + expect(shallow(
)).toMatchSnapshot(); + }); +}); diff --git a/src/containers/GradebookPage/__snapshots__/test.jsx.snap b/src/containers/GradebookPage/__snapshots__/test.jsx.snap new file mode 100644 index 0000000..976a90b --- /dev/null +++ b/src/containers/GradebookPage/__snapshots__/test.jsx.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GradebookPage component snapshot - shows BulkManagementTab if showBulkManagement 1`] = ` + + } + sidebarHeader={} +> +
+ + + + + + + + + +
+
+`; + +exports[`GradebookPage component snapshot - shows only GradesTab if showBulkManagement=false 1`] = ` + + } + sidebarHeader={} +> +
+ + + + + + +
+
+`; diff --git a/src/containers/GradebookPage/index.jsx b/src/containers/GradebookPage/index.jsx index f2e8670..79eceaf 100644 --- a/src/containers/GradebookPage/index.jsx +++ b/src/containers/GradebookPage/index.jsx @@ -16,6 +16,12 @@ import GradebookFilters from 'components/GradebookFilters'; import GradebookFiltersHeader from 'components/GradebookFiltersHeader'; import BulkManagementTab from 'components/BulkManagementTab'; +/** + * + * Top-level view for the Gradebook MFE. + * Organizes a header and a pair of tabs (Grades and BulkManagement) with a toggle-able + * filter sidebar. + */ export class GradebookPage extends React.Component { constructor(props) { super(props); @@ -24,7 +30,7 @@ export class GradebookPage extends React.Component { componentDidMount() { const urlQuery = queryString.parse(this.props.location.search); - this.props.initializeApp(this.props.courseId, urlQuery); + this.props.initializeApp(this.props.match.params.courseId, urlQuery); } updateQueryParams(queryParams) { @@ -63,21 +69,24 @@ export class GradebookPage extends React.Component { } } GradebookPage.defaultProps = { - location: { search: '' }, showBulkManagement: false, + location: { search: '' }, }; GradebookPage.propTypes = { history: PropTypes.shape({ push: PropTypes.func, }).isRequired, location: PropTypes.shape({ search: PropTypes.string }), - courseId: PropTypes.string.isRequired, initializeApp: PropTypes.func.isRequired, showBulkManagement: PropTypes.bool, + match: PropTypes.shape({ + params: PropTypes.shape({ + courseId: PropTypes.string, + }), + }).isRequired, }; -export const mapStateToProps = (state, ownProps) => ({ - courseId: ownProps.match.params.courseId, +export const mapStateToProps = (state) => ({ showBulkManagement: selectors.root.showBulkManagement(state), }); diff --git a/src/containers/GradebookPage/test.jsx b/src/containers/GradebookPage/test.jsx new file mode 100644 index 0000000..f4b2ce2 --- /dev/null +++ b/src/containers/GradebookPage/test.jsx @@ -0,0 +1,181 @@ +/* eslint-disable import/no-named-as-default */ +import React from 'react'; +import { shallow } from 'enzyme'; +import queryString from 'query-string'; + +import selectors from 'data/selectors'; +import thunkActions from 'data/thunkActions'; + +import { Tab, Tabs } from '@edx/paragon'; + +import GradebookFilters from 'components/GradebookFilters'; +import GradebookFiltersHeader from 'components/GradebookFiltersHeader'; +import GradebookHeader from 'components/GradebookHeader'; +import GradesTab from 'components/GradesTab'; +import BulkManagementTab from 'components/BulkManagementTab'; + +import { GradebookPage, mapStateToProps, mapDispatchToProps } from '.'; + +jest.mock('query-string', () => ({ + parse: jest.fn(val => ({ parsed: val })), + stringify: (val) => `stringify: ${JSON.stringify(val, Object.keys(val).sort())}`, +})); + +jest.mock('@edx/paragon', () => ({ + Tab: () => 'Tab', + Tabs: () => 'Tabs', +})); +jest.mock('data/selectors', () => ({ + __esModule: true, + default: { + root: { + showBulkManagement: (state) => ({ showBulkManagement: state }), + }, + }, +})); +jest.mock('data/thunkActions', () => ({ + __esModule: true, + default: { + app: { initialize: jest.fn() }, + }, +})); + +jest.mock('components/WithSidebar', () => 'WithSidebar'); +jest.mock('components/GradebookHeader', () => 'GradebookHeader'); +jest.mock('components/GradesTab', () => 'GradesTab'); +jest.mock('components/GradebookFilters', () => 'GradebookFilters'); +jest.mock('components/GradebookFiltersHeader', () => 'GradebookFiltersHeader'); +jest.mock('components/BulkManagementTab', () => 'BulkManagementTab'); + +describe('GradebookPage', () => { + describe('component', () => { + const courseId = 'a course'; + let el; + const props = { + location: { + search: 'searchString', + }, + match: { params: { courseId } }, + }; + beforeEach(() => { + props.initializeApp = jest.fn(); + props.history = { push: jest.fn() }; + }); + test('snapshot - shows BulkManagementTab if showBulkManagement', () => { + el = shallow(); + el.instance().updateQueryParams = jest.fn().mockName('updateQueryParams'); + expect(el.instance().render()).toMatchSnapshot(); + }); + test('snapshot - shows only GradesTab if showBulkManagement=false', () => { + el = shallow(); + el.instance().updateQueryParams = jest.fn().mockName('updateQueryParams'); + expect(el.instance().render()).toMatchSnapshot(); + }); + describe('render', () => { + beforeEach(() => { + el = shallow(); + }); + describe('top-level WithSidebar', () => { + test('sidebar from GradebookFilters, with updateQueryParams', () => { + const { sidebar } = el.props(); + expect(sidebar).toEqual( + , + ); + }); + test('sidebarHeader from GradebookFiltersHeader', () => { + const { sidebarHeader } = el.props(); + expect(sidebarHeader).toEqual(); + }); + }); + describe('gradebook-content', () => { + let content; + let children; + beforeEach(() => { + content = el.props().children; + children = content.props.children; + }); + it('is wrapped in a div w/ px-3 gradebook-content classNames', () => { + expect(content.type).toEqual('div'); + expect(content.props.className).toEqual('px-3 gradebook-content'); + }); + it('displays Gradebook header and then tabs', () => { + expect(children[0]).toEqual(); + }); + it('displays tabs with only GradesTab if not showBulkManagement', () => { + expect(shallow(children[1])).toEqual(shallow( + + + + + , + )); + }); + it('displays tabs with grades and BulkManagement if showBulkManagement', () => { + el = shallow(); + const tabs = el.props().children.props.children[1]; + expect(tabs).toEqual( + + + + + + + + , + ); + }); + }); + }); + describe('behavior', () => { + beforeEach(() => { + el = shallow(, { disableLifecucleMethods: true }); + }); + describe('componentDidMount', () => { + test('initializes app with courseId and urlQuery', () => { + el.instance().componentDidMount(); + expect(props.initializeApp).toHaveBeenCalledWith( + courseId, + queryString.parse(props.location.search), + ); + }); + }); + describe('updateQueryParams', () => { + it('replaces values for truthy values', () => { + queryString.parse.mockImplementation(key => ({ [key]: key })); + const newKey = 'testKey'; + const val1 = 'VALUE'; + const val2 = 'VALTWO!!'; + const args = { [newKey]: val1, [props.location.search]: val2 }; + el.instance().updateQueryParams(args); + expect(props.history.push).toHaveBeenCalledWith(`?${queryString.stringify(args)}`); + }); + it('clears values for non-truthy values', () => { + queryString.parse.mockImplementation(key => ({ [key]: key })); + const newKey = 'testKey'; + const val1 = 'VALUE'; + const val2 = false; + const args = { [newKey]: val1, [props.location.search]: val2 }; + el.instance().updateQueryParams(args); + expect(props.history.push).toHaveBeenCalledWith( + `?${queryString.stringify({ [newKey]: val1 })}`, + ); + }); + }); + }); + }); + describe('mapStateToProps', () => { + let mapped; + const testState = { trash: 'in', the: 'wind' }; + beforeEach(() => { + mapped = mapStateToProps(testState); + }); + test('showBulkManagement from root.showBulkManagement', () => { + expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState)); + }); + }); + describe('mapDispatchToProps', () => { + test('initializeApp from thunkActions.app.initialize', () => { + expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize); + }); + }); +});