Refactor: add unit tests and docstrings to remaining components (#195)

* add unit tests and docstrings

* v1.4.38
This commit is contained in:
Ben Warzeski
2021-06-22 15:25:57 -04:00
committed by GitHub
parent b4f4a27f73
commit 5688adcd57
18 changed files with 755 additions and 91 deletions

View File

@@ -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",

View File

@@ -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: <Icon className="fa fa-download mr-2" />,
pending: <Icon className="fa fa-spinner fa-spin mr-2" />,
},
disabledStates: ['pending'],
className: 'ml-2',
});
export const buttonStates = StrictDict({
pending: 'pending',
default: 'default',
});
/**
* <BulkManagementControls />
* 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 && (
<div>
<StatefulButton
variant="outline-primary"
{...this.buttonProps('Bulk Management')}
onClick={this.handleClickExportGrades}
state={this.props.showSpinner ? 'pending' : 'default'}
labels={{
default: 'Bulk Management',
pending: 'Bulk Management',
}}
icons={{
default: <FontAwesomeIcon className="mr-2" icon={faDownload} />,
pending: <FontAwesomeIcon className="fa-spin mr-2" icon={faSpinner} />,
}}
disabledStates={['pending']}
/>
<StatefulButton
variant="outline-primary"
{...this.buttonProps('Interventions')}
onClick={this.handleClickDownloadInterventions}
state={this.props.showSpinner ? 'pending' : 'default'}
className="ml-2"
labels={{
default: 'Interventions*',
pending: 'Interventions*',
}}
icons={{
default: <FontAwesomeIcon className="mr-2" icon={faDownload} />,
pending: <FontAwesomeIcon className="fa-spin mr-2" icon={faSpinner} />,
}}
disabledStates={['pending']}
/>
</div>
);
@@ -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),
});

View File

@@ -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(<BulkManagementControls {...props} />)).toEqual({});
});
test('snapshot - buttonProps for each button ("Bulk Management" and "Interventions")', () => {
el = shallow(<BulkManagementControls {...props} showBulkManagement />);
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(<BulkManagementControls {...props} showBulkManagement />);
});
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);
});
});
});

View File

@@ -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 }) => (
<InputSelect
label="Score View:"
name="ScoreView"
value="percent"
options={[{ label: 'Percent', value: 'percent' }, { label: 'Absolute', value: 'absolute' }]}
onChange={toggleFormat}
/>
/**
* <ScoreViewInput />
* redux-connected select control for grade format (percent vs absolute)
*/
export const ScoreViewInput = ({ toggleFormat }) => (
<FormGroup controlId="ScoreView">
<FormLabel>Score View:</FormLabel>
<FormControl
as="select"
value="percent"
onChange={toggleFormat}
>
<option value="percent">Percent</option>
<option value="absolute">Absolute</option>
</FormControl>
</FormGroup>
);
ScoreViewInput.propTypes = {
toggleFormat: PropTypes.func.isRequired,

View File

@@ -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(<ScoreViewInput {...props} />);
});
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);
});
});
});

View File

@@ -7,19 +7,21 @@ import { Icon } from '@edx/paragon';
import selectors from 'data/selectors';
const SpinnerIcon = ({ show }) => {
if (!show) {
return null;
}
return (
<div className="spinner-overlay">
<Icon className="fa fa-spinner fa-spin fa-5x color-black" />
</div>
);
/**
* <SpinnerIcon />
* Simmple redux-connected icon component that shows a spinner overlay only if
* redux state says it should.
*/
export const SpinnerIcon = ({ show }) => show && (
<div className="spinner-overlay">
<Icon className="fa fa-spinner fa-spin fa-5x color-black" />
</div>
);
SpinnerIcon.defaultProps = {
show: false,
};
SpinnerIcon.propTypes = {
show: PropTypes.bool.isRequired,
show: PropTypes.bool,
};
export const mapStateToProps = (state) => ({

View File

@@ -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(<SpinnerIcon />)).toMatchSnapshot();
});
test('snapshot - displays spinner overlay with spinner icon', () => {
expect(shallow(<SpinnerIcon show />)).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));
});
});
});

View File

@@ -5,7 +5,11 @@ import { connect } from 'react-redux';
import selectors from 'data/selectors';
const UsersLabel = ({
/**
* <UsersLabel />
* Simple label component displaying the filtered and total users shown
*/
export const UsersLabel = ({
filteredUsersCount,
totalUsersCount,
}) => {

View File

@@ -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(<UsersLabel {...props} totalUsersCount={0} />)).toEqual({});
});
test('snapshot - displays label with number of filtered users out of total', () => {
expect(shallow(<UsersLabel {...props} />)).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));
});
});
});

View File

@@ -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`] = `
<FormGroup
controlId="ScoreView"
>
<FormLabel>
Score View:
</FormLabel>
<FormControl
as="select"
onChange={[MockFunction]}
value="percent"
>
<option
value="percent"
>
Percent
</option>
<option
value="absolute"
>
Absolute
</option>
</FormControl>
</FormGroup>
`;

View File

@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SpinnerIcon component snapshot - displays spinner overlay with spinner icon 1`] = `
<div
className="spinner-overlay"
>
<Icon
className="fa fa-spinner fa-spin fa-5x color-black"
/>
</div>
`;
exports[`SpinnerIcon component snapshot - does not render if show: false 1`] = `""`;

View File

@@ -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`] = `
<Fragment>
Showing
<span
className="font-weight-bold"
>
23
</span>
of
<span
className="font-weight-bold"
>
140
</span>
total learners
</Fragment>
`;

View File

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header snapshot - has edx link with logo url 1`] = `
<div
className="mb-3"
>
<header
className="d-flex justify-content-center align-items-center p-3 border-bottom-blue"
>
<Hyperlink
destination="undefined/dashboard"
>
<img
alt="edX logo"
height="30"
src="www.ourLogo.url"
width="60"
/>
</Hyperlink>
<div />
</header>
</div>
`;

View File

@@ -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 (
<img src={getConfig().LOGO_URL} alt="edX logo" height="30" width="60" />
);
}
/**
* <Header />
* Gradebook MFE app header.
* Displays edx logo, linked to lms dashboard
*/
const Header = () => (
<div className="mb-3">
<header className="d-flex justify-content-center align-items-center p-3 border-bottom-blue">
<Hyperlink destination={`${getConfig().LMS_BASE_URL}/dashboard`}>
<img src={getConfig().LOGO_URL} alt="edX logo" height="30" width="60" />
</Hyperlink>
<div />
</header>
</div>
);
render() {
return (
<div className="mb-3">
<header className="d-flex justify-content-center align-items-center p-3 border-bottom-blue">
<Hyperlink destination={`${getConfig().LMS_BASE_URL}/dashboard`}>
{this.renderLogo()}
</Hyperlink>
<div />
</header>
</div>
);
}
}
export default Header;

View File

@@ -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(<Header />)).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookPage component snapshot - shows BulkManagementTab if showBulkManagement 1`] = `
<WithSidebar
sidebar={
<GradebookFilters
updateQueryParams={[MockFunction updateQueryParams]}
/>
}
sidebarHeader={<GradebookFiltersHeader />}
>
<div
className="px-3 gradebook-content"
>
<GradebookHeader />
<Tabs
defaultActiveKey="grades"
>
<Tab
eventKey="grades"
title="Grades"
>
<GradesTab
updateQueryParams={[MockFunction updateQueryParams]}
/>
</Tab>
<Tab
eventKey="bulk_management"
title="Bulk Management"
>
<BulkManagementTab />
</Tab>
</Tabs>
</div>
</WithSidebar>
`;
exports[`GradebookPage component snapshot - shows only GradesTab if showBulkManagement=false 1`] = `
<WithSidebar
sidebar={
<GradebookFilters
updateQueryParams={[MockFunction updateQueryParams]}
/>
}
sidebarHeader={<GradebookFiltersHeader />}
>
<div
className="px-3 gradebook-content"
>
<GradebookHeader />
<Tabs
defaultActiveKey="grades"
>
<Tab
eventKey="grades"
title="Grades"
>
<GradesTab
updateQueryParams={[MockFunction updateQueryParams]}
/>
</Tab>
</Tabs>
</div>
</WithSidebar>
`;

View File

@@ -16,6 +16,12 @@ import GradebookFilters from 'components/GradebookFilters';
import GradebookFiltersHeader from 'components/GradebookFiltersHeader';
import BulkManagementTab from 'components/BulkManagementTab';
/**
* <GradebookPage />
* 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),
});

View File

@@ -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(<GradebookPage {...props} showBulkManagement />);
el.instance().updateQueryParams = jest.fn().mockName('updateQueryParams');
expect(el.instance().render()).toMatchSnapshot();
});
test('snapshot - shows only GradesTab if showBulkManagement=false', () => {
el = shallow(<GradebookPage {...props} />);
el.instance().updateQueryParams = jest.fn().mockName('updateQueryParams');
expect(el.instance().render()).toMatchSnapshot();
});
describe('render', () => {
beforeEach(() => {
el = shallow(<GradebookPage {...props} />);
});
describe('top-level WithSidebar', () => {
test('sidebar from GradebookFilters, with updateQueryParams', () => {
const { sidebar } = el.props();
expect(sidebar).toEqual(
<GradebookFilters updateQueryParams={el.instance().updateQueryParams} />,
);
});
test('sidebarHeader from GradebookFiltersHeader', () => {
const { sidebarHeader } = el.props();
expect(sidebarHeader).toEqual(<GradebookFiltersHeader />);
});
});
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(<GradebookHeader />);
});
it('displays tabs with only GradesTab if not showBulkManagement', () => {
expect(shallow(children[1])).toEqual(shallow(
<Tabs defaultActiveKey="grades">
<Tab eventKey="grades" title="Grades">
<GradesTab updateQueryParams={el.instance().updateQueryParams} />
</Tab>
</Tabs>,
));
});
it('displays tabs with grades and BulkManagement if showBulkManagement', () => {
el = shallow(<GradebookPage {...props} showBulkManagement />);
const tabs = el.props().children.props.children[1];
expect(tabs).toEqual(
<Tabs defaultActiveKey="grades">
<Tab eventKey="grades" title="Grades">
<GradesTab updateQueryParams={el.instance().updateQueryParams} />
</Tab>
<Tab eventKey="bulk_management" title="Bulk Management">
<BulkManagementTab />
</Tab>
</Tabs>,
);
});
});
});
describe('behavior', () => {
beforeEach(() => {
el = shallow(<GradebookPage {...props} />, { 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);
});
});
});