diff --git a/package.json b/package.json index 1ce3dc5..6f80735 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/FilterBadges/FilterBadge.jsx b/src/components/FilterBadges/FilterBadge.jsx index a5e643c..a55efe0 100644 --- a/src/components/FilterBadges/FilterBadge.jsx +++ b/src/components/FilterBadges/FilterBadge.jsx @@ -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 && (
- {name}{showValue && `: ${value}`} + {displayName}{!hideValue && `: ${value}`} @@ -31,17 +40,26 @@ const FilterBadge = ({
); -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); diff --git a/src/components/FilterBadges/FilterBadge.test.jsx b/src/components/FilterBadges/FilterBadge.test.jsx new file mode 100644 index 0000000..f09ba24 --- /dev/null +++ b/src/components/FilterBadges/FilterBadge.test.jsx @@ -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( + , + ); + }); + 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( + , + ); + }); + 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(); + }); + 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)); + }); + }); +}); diff --git a/src/components/FilterBadges/RangeFilterBadge.jsx b/src/components/FilterBadges/RangeFilterBadge.jsx deleted file mode 100644 index 50cf51e..0000000 --- a/src/components/FilterBadges/RangeFilterBadge.jsx +++ /dev/null @@ -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]) - ) && ( - - ) -); -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; diff --git a/src/components/FilterBadges/SingleValueFilterBadge.jsx b/src/components/FilterBadges/SingleValueFilterBadge.jsx deleted file mode 100644 index 0273d5a..0000000 --- a/src/components/FilterBadges/SingleValueFilterBadge.jsx +++ /dev/null @@ -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]) && ( - - ) -); -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; diff --git a/src/components/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap b/src/components/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap new file mode 100644 index 0000000..f69754f --- /dev/null +++ b/src/components/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap @@ -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`] = ` +
+ + + a common name + : a common value + + + +
+
+`; + +exports[`FilterBadge component with non-default value (active) if hideValue is true snapshot - shows displayName but not value in span 1`] = ` +
+ + + a common name + + + +
+
+`; diff --git a/src/components/FilterBadges/__snapshots__/test.jsx.snap b/src/components/FilterBadges/__snapshots__/test.jsx.snap new file mode 100644 index 0000000..5d97e37 --- /dev/null +++ b/src/components/FilterBadges/__snapshots__/test.jsx.snap @@ -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`] = ` +
+ + + +
+`; diff --git a/src/components/FilterBadges/index.jsx b/src/components/FilterBadges/index.jsx index 2900733..d6449ec 100644 --- a/src/components/FilterBadges/index.jsx +++ b/src/components/FilterBadges/index.jsx @@ -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 }) => (
- - - - - - - + {badgeOrder.map(filterName => ( + + ))}
); -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; diff --git a/src/components/FilterBadges/test.jsx b/src/components/FilterBadges/test.jsx new file mode 100644 index 0000000..84c54d6 --- /dev/null +++ b/src/components/FilterBadges/test.jsx @@ -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(); + }); + 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); + }); + }); +}); diff --git a/src/components/GradesTab/__snapshots__/test.jsx.snap b/src/components/GradesTab/__snapshots__/test.jsx.snap index 9c9fe20..f3b2627 100644 --- a/src/components/GradesTab/__snapshots__/test.jsx.snap +++ b/src/components/GradesTab/__snapshots__/test.jsx.snap @@ -5,7 +5,7 @@ exports[`GradesTab Component snapshots basic snapshot 1`] = `

diff --git a/src/components/GradesTab/index.jsx b/src/components/GradesTab/index.jsx index e206026..aa67e54 100644 --- a/src/components/GradesTab/index.jsx +++ b/src/components/GradesTab/index.jsx @@ -40,7 +40,7 @@ export class GradesTab extends React.Component { <> - +

Step 2: View or Modify Individual Grades

diff --git a/src/data/constants/filters.js b/src/data/constants/filters.js index 8cfe2c3..1499637 100644 --- a/src/data/constants/filters.js +++ b/src/data/constants/filters.js @@ -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; diff --git a/src/data/selectors/filters.js b/src/data/selectors/filters.js index e154285..68b846a 100644 --- a/src/data/selectors/filters.js +++ b/src/data/selectors/filters.js @@ -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, diff --git a/src/data/selectors/filters.test.js b/src/data/selectors/filters.test.js index 42b3619..3c102da 100644 --- a/src/data/selectors/filters.test.js +++ b/src/data/selectors/filters.test.js @@ -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); diff --git a/src/data/selectors/index.js b/src/data/selectors/index.js index 3b1650f..911fd61 100644 --- a/src/data/selectors/index.js +++ b/src/data/selectors/index.js @@ -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, diff --git a/src/data/selectors/index.test.js b/src/data/selectors/index.test.js index e94924a..00a8c9d 100644 --- a/src/data/selectors/index.test.js +++ b/src/data/selectors/index.test.js @@ -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) => {