Refactor: add unit tests and docstrings to remaining components (#195)
* add unit tests and docstrings * v1.4.38
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
168
src/components/GradesTab/BulkManagementControls.test.jsx
Normal file
168
src/components/GradesTab/BulkManagementControls.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
42
src/components/GradesTab/ScoreViewInput.test.jsx
Normal file
42
src/components/GradesTab/ScoreViewInput.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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) => ({
|
||||
|
||||
32
src/components/GradesTab/SpinnerIcon.test.jsx
Normal file
32
src/components/GradesTab/SpinnerIcon.test.jsx
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
46
src/components/GradesTab/UsersLabel.test.jsx
Normal file
46
src/components/GradesTab/UsersLabel.test.jsx
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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`] = `""`;
|
||||
@@ -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>
|
||||
`;
|
||||
23
src/components/Header/__snapshots__/test.jsx.snap
Normal file
23
src/components/Header/__snapshots__/test.jsx.snap
Normal 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>
|
||||
`;
|
||||
@@ -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;
|
||||
|
||||
21
src/components/Header/test.jsx
Normal file
21
src/components/Header/test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
65
src/containers/GradebookPage/__snapshots__/test.jsx.snap
Normal file
65
src/containers/GradebookPage/__snapshots__/test.jsx.snap
Normal 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>
|
||||
`;
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
181
src/containers/GradebookPage/test.jsx
Normal file
181
src/containers/GradebookPage/test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user