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:
+
+ Percent
+ Absolute
+
+
);
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:
+
+
+
+ Percent
+
+
+ Absolute
+
+
+
+`;
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`] = `
+
+
+
+
+
+
+
+
+`;
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 (
-
- );
- }
+/**
+ *
+ * Gradebook MFE app header.
+ * Displays edx logo, linked to lms dashboard
+ */
+const Header = () => (
+
+
+
+
+
+
+
+
+);
- 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);
+ });
+ });
+});