refactor: unit test and docstring FilterBadges (#193)

* unit test and docstring FilterBadges

* v1.4.38
This commit is contained in:
Ben Warzeski
2021-06-16 15:34:50 -04:00
committed by GitHub
parent 02c154ef50
commit 868048381b
16 changed files with 642 additions and 254 deletions

View File

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

View File

@@ -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">&times;</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);

View 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));
});
});
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
`;

View 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>
`;

View File

@@ -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;

View 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);
});
});
});

View File

@@ -5,7 +5,7 @@ exports[`GradesTab Component snapshots basic snapshot 1`] = `
<SpinnerIcon />
<SearchControls />
<FilterBadges
handleFilterBadgeClose={[MockFunction this.handleFilterBadgeClose]}
handleClose={[MockFunction this.handleFilterBadgeClose]}
/>
<StatusAlerts />
<h4>

View File

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

View File

@@ -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;

View File

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

View File

@@ -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);

View File

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

View File

@@ -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) => {