refactor: unit test and docstring FilterBadges (#193)
* unit test and docstring FilterBadges * v1.4.38
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-gradebook",
|
||||
"version": "1.4.36",
|
||||
"version": "1.4.37",
|
||||
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
/**
|
||||
* FilterBadge
|
||||
* Base filter badge component, that displays a name and a close button.
|
||||
* If showValue is true, it will also display the included value.
|
||||
* @param {string} name - filter name
|
||||
* @param {bool/string} value - filter value
|
||||
* @param {func} onClick - close/dismiss filter event
|
||||
* @param {bool} showValue - should the Value be displayed instead of just name?
|
||||
* @param {func} handleClose - close/dismiss filter event, taking a list of filternames
|
||||
* to reset when the filter badge closes.
|
||||
* @param {string} filterName - api filter name (for redux connector)
|
||||
*/
|
||||
const FilterBadge = ({
|
||||
name, value, onClick, showValue,
|
||||
}) => (
|
||||
export const FilterBadge = ({
|
||||
handleClose,
|
||||
config: {
|
||||
displayName,
|
||||
isDefault,
|
||||
hideValue,
|
||||
value,
|
||||
connectedFilters,
|
||||
},
|
||||
}) => !isDefault && (
|
||||
<div>
|
||||
<span className="badge badge-info">
|
||||
<span>
|
||||
{name}{showValue && `: ${value}`}
|
||||
{displayName}{!hideValue && `: ${value}`}
|
||||
</span>
|
||||
<Button
|
||||
className="btn-info"
|
||||
aria-label="close"
|
||||
onClick={onClick}
|
||||
onClick={handleClose(connectedFilters)}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</Button>
|
||||
@@ -31,17 +40,26 @@ const FilterBadge = ({
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
FilterBadge.defaultProps = {
|
||||
showValue: true,
|
||||
};
|
||||
|
||||
FilterBadge.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
showValue: PropTypes.bool,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
// eslint-disable-next-line
|
||||
filterName: PropTypes.string.isRequired,
|
||||
// redux
|
||||
config: PropTypes.shape({
|
||||
connectedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||
displayName: PropTypes.string.isRequired,
|
||||
isDefault: PropTypes.bool.isRequired,
|
||||
hideValue: PropTypes.bool,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default FilterBadge;
|
||||
export const mapStateToProps = (state, ownProps) => ({
|
||||
config: selectors.root.filterBadgeConfig(state, ownProps.filterName),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(FilterBadge);
|
||||
|
||||
93
src/components/FilterBadges/FilterBadge.test.jsx
Normal file
93
src/components/FilterBadges/FilterBadge.test.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import selectors from 'data/selectors';
|
||||
import { FilterBadge, mapStateToProps } from './FilterBadge';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
root: {
|
||||
filterBadgeConfig: jest.fn(state => ({ filterBadgeConfig: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FilterBadge', () => {
|
||||
describe('component', () => {
|
||||
const config = {
|
||||
displayName: 'a common name',
|
||||
isDefault: false,
|
||||
hideValue: false,
|
||||
value: 'a common value',
|
||||
connectedFilters: ['some', 'filters'],
|
||||
};
|
||||
const filterName = 'api.filter.name';
|
||||
let handleClose;
|
||||
let el;
|
||||
let props;
|
||||
beforeEach(() => {
|
||||
handleClose = (filters) => ({ handleClose: filters });
|
||||
props = { filterName, handleClose, config };
|
||||
});
|
||||
describe('with default value', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(
|
||||
<FilterBadge {...props} config={{ ...config, isDefault: true }} />,
|
||||
);
|
||||
});
|
||||
test('snapshot - empty', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('does not display', () => {
|
||||
expect(el).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('with non-default value (active)', () => {
|
||||
describe('if hideValue is true', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(
|
||||
<FilterBadge {...props} config={{ ...config, hideValue: true }} />,
|
||||
);
|
||||
});
|
||||
test('snapshot - shows displayName but not value in span', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('shows displayName but not value in span', () => {
|
||||
expect(el.find('span.badge').childAt(0).text()).toEqual(config.displayName);
|
||||
});
|
||||
it('calls a handleClose event for connected filters on button click', () => {
|
||||
expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
|
||||
});
|
||||
});
|
||||
describe('if hideValue is false (default)', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilterBadge {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('shows displayName and value in span', () => {
|
||||
expect(el.find('span.badge').childAt(0).text()).toEqual(
|
||||
`${config.displayName}: ${config.value}`,
|
||||
);
|
||||
});
|
||||
it('calls a handleClose event for connected filters on button click', () => {
|
||||
expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'kind', of: 'alien' };
|
||||
const filterName = 'Lilu Dallas Multipass';
|
||||
test('config loads config from root.filterBadgeConfig with ownProps.filterName', () => {
|
||||
const { config } = mapStateToProps(testState, { filterName });
|
||||
expect(config).toEqual(selectors.root.filterBadgeConfig(testState, filterName));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import initialFilters from 'data/constants/filters';
|
||||
|
||||
import FilterBadge from './FilterBadge';
|
||||
|
||||
/**
|
||||
* RangeFilterBadge
|
||||
* Simple override to base FilterBadge component for range-value types.
|
||||
* Only displays if either filter is not at its default value
|
||||
* @param {string} displayName - string to display as filter name
|
||||
* @param {string} filterName1 - 1st filter name/key in the data model
|
||||
* @param {string/bool} filterValue1 - 1st filterValue
|
||||
* @param {string} filterName2 - 2nd filter name/key in the data model
|
||||
* @param {string/bool} filterValue2 - 2nd filterValue
|
||||
* @param {func} handleBadgeClose - filter close/reset event
|
||||
*/
|
||||
const RangeFilterBadge = ({
|
||||
displayName,
|
||||
filterName1,
|
||||
filterValue1,
|
||||
filterName2,
|
||||
filterValue2,
|
||||
handleBadgeClose,
|
||||
}) => (
|
||||
(
|
||||
(filterValue1 !== initialFilters[filterName1])
|
||||
|| (filterValue2 !== initialFilters[filterName2])
|
||||
) && (
|
||||
<FilterBadge
|
||||
name={displayName}
|
||||
value={`${filterValue1} - ${filterValue2}`}
|
||||
onClick={handleBadgeClose}
|
||||
/>
|
||||
)
|
||||
);
|
||||
RangeFilterBadge.propTypes = {
|
||||
displayName: PropTypes.string.isRequired,
|
||||
filterName1: PropTypes.string.isRequired,
|
||||
filterValue1: PropTypes.string.isRequired,
|
||||
filterName2: PropTypes.string.isRequired,
|
||||
filterValue2: PropTypes.string.isRequired,
|
||||
handleBadgeClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default RangeFilterBadge;
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import initialFilters from 'data/constants/filters';
|
||||
|
||||
import FilterBadge from './FilterBadge';
|
||||
|
||||
/**
|
||||
* SingleValueFilterBadge
|
||||
* Simple override to base FilterBadge component for single-value filter types
|
||||
* Only displays if the filter is not at its default value
|
||||
* @param {string} displayName - string to display as filter name
|
||||
* @param {string} filterName - filter name/key in the data model
|
||||
* @param {string/bool} filterValue - filterValue
|
||||
* @param {func} handleBadgeClose - filter close/reset event
|
||||
* @param {bool} showValue - should show value string?
|
||||
*/
|
||||
const SingleValueFilterBadge = ({
|
||||
displayName,
|
||||
filterName,
|
||||
filterValue,
|
||||
handleBadgeClose,
|
||||
showValue,
|
||||
}) => (
|
||||
(filterValue !== initialFilters[filterName]) && (
|
||||
<FilterBadge
|
||||
name={displayName}
|
||||
value={filterValue}
|
||||
onClick={handleBadgeClose}
|
||||
showValue={showValue}
|
||||
/>
|
||||
)
|
||||
);
|
||||
SingleValueFilterBadge.defaultProps = {
|
||||
showValue: true,
|
||||
};
|
||||
SingleValueFilterBadge.propTypes = {
|
||||
displayName: PropTypes.string.isRequired,
|
||||
filterName: PropTypes.string.isRequired,
|
||||
filterValue: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]).isRequired,
|
||||
handleBadgeClose: PropTypes.func.isRequired,
|
||||
showValue: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SingleValueFilterBadge;
|
||||
@@ -0,0 +1,66 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilterBadge component with default value snapshot - empty 1`] = `""`;
|
||||
|
||||
exports[`FilterBadge component with non-default value (active) if hideValue is false (default) snapshot 1`] = `
|
||||
<div>
|
||||
<span
|
||||
className="badge badge-info"
|
||||
>
|
||||
<span>
|
||||
a common name
|
||||
: a common value
|
||||
</span>
|
||||
<Button
|
||||
aria-label="close"
|
||||
className="btn-info"
|
||||
onClick={
|
||||
Object {
|
||||
"handleClose": Array [
|
||||
"some",
|
||||
"filters",
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
<br />
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FilterBadge component with non-default value (active) if hideValue is true snapshot - shows displayName but not value in span 1`] = `
|
||||
<div>
|
||||
<span
|
||||
className="badge badge-info"
|
||||
>
|
||||
<span>
|
||||
a common name
|
||||
</span>
|
||||
<Button
|
||||
aria-label="close"
|
||||
className="btn-info"
|
||||
onClick={
|
||||
Object {
|
||||
"handleClose": Array [
|
||||
"some",
|
||||
"filters",
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
<br />
|
||||
</div>
|
||||
`;
|
||||
21
src/components/FilterBadges/__snapshots__/test.jsx.snap
Normal file
21
src/components/FilterBadges/__snapshots__/test.jsx.snap
Normal file
@@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilterBadges component snapshot - has a filterbadge with handleClose for each filter in badgeOrder 1`] = `
|
||||
<div>
|
||||
<FilterBadge
|
||||
filterName="filter1"
|
||||
handleClose={[MockFunction this.props.handleClose]}
|
||||
key="filter1"
|
||||
/>
|
||||
<FilterBadge
|
||||
filterName="filter2"
|
||||
handleClose={[MockFunction this.props.handleClose]}
|
||||
key="filter2"
|
||||
/>
|
||||
<FilterBadge
|
||||
filterName="filter3"
|
||||
handleClose={[MockFunction this.props.handleClose]}
|
||||
key="filter3"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,123 +1,25 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import initialFilters from 'data/constants/filters';
|
||||
import selectors from 'data/selectors';
|
||||
import { badgeOrder } from 'data/constants/filters';
|
||||
|
||||
import RangeFilterBadge from './RangeFilterBadge';
|
||||
import SingleValueFilterBadge from './SingleValueFilterBadge';
|
||||
import FilterBadge from './FilterBadge';
|
||||
|
||||
/**
|
||||
* FilterBadges
|
||||
* Displays a FilterBadge for each filter type in the data model with their current values.
|
||||
* @param {func} handleFilterBadgeClose - event taking a list of filternames to reset
|
||||
* @param {func} handleClose - event taking a list of filternames to reset
|
||||
*/
|
||||
export const FilterBadges = ({
|
||||
assignment,
|
||||
assignmentType,
|
||||
cohortEntry,
|
||||
trackEntry,
|
||||
assignmentGradeMin,
|
||||
assignmentGradeMax,
|
||||
courseGradeMin,
|
||||
courseGradeMax,
|
||||
includeCourseRoleMembers,
|
||||
handleFilterBadgeClose,
|
||||
}) => (
|
||||
export const FilterBadges = ({ handleClose }) => (
|
||||
<div>
|
||||
<SingleValueFilterBadge
|
||||
displayName="Assignment Type"
|
||||
filterName="assignmentType"
|
||||
filterValue={assignmentType}
|
||||
handleBadgeClose={handleFilterBadgeClose(['assignmentType'])}
|
||||
/>
|
||||
<SingleValueFilterBadge
|
||||
displayName="Assignment"
|
||||
filterName="assignment"
|
||||
filterValue={assignment}
|
||||
handleBadgeClose={handleFilterBadgeClose([
|
||||
'assignment',
|
||||
'assignmentGradeMax',
|
||||
'assignmentGradeMin',
|
||||
])}
|
||||
/>
|
||||
<RangeFilterBadge
|
||||
displayName="Assignment Grade"
|
||||
filterName1="assignmentGradeMin"
|
||||
filterValue1={assignmentGradeMin}
|
||||
filterName2="assignmentGradeMax"
|
||||
filterValue2={assignmentGradeMax}
|
||||
handleBadgeClose={handleFilterBadgeClose(['assignmentGradeMin', 'assignmentGradeMax'])}
|
||||
/>
|
||||
<RangeFilterBadge
|
||||
displayName="Course Grade"
|
||||
filterName1="courseGradeMin"
|
||||
filterValue1={courseGradeMin}
|
||||
filterName2="courseGradeMax"
|
||||
filterValue2={courseGradeMax}
|
||||
handleBadgeClose={handleFilterBadgeClose(['courseGradeMin', 'courseGradeMax'])}
|
||||
/>
|
||||
<SingleValueFilterBadge
|
||||
displayName="Track"
|
||||
filterName="track"
|
||||
filterValue={trackEntry.name}
|
||||
handleBadgeClose={handleFilterBadgeClose(['track'])}
|
||||
/>
|
||||
<SingleValueFilterBadge
|
||||
displayName="Cohort"
|
||||
filterName="cohort"
|
||||
filterValue={cohortEntry.name}
|
||||
handleBadgeClose={handleFilterBadgeClose(['cohort'])}
|
||||
/>
|
||||
<SingleValueFilterBadge
|
||||
displayName="Including Course Team Members"
|
||||
filterName="includeCourseRoleMembers"
|
||||
filterValue={includeCourseRoleMembers}
|
||||
showValue={false}
|
||||
handleBadgeClose={handleFilterBadgeClose(['includeCourseRoleMembers'])}
|
||||
/>
|
||||
{badgeOrder.map(filterName => (
|
||||
<FilterBadge key={filterName} {...{ handleClose, filterName }} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
FilterBadges.defaultProps = {
|
||||
assignment: initialFilters.assignmentType,
|
||||
assignmentType: initialFilters.assignmentType,
|
||||
cohortEntry: { name: '' },
|
||||
trackEntry: { name: '' },
|
||||
assignmentGradeMin: initialFilters.assignmentGradeMin,
|
||||
assignmentGradeMax: initialFilters.assignmentGradeMax,
|
||||
courseGradeMin: initialFilters.courseGradeMin,
|
||||
courseGradeMax: initialFilters.courseGradeMax,
|
||||
includeCourseRoleMembers: initialFilters.includeCourseRoleMembers,
|
||||
};
|
||||
FilterBadges.propTypes = {
|
||||
handleFilterBadgeClose: PropTypes.func.isRequired,
|
||||
|
||||
// redux
|
||||
assignment: PropTypes.string,
|
||||
assignmentType: PropTypes.string,
|
||||
cohortEntry: PropTypes.shape({ name: PropTypes.string }),
|
||||
trackEntry: PropTypes.shape({ name: PropTypes.string }),
|
||||
assignmentGradeMin: PropTypes.string,
|
||||
assignmentGradeMax: PropTypes.string,
|
||||
courseGradeMin: PropTypes.string,
|
||||
courseGradeMax: PropTypes.string,
|
||||
includeCourseRoleMembers: PropTypes.bool,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => (
|
||||
{
|
||||
assignment: selectors.filters.selectedAssignmentLabel(state),
|
||||
assignmentType: selectors.filters.assignmentType(state),
|
||||
cohortEntry: selectors.root.selectedCohortEntry(state),
|
||||
trackEntry: selectors.root.selectedTrackEntry(state),
|
||||
assignmentGradeMin: selectors.filters.assignmentGradeMin(state),
|
||||
assignmentGradeMax: selectors.filters.assignmentGradeMax(state),
|
||||
courseGradeMin: selectors.filters.courseGradeMin(state),
|
||||
courseGradeMax: selectors.filters.courseGradeMax(state),
|
||||
includeCourseRoleMembers: selectors.filters.includeCourseRoleMembers(state),
|
||||
}
|
||||
);
|
||||
|
||||
export default connect(mapStateToProps)(FilterBadges);
|
||||
export default FilterBadges;
|
||||
|
||||
33
src/components/FilterBadges/test.jsx
Normal file
33
src/components/FilterBadges/test.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import * as constants from 'data/constants/filters';
|
||||
import FilterBadges from '.';
|
||||
import FilterBadge from './FilterBadge';
|
||||
|
||||
jest.mock('./FilterBadge', () => 'FilterBadge');
|
||||
|
||||
describe('FilterBadges', () => {
|
||||
describe('component', () => {
|
||||
let el;
|
||||
let handleClose;
|
||||
const order = ['filter1', 'filter2', 'filter3'];
|
||||
beforeEach(() => {
|
||||
handleClose = jest.fn().mockName('this.props.handleClose');
|
||||
constants.badgeOrder = order;
|
||||
el = shallow(<FilterBadges handleClose={handleClose} />);
|
||||
});
|
||||
test('snapshot - has a filterbadge with handleClose for each filter in badgeOrder', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('has a filterbadge with handleClose for each filter in badgeOrder', () => {
|
||||
const badgeProps = el.find(FilterBadge).map(badgeEl => badgeEl.props());
|
||||
// key prop is not rendered by react
|
||||
expect(badgeProps[0]).toEqual({ filterName: order[0], handleClose });
|
||||
expect(badgeProps[1]).toEqual({ filterName: order[1], handleClose });
|
||||
expect(badgeProps[2]).toEqual({ filterName: order[2], handleClose });
|
||||
expect(badgeProps.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ exports[`GradesTab Component snapshots basic snapshot 1`] = `
|
||||
<SpinnerIcon />
|
||||
<SearchControls />
|
||||
<FilterBadges
|
||||
handleFilterBadgeClose={[MockFunction this.handleFilterBadgeClose]}
|
||||
handleClose={[MockFunction this.handleFilterBadgeClose]}
|
||||
/>
|
||||
<StatusAlerts />
|
||||
<h4>
|
||||
|
||||
@@ -40,7 +40,7 @@ export class GradesTab extends React.Component {
|
||||
<>
|
||||
<SpinnerIcon />
|
||||
<SearchControls />
|
||||
<FilterBadges handleFilterBadgeClose={this.handleFilterBadgeClose} />
|
||||
<FilterBadges handleClose={this.handleFilterBadgeClose} />
|
||||
<StatusAlerts />
|
||||
|
||||
<h4>Step 2: View or Modify Individual Grades</h4>
|
||||
|
||||
@@ -1,13 +1,73 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export const filters = StrictDict({
|
||||
assignment: 'assignment',
|
||||
assignmentGrade: 'assignmentGrade',
|
||||
assignmentGradeMax: 'assignmentGradeMax',
|
||||
assignmentGradeMin: 'assignmentGradeMin',
|
||||
assignmentType: 'assignmentType',
|
||||
cohort: 'cohort',
|
||||
courseGrade: 'courseGrade',
|
||||
courseGradeMax: 'courseGradeMax',
|
||||
courseGradeMin: 'courseGradeMin',
|
||||
includeCourseRoleMembers: 'includeCourseRoleMembers',
|
||||
track: 'track',
|
||||
});
|
||||
|
||||
const initialFilters = {
|
||||
assignment: '',
|
||||
assignmentType: '',
|
||||
track: '',
|
||||
cohort: '',
|
||||
assignmentGradeMin: '0',
|
||||
assignmentGradeMax: '100',
|
||||
courseGradeMin: '0',
|
||||
courseGradeMax: '100',
|
||||
includeCourseRoleMembers: false,
|
||||
[filters.assignment]: '',
|
||||
[filters.assignmentGradeMax]: '100',
|
||||
[filters.assignmentGradeMin]: '0',
|
||||
[filters.assignmentType]: '',
|
||||
[filters.cohort]: '',
|
||||
[filters.courseGradeMax]: '100',
|
||||
[filters.courseGradeMin]: '0',
|
||||
[filters.includeCourseRoleMembers]: false,
|
||||
[filters.track]: '',
|
||||
};
|
||||
|
||||
export const filterConfig = StrictDict({
|
||||
[filters.assignment]: {
|
||||
displayName: 'Assignment',
|
||||
connectedFilters: ['assignment', 'assignmentGradeMax', 'assignmentGradeMax'],
|
||||
},
|
||||
[filters.assignmentType]: {
|
||||
displayName: 'Assignment Type',
|
||||
connectedFilters: ['assignmentType'],
|
||||
},
|
||||
[filters.assignmentGrade]: {
|
||||
displayName: 'Assignment Grade',
|
||||
filterOrder: ['courseGradeMin', 'courseGradeMax'],
|
||||
connectedFilters: ['courseGradeMax', 'courseGradeMin'],
|
||||
},
|
||||
[filters.cohort]: {
|
||||
displayName: 'Cohort',
|
||||
connectedFilters: ['cohort'],
|
||||
},
|
||||
[filters.courseGrade]: {
|
||||
displayName: 'Course Grade',
|
||||
filterOrder: ['courseGradeMin', 'courseGradeMax'],
|
||||
connectedFilters: ['courseGradeMax', 'courseGradeMin'],
|
||||
},
|
||||
[filters.includeCourseRoleMembers]: {
|
||||
displayName: 'Includeing Course Team Members',
|
||||
connectedFilters: ['includeCourseRoleMembers'],
|
||||
hideValue: true,
|
||||
},
|
||||
[filters.track]: {
|
||||
displayName: 'Track',
|
||||
connectedFilters: ['track'],
|
||||
},
|
||||
});
|
||||
|
||||
export const badgeOrder = [
|
||||
filters.assignmentType,
|
||||
filters.assignment,
|
||||
filters.assignmentGrade,
|
||||
filters.courseGrade,
|
||||
filters.track,
|
||||
filters.cohort,
|
||||
filters.includeCourseRoleMembers,
|
||||
];
|
||||
|
||||
export default initialFilters;
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
/* eslint-disable import/no-self-import */
|
||||
import { StrictDict } from 'utils';
|
||||
import * as module from './filters';
|
||||
|
||||
import initialFilters from 'data/constants/filters';
|
||||
import simpleSelectorFactory from '../utils';
|
||||
import * as module from './filters';
|
||||
|
||||
// Transformers
|
||||
/**
|
||||
* isDefault(name, value)
|
||||
* returns true iff the value is equal to the initial filter value
|
||||
* associated with the given name
|
||||
* @param {string} name - api filter name
|
||||
* @param {string/number/bool} value - filter value
|
||||
* @return {bool} - is this the default value for the given filter?
|
||||
*/
|
||||
export const isDefault = (name, value) => (
|
||||
value === initialFilters[name]
|
||||
);
|
||||
|
||||
/**
|
||||
* chooseRelevantAssignmentData(assignment)
|
||||
* formats the assignment api data for an assignment object for consumption
|
||||
@@ -120,6 +134,7 @@ export const selectedAssignmentLabel = (state) => (simpleSelectors.assignment(st
|
||||
|
||||
export default StrictDict({
|
||||
...simpleSelectors,
|
||||
isDefault,
|
||||
relevantAssignmentDataFromResults,
|
||||
selectedAssignmentId,
|
||||
selectedAssignmentLabel,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import initialFilters, { filters as filterNames } from 'data/constants/filters';
|
||||
// import * in order to mock in-file references
|
||||
import * as selectors from './filters';
|
||||
// import default export in order to test simpleSelectors not exported individually
|
||||
@@ -11,27 +12,27 @@ const selectedAssignmentInfo = {
|
||||
};
|
||||
|
||||
const filters = {
|
||||
assignment: selectedAssignmentInfo,
|
||||
assignmentGradeMax: '100',
|
||||
assignmentGradeMin: '0',
|
||||
assignmentType: 'Homework',
|
||||
cohort: 'Spring Term',
|
||||
courseGradeMax: '100',
|
||||
courseGradeMin: '0',
|
||||
includeCourseRoleMembers: false,
|
||||
track: 'masters',
|
||||
[filterNames.assignment]: selectedAssignmentInfo,
|
||||
[filterNames.assignmentGradeMax]: '100',
|
||||
[filterNames.assignmentGradeMin]: '0',
|
||||
[filterNames.assignmentType]: 'Homework',
|
||||
[filterNames.cohort]: 'Spring Term',
|
||||
[filterNames.courseGradeMax]: '100',
|
||||
[filterNames.courseGradeMin]: '0',
|
||||
[filterNames.includeCourseRoleMembers]: false,
|
||||
[filterNames.track]: 'masters',
|
||||
};
|
||||
|
||||
const noFilters = {
|
||||
assignment: undefined,
|
||||
assignmentGradeMax: '100',
|
||||
assignmentGradeMin: '0',
|
||||
assignmentType: 'All',
|
||||
cohort: '',
|
||||
courseGradeMax: '100',
|
||||
courseGradeMin: '0',
|
||||
includeCourseRoleMembers: false,
|
||||
track: '',
|
||||
[filterNames.assignment]: undefined,
|
||||
[filterNames.assignmentGradeMax]: '100',
|
||||
[filterNames.assignmentGradeMin]: '0',
|
||||
[filterNames.assignmentType]: 'All',
|
||||
[filterNames.cohort]: '',
|
||||
[filterNames.courseGradeMax]: '100',
|
||||
[filterNames.courseGradeMin]: '0',
|
||||
[filterNames.includeCourseRoleMembers]: false,
|
||||
[filterNames.track]: '',
|
||||
};
|
||||
|
||||
const sectionBreakdowns = [
|
||||
@@ -74,6 +75,17 @@ describe('filters selectors', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDefault', () => {
|
||||
it('returns true iff value is equal to initialFilters[name]', () => {
|
||||
expect(
|
||||
selectors.isDefault(filterNames.assignment, initialFilters[filterNames.assignment]),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
selectors.isDefault(filterNames.assignment, 'bananas'),
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAssignmentsFromResultsSubstate', () => {
|
||||
it('gets section breakdowns from state', () => {
|
||||
const assignments = selectors.getAssignmentsFromResultsSubstate(gradesData.results);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* eslint-disable import/no-named-as-default-member, import/no-self-import */
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import LmsApiService from 'data/services/LmsApiService';
|
||||
import * as filterConstants from 'data/constants/filters';
|
||||
|
||||
import * as module from '.';
|
||||
import app from './app';
|
||||
@@ -12,6 +14,11 @@ import roles from './roles';
|
||||
import special from './special';
|
||||
import tracks from './tracks';
|
||||
|
||||
const {
|
||||
filterConfig,
|
||||
filters: filterNames,
|
||||
} = filterConstants;
|
||||
|
||||
/**
|
||||
* editModalPossibleGrade(state)
|
||||
* Returns the "possible" grade as shown in the edit modal.
|
||||
@@ -22,6 +29,58 @@ export const editModalPossibleGrade = (state) => (
|
||||
app.modalState.adjustedGradePossible(state) || grades.gradeOriginalPossibleGraded(state)
|
||||
);
|
||||
|
||||
/**
|
||||
* filterBadgeConfig(state, name)
|
||||
* Takes a filter name and returns the appropriate badge config, with value and isDefault.
|
||||
* Determines if it should return a range or single-value config based on the presence of
|
||||
* a filterOrder prop in the filter config associated with the passed name.
|
||||
* @param {object} state - redux state
|
||||
* @param {string} name - api filter name
|
||||
*/
|
||||
export const filterBadgeConfig = (state, name) => {
|
||||
const filterValue = module.filterBadgeValues[name](state);
|
||||
const { filterOrder, ...config } = filterConfig[name];
|
||||
const isRange = !!filterOrder;
|
||||
const value = isRange ? `${filterValue[0]} - ${filterValue[1]}` : filterValue;
|
||||
const isDefault = (isRange
|
||||
? (
|
||||
filters.isDefault(filterOrder[0], filterValue[0])
|
||||
&& filters.isDefault(filterOrder[1], filterValue[1])
|
||||
)
|
||||
: filters.isDefault(name, filterValue)
|
||||
);
|
||||
return { ...config, value, isDefault };
|
||||
};
|
||||
|
||||
/**
|
||||
* filterBadgeValues methods
|
||||
* For each filter type with an associated badge, provides a selector that returns the
|
||||
* content of that badge
|
||||
*/
|
||||
export const filterBadgeValues = StrictDict({
|
||||
[filterNames.assignment]: (state) => (
|
||||
filters.selectedAssignmentLabel(state) || ''
|
||||
),
|
||||
[filterNames.assignmentType]: filters.assignmentType,
|
||||
[filterNames.includeCourseRoleMembers]: filters.includeCourseRoleMembers,
|
||||
[filterNames.cohort]: (state) => {
|
||||
const entry = module.selectedCohortEntry(state);
|
||||
return entry ? entry.name : '';
|
||||
},
|
||||
[filterNames.track]: (state) => {
|
||||
const entry = module.selectedTrackEntry(state);
|
||||
return entry ? entry.name : '';
|
||||
},
|
||||
[filterNames.assignmentGrade]: (state) => ([
|
||||
filters.assignmentGradeMin(state),
|
||||
filters.assignmentGradeMax(state),
|
||||
]),
|
||||
[filterNames.courseGrade]: (state) => ([
|
||||
filters.courseGradeMin(state),
|
||||
filters.courseGradeMax(state),
|
||||
]),
|
||||
});
|
||||
|
||||
/**
|
||||
* formattedGradeLimits(state)
|
||||
* Returns an object of local grade limits, formatted for fetching.
|
||||
@@ -177,6 +236,7 @@ export const showBulkManagement = (state) => (
|
||||
export default StrictDict({
|
||||
root: StrictDict({
|
||||
editModalPossibleGrade,
|
||||
filterBadgeConfig,
|
||||
getHeadings,
|
||||
gradeExportUrl,
|
||||
interventionExportUrl,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable import/no-named-as-default-member */
|
||||
import * as filterConstants from '../constants/filters';
|
||||
import selectors from '.';
|
||||
import * as moduleSelectors from '.';
|
||||
import { minGrade, maxGrade } from './grades';
|
||||
@@ -18,6 +19,7 @@ jest.mock('../services/LmsApiService', () => ({
|
||||
const mockFn = (key) => jest.fn((state) => ({ [key]: state }));
|
||||
const mockMetaFn = (key) => jest.fn((...args) => ({ [key]: { args } }));
|
||||
const testState = { a: 'test', state: 'of', random: 'data' };
|
||||
const testVal = 'bananas';
|
||||
|
||||
describe('root selectors', () => {
|
||||
const testCourseId = 'OxfordX+Time+Travel';
|
||||
@@ -42,6 +44,207 @@ describe('root selectors', () => {
|
||||
expect(selector(testState)).toEqual(grade2);
|
||||
});
|
||||
});
|
||||
describe('filterBadgeConfig', () => {
|
||||
let config;
|
||||
const filterName = 'seasoning';
|
||||
const filters = ['withSalt', 'withGarlic'];
|
||||
|
||||
const badgeValues = moduleSelectors.filterBadgeValues;
|
||||
const { isDefault } = selectors.filters;
|
||||
const oldConfig = filterConstants.filterConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
selectors.filters.isDefault = jest.fn();
|
||||
});
|
||||
afterEach(() => {
|
||||
moduleSelectors.filterBadgeValues = badgeValues;
|
||||
selectors.filters.isDefault = isDefault;
|
||||
filterConstants.filterConfig = oldConfig;
|
||||
});
|
||||
describe('range filter (filterOrder in config)', () => {
|
||||
const values = [3.14, 42];
|
||||
const valueFn = () => values;
|
||||
const testConfig = {
|
||||
fake: 'config',
|
||||
fields: 'for tests',
|
||||
filterOrder: filters,
|
||||
};
|
||||
beforeEach(() => {
|
||||
moduleSelectors.filterBadgeValues = { [filterName]: valueFn };
|
||||
});
|
||||
describe('if both are default values', () => {
|
||||
beforeEach(() => {
|
||||
filterConstants.filterConfig[filterName] = testConfig;
|
||||
selectors.filters.isDefault.mockReturnValue(true);
|
||||
config = selectors.root.filterBadgeConfig(testState, filterName);
|
||||
});
|
||||
it('returns isDefault: true, string value, and remaining filterConfig', () => {
|
||||
const { filterOrder, ...rest } = testConfig;
|
||||
expect(config).toEqual({
|
||||
isDefault: true,
|
||||
value: `${values[0]} - ${values[1]}`,
|
||||
...rest,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('if neither/only 1 are default values', () => {
|
||||
beforeEach(() => {
|
||||
filterConstants.filterConfig[filterName] = testConfig;
|
||||
config = selectors.root.filterBadgeConfig(testState, filterName);
|
||||
});
|
||||
describe.each([
|
||||
['neither', () => false],
|
||||
['only filter1', (v) => v === filters[0]],
|
||||
['only filter2', (v) => v === filters[1]],
|
||||
], '%1 is default', (label, isDefaultFn) => {
|
||||
it('returns isDefault: false, string value, and remaining filterConfig', () => {
|
||||
selectors.filters.isDefault.mockImplementation(isDefaultFn);
|
||||
const { filterOrder, ...rest } = testConfig;
|
||||
expect(config).toEqual({
|
||||
isDefault: false,
|
||||
value: `${values[0]} - ${values[1]}`,
|
||||
...rest,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('single-value filter', () => {
|
||||
const value = 3.14;
|
||||
const valueFn = () => value;
|
||||
const testConfig = {
|
||||
fake: 'config',
|
||||
fields: 'for tests',
|
||||
};
|
||||
beforeEach(() => {
|
||||
filterConstants.filterConfig[filterName] = testConfig;
|
||||
moduleSelectors.filterBadgeValues = { [filterName]: valueFn };
|
||||
});
|
||||
describe('if is default', () => {
|
||||
beforeEach(() => {
|
||||
selectors.filters.isDefault.mockReturnValue(true);
|
||||
config = selectors.root.filterBadgeConfig(testState, filterName);
|
||||
});
|
||||
it('returns isDefault: true, string value, and filterConfig values', () => {
|
||||
expect(config).toEqual({
|
||||
isDefault: true,
|
||||
value,
|
||||
...testConfig,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('if is not default', () => {
|
||||
beforeEach(() => {
|
||||
selectors.filters.isDefault.mockReturnValue(false);
|
||||
config = selectors.root.filterBadgeConfig(testState, filterName);
|
||||
});
|
||||
it('returns isDefault: true, string value, and filterConfig values', () => {
|
||||
expect(config).toEqual({
|
||||
isDefault: false,
|
||||
value,
|
||||
...testConfig,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('filterBadgeValues', () => {
|
||||
const mockSelector = (obj, name, key) => (
|
||||
jest.spyOn(obj, name).mockImplementation(state => state[key])
|
||||
);
|
||||
describe('assignment', () => {
|
||||
let mock;
|
||||
const selector = moduleSelectors.filterBadgeValues.assignment;
|
||||
beforeEach(() => {
|
||||
mock = mockSelector(selectors.filters, 'selectedAssignmentLabel', 'value');
|
||||
});
|
||||
afterEach(() => {
|
||||
mock.mockRestore();
|
||||
});
|
||||
it('returns selectedAssignmentLabel if there is one selected', () => {
|
||||
expect(selector({ value: testVal })).toEqual(testVal);
|
||||
});
|
||||
it('returns empty string if no assignment is selected', () => {
|
||||
expect(selector({ value: null })).toEqual('');
|
||||
});
|
||||
});
|
||||
describe('assignmentType', () => {
|
||||
it('returns assignmentType filter', () => {
|
||||
expect(
|
||||
moduleSelectors.filterBadgeValues.assignmentType,
|
||||
).toEqual(selectors.filters.assignmentType);
|
||||
});
|
||||
});
|
||||
describe('includeCourseRoleMembers', () => {
|
||||
it('returns includeCourseRoleMembers filter', () => {
|
||||
expect(
|
||||
moduleSelectors.filterBadgeValues.includeCourseRoleMembers,
|
||||
).toEqual(selectors.filters.includeCourseRoleMembers);
|
||||
});
|
||||
});
|
||||
describe('cohort', () => {
|
||||
let mock;
|
||||
const selector = moduleSelectors.filterBadgeValues.cohort;
|
||||
beforeEach(() => {
|
||||
mock = mockSelector(moduleSelectors, 'selectedCohortEntry', 'entry');
|
||||
});
|
||||
afterEach(() => {
|
||||
mock.mockRestore();
|
||||
});
|
||||
it('returns selectedCohortEntry name if one is selected', () => {
|
||||
expect(selector({ entry: { name: testVal } })).toEqual(testVal);
|
||||
});
|
||||
it('returns empty string if no cohort selected', () => {
|
||||
expect(selector({ entry: undefined })).toEqual('');
|
||||
});
|
||||
});
|
||||
describe('track', () => {
|
||||
let mock;
|
||||
const selector = moduleSelectors.filterBadgeValues.track;
|
||||
beforeEach(() => {
|
||||
mock = mockSelector(moduleSelectors, 'selectedTrackEntry', 'entry');
|
||||
});
|
||||
afterEach(() => {
|
||||
mock.mockRestore();
|
||||
});
|
||||
it('returns selectedTrackEntry name if one is selected', () => {
|
||||
expect(selector({ entry: { name: testVal } })).toEqual(testVal);
|
||||
});
|
||||
it('returns empty string if no track selected', () => {
|
||||
expect(selector({ entry: undefined })).toEqual('');
|
||||
});
|
||||
});
|
||||
describe('assignmentGrade', () => {
|
||||
const selector = moduleSelectors.filterBadgeValues.assignmentGrade;
|
||||
it('returns [filters.assignmentGradeMin, filters.assignmentGradeMax]', () => {
|
||||
jest.spyOn(selectors.filters, 'assignmentGradeMin').mockImplementation(
|
||||
state => ({ assignmentGradeMin: state }),
|
||||
);
|
||||
jest.spyOn(selectors.filters, 'assignmentGradeMax').mockImplementation(
|
||||
state => ({ assignmentGradeMax: state }),
|
||||
);
|
||||
expect(selector(testState)).toEqual([
|
||||
selectors.filters.assignmentGradeMin(testState),
|
||||
selectors.filters.assignmentGradeMax(testState),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('courseGrade', () => {
|
||||
const selector = moduleSelectors.filterBadgeValues.courseGrade;
|
||||
it('returns [filters.courseGradeMin, filters.courseGradeMax]', () => {
|
||||
jest.spyOn(selectors.filters, 'courseGradeMin').mockImplementation(
|
||||
state => ({ courseGradeMin: state }),
|
||||
);
|
||||
jest.spyOn(selectors.filters, 'courseGradeMax').mockImplementation(
|
||||
state => ({ courseGradeMax: state }),
|
||||
);
|
||||
expect(selector(testState)).toEqual([
|
||||
selectors.filters.courseGradeMin(testState),
|
||||
selectors.filters.courseGradeMax(testState),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('formattedGradeLimits', () => {
|
||||
const selector = moduleSelectors.formattedGradeLimits;
|
||||
const mockAssgn = (assignmentGradeMax, assignmentGradeMin) => {
|
||||
|
||||
Reference in New Issue
Block a user