app selectors clean up and unit testing

This commit is contained in:
Ben Warzeski
2021-10-04 21:18:07 -04:00
parent 488bc06618
commit 48e32eb323
10 changed files with 312 additions and 39 deletions

View File

@@ -56,7 +56,7 @@ CriterionFeedback.propTypes = {
export const mapStateToProps = (state, { orderNum }) => ({
isGrading: selectors.app.isGrading(state),
config: selectors.app.rubricCriterionFeedbackConfig(state, { orderNum }),
config: selectors.app.rubric.criterionFeedbackConfig(state, { orderNum }),
value: selectors.grading.selected.criterionFeedback(state, { orderNum }),
});

View File

@@ -78,7 +78,7 @@ GradingCriterion.propTypes = {
};
export const mapStateToProps = (state, { orderNum }) => ({
config: selectors.app.rubricCriterionConfig(state, { orderNum }),
config: selectors.app.rubric.criterionConfig(state, { orderNum }),
data: selectors.grading.selected.criterionGradeData(state, { orderNum }),
});

View File

@@ -54,7 +54,7 @@ ReviewCriterion.propTypes = {
};
export const mapStateToProps = (state, { orderNum }) => ({
config: selectors.app.rubricCriterionConfig(state, { orderNum }),
config: selectors.app.rubric.criterionConfig(state, { orderNum }),
data: selectors.grading.selected.criterionGradeData(state, { orderNum }),
});

View File

@@ -73,7 +73,7 @@ CriterionContainer.propTypes = {
};
export const mapStateToProps = (state, { orderNum }) => ({
config: selectors.app.rubricCriterionConfig(state, { orderNum }),
config: selectors.app.rubric.criterionConfig(state, { orderNum }),
});
export const mapDispatchToProps = {

View File

@@ -39,7 +39,7 @@ ListViewBreadcrumb.propTypes = {
export const mapStateToProps = (state) => ({
courseId: selectors.app.courseId(state),
oraName: selectors.app.oraName(state),
oraName: selectors.app.ora.name(state),
});
export const mapDispatchToProps = {

View File

@@ -69,7 +69,7 @@ ReviewModal.propTypes = {
export const mapStateToProps = (state) => ({
isOpen: selectors.app.showReview(state),
oraName: selectors.app.oraName(state),
oraName: selectors.app.ora.name(state),
response: selectors.grading.selected.response(state),
showRubric: selectors.app.showRubric(state),
});

View File

@@ -56,7 +56,7 @@ RubricFeedback.propTypes = {
export const mapStateToProps = (state) => ({
isGrading: selectors.app.isGrading(state),
value: selectors.grading.selected.overallFeedback(state),
config: selectors.app.rubricFeedbackConfig(state),
config: selectors.app.rubric.feedbackConfig(state),
});
export const mapDispatchToProps = {

View File

@@ -48,7 +48,7 @@ Rubric.propTypes = {
export const mapStateToProps = (state) => ({
isGrading: selectors.app.isGrading(state),
criteriaIndices: selectors.app.rubricCriteriaIndices(state),
criteriaIndices: selectors.app.rubric.criteriaIndices(state),
});
export const mapDispatchToProps = {

View File

@@ -6,42 +6,83 @@ import { StrictDict } from 'utils';
import * as module from './app';
export const appSelector = (state) => state.app;
const mkSimpleSelector = (cb) => createSelector([module.appSelector], cb);
// top-level app data selectors
export const simpleSelectors = {
showReview: state => state.app.showReview,
showRubric: state => state.app.showRubric,
isGrading: state => state.app.isGrading,
courseMetadata: state => state.app.courseMetadata,
courseId: state => state.app.courseMetadata.courseId,
oraName: state => state.app.oraMetadata.name,
oraPrompt: state => state.app.oraMetadata.prompt,
oraTypes: state => state.app.oraMetadata.type,
rubricConfig: state => state.app.oraMetadata.rubricConfig,
showReview: mkSimpleSelector(app => app.showReview),
showRubric: mkSimpleSelector(app => app.showRubric),
isGrading: mkSimpleSelector(app => app.isGrading),
courseMetadata: mkSimpleSelector(app => app.courseMetadata),
oraMetadata: mkSimpleSelector(app => app.oraMetadata),
};
export const courseId = (
createSelector([module.simpleSelectors.courseMetadata], (data) => data.courseId)
);
const oraMetadataSelector = (cb) => createSelector([module.simpleSelectors.oraMetadata], cb);
// ORA metadata selectors
export const ora = {
/**
* Returns the ORA name
* @return {string} - ORA name
*/
name: oraMetadataSelector(data => data.name),
/**
* Returns the ORA Prompt
* @return {string} - ORA prompt
*/
prompt: oraMetadataSelector(data => data.prompt),
/**
* Returns the ORA type
* @return {string} - ORA type (team vs individual)
*/
type: oraMetadataSelector(data => data.type),
};
/**
* Container for rubric config selectors
*/
export const rubric = {};
/**
* Returns the full top-level rubric config from the ora metadata
* @return {object} - rubric config object
*/
rubric.config = oraMetadataSelector(data => data.rubricConfig);
/**
* Returns a momoized selector depending on the rubric config with the given callback
* @param {func} cb - callback taking the rubric config as an arg, and returning a value
* @return {func} - a memoized selector that calls cb with the rubric config
*/
const rubricConfigSelector = (cb) => createSelector([module.rubric.config], cb);
/**
* Returns true iff the rubric object has loaded.
* @return {bool} - has a rubric config been loaded?
*/
rubric.hasConfig = rubricConfigSelector(config => config !== undefined);
/**
* Returns the rubric-level feedback config string
* @return {string} - rubric-level feedback config string
*/
export const rubricFeedbackConfig = createSelector(
[module.simpleSelectors.rubricConfig],
(config) => config.feedback,
);
rubric.feedbackConfig = rubricConfigSelector(config => config.feedback);
/**
* Returns a list of rubric criterion config objects for the ORA
* @return {obj[]} - array of criterion config objects
*/
export const criteria = createSelector(
[module.simpleSelectors.rubricConfig],
(config) => config.criteria,
);
rubric.criteria = rubricConfigSelector(config => config.criteria);
/**
* Returns the config object for the rubric criterion at the given index (orderNum)
* @param {number} orderNum - rubric criterion index
* @return {obj} - criterion config object
*/
export const rubricCriterionConfig = (state, { orderNum }) => module.criteria(state)[orderNum];
rubric.criterionConfig = (state, { orderNum }) => module.rubric.criteria(state)[orderNum];
/**
* Returns the feeback configuration string for tor the criterion at the given index
@@ -49,16 +90,16 @@ export const rubricCriterionConfig = (state, { orderNum }) => module.criteria(st
* @param {number} orderNum - rubric criterion index
* @return {string} - criterion feedback config string
*/
export const rubricCriterionFeedbackConfig = (state, { orderNum }) => (
module.rubricCriterionConfig(state, { orderNum }).feedback
rubric.criterionFeedbackConfig = (state, { orderNum }) => (
module.rubric.criterionConfig(state, { orderNum }).feedback
);
/**
* Returns a list of rubric criteria indices for iterating over
* @return {number[]} - list of rubric criteria indices
*/
export const rubricCriteriaIndices = createSelector(
[module.criteria],
rubric.criteriaIndices = createSelector(
[module.rubric.criteria],
(rubricCriteria) => rubricCriteria.map(({ orderNum }) => orderNum),
);
@@ -76,16 +117,16 @@ const shouldIncludeFeedback = (feedback) => ([
* @return {obj} - empty grade data object
*/
export const emptyGrade = createSelector(
[module.simpleSelectors.rubricConfig],
(rubricConfig) => {
if (rubricConfig === undefined) {
[module.rubric.hasConfig, module.rubric.criteria, module.rubric.feedbackConfig],
(hasConfig, criteria, feedbackConfig) => {
if (!hasConfig) {
return null;
}
const gradeData = {};
if (shouldIncludeFeedback(rubricConfig.feedback)) {
if (shouldIncludeFeedback(feedbackConfig)) {
gradeData.overallFeedback = '';
}
gradeData.criteria = rubricConfig.criteria.map(criterion => {
gradeData.criteria = criteria.map(criterion => {
const entry = {
orderNum: criterion.orderNum,
name: criterion.name,
@@ -102,9 +143,8 @@ export const emptyGrade = createSelector(
export default StrictDict({
...simpleSelectors,
courseId,
ora,
rubric: StrictDict(rubric),
emptyGrade,
rubricCriteriaIndices,
rubricCriterionConfig,
rubricCriterionFeedbackConfig,
rubricFeedbackConfig,
});

View File

@@ -0,0 +1,233 @@
import { createSelector } from 'reselect';
import { feedbackRequirement } from 'data/services/lms/constants';
// import * in order to mock in-file references
import * as selectors from './app';
// import default export in order to test simpleSelectors not exported individually
import exportedSelectors from './app';
jest.mock('reselect', () => ({
createSelector: jest.fn((preSelectors, cb) => ({ preSelectors, cb })),
}));
const testState = {
app: {
showReview: false,
showRubric: false,
isGrading: false,
courseMetadata: {
courseId: 'test-course-id',
},
oraMetadata: {
name: 'test-ora-name',
prompt: 'test-ora-prompt',
type: 'test-ora-type',
rubricConfig: {
feedback: 'optional',
criteria: [
{
orderNum: 0,
name: 'critERia0',
feedback: 'optional',
},
{
orderNum: 1,
name: 'critEriA1',
feedback: 'disabled',
},
{
orderNum: 2,
name: 'cRIteria2',
feedback: 'required',
},
],
},
},
},
};
describe('app selectors unit tests', () => {
const { appSelector, simpleSelectors, rubric } = selectors;
describe('appSelector', () => {
it('returns the app data', () => {
expect(appSelector(testState)).toEqual(testState.app);
});
});
describe('simpleSelectors', () => {
const testSimpleSelector = (key) => {
const { preSelectors, cb } = simpleSelectors[key];
expect(preSelectors).toEqual([appSelector]);
expect(cb(testState.app)).toEqual(testState.app[key]);
};
test('simple selectors link their values from app store', () => {
[
'showReview',
'showRubric',
'isGrading',
'courseMetadata',
'oraMetadata',
].map(testSimpleSelector);
});
});
const testReselect = ({
selector,
preSelectors,
args,
expected,
}) => {
expect(selector.preSelectors).toEqual(preSelectors);
expect(selector.cb(args)).toEqual(expected);
};
describe('courseId selector', () => {
it('returns course id from courseMetadata', () => {
testReselect({
selector: selectors.courseId,
preSelectors: [simpleSelectors.courseMetadata],
args: testState.app.courseMetadata,
expected: testState.app.courseMetadata.courseId,
});
});
});
describe('ora metadata selectors', () => {
const { oraMetadata } = testState.app;
const testOraSelector = (selector, expected) => (
testReselect({
selector,
preSelectors: [simpleSelectors.oraMetadata],
args: oraMetadata,
expected,
})
);
test('ora.name selector returns name from oraMetadata', () => {
testOraSelector(selectors.ora.name, oraMetadata.name);
});
test('ora.prompt selector returns prompt from oraMetadata', () => {
testOraSelector(selectors.ora.prompt, oraMetadata.prompt);
});
test('ora.type selector returns type from oraMetadata', () => {
testOraSelector(selectors.ora.type, oraMetadata.type);
});
test('rubricConfig selector returns rubricConfig from oraMetadata', () => {
testOraSelector(selectors.rubricConfig, oraMetadata.rubricConfig);
});
});
describe('rubricConfig selectors', () => {
const { rubricConfig } = testState.app.oraMetadata;
const testRubricSelector = (selector, expected, args = null) => (
testReselect({
selector,
preSelectors: [selectors.rubricConfig],
args: args === null ? rubricConfig : args,
expected,
})
);
test('hasConfig', () => {
testReselect({
selector: rubric.hasConfig,
preSelectors: [selectors.rubricConfig],
args: rubricConfig,
expected: true,
});
testReselect({
selector: rubric.hasConfig,
preSelectors: [selectors.rubricConfig],
args: undefined,
expected: false,
});
});
test('feedbackConfig', () => {
testRubricSelector(rubric.feedbackConfig, rubricConfig.feedback);
});
test('criteria', () => {
testRubricSelector(rubric.criteria, rubricConfig.criteria);
});
describe('criteria selectors', () => {
let criteria;
beforeEach(() => {
criteria = rubric.criteria;
rubric.criteria = jest.fn(({ app }) => app.oraMetadata.rubricConfig.criteria);
});
afterEach(() => {
rubric.criteria = criteria;
});
test('criterionConfig returns config by orderNum/index', () => {
const testCriterion = (orderNum) => {
expect(
rubric.criterionConfig(testState, { orderNum }),
).toEqual(rubricConfig.criteria[orderNum]);
};
[0, 1, 2].map(testCriterion);
});
test('criterionFeedbackConfig', () => {
const testCriterion = (orderNum) => {
expect(
rubric.criterionFeedbackConfig(testState, { orderNum }),
).toEqual(rubricConfig.criteria[orderNum].feedback);
};
[0, 1, 2].map(testCriterion);
});
test('criteriaIndices returns ordered list of orderNum values', () => {
testReselect({
selector: rubric.criteriaIndices,
preSelectors: [criteria],
args: rubricConfig.criteria,
expected: [0, 1, 2],
});
});
});
});
describe('emptyGrade selector', () => {
const { rubricConfig } = testState.app.oraMetadata;
let preSelectors;
let cb;
beforeEach(() => {
({ preSelectors, cb } = selectors.emptyGrade);
});
it('is a memoized selector based on rubric.[hasConfig, criteria, feedbackConfig]', () => {
expect(preSelectors).toEqual([
rubric.hasConfig,
rubric.criteria,
rubric.feedbackConfig,
]);
});
describe('If the config is not loaded (hasConfig = undefined)', () => {
it('returns null', () => {
expect(cb(false, {}, '')).toEqual(null);
});
});
describe('The generated object', () => {
it('loads an overallFeedback field iff feedbackConfig is optional or required', () => {
let gradeData = cb(true, rubricConfig.criteria, feedbackRequirement.optional);
expect(gradeData.overallFeedback).toEqual('');
gradeData = cb(true, rubricConfig.criteria, feedbackRequirement.required);
expect(gradeData.overallFeedback).toEqual('');
gradeData = cb(true, rubricConfig.criteria, feedbackRequirement.disabled);
expect(gradeData.overallFeedback).toEqual(undefined);
});
it('loads criteria with feedback field based on requirement config', () => {
const gradeData = cb(true, rubricConfig.criteria, rubricConfig.feedback);
const { criteria } = rubricConfig;
expect(gradeData.criteria).toEqual([
{
orderNum: criteria[0].orderNum,
name: criteria[0].name,
selectedOption: '',
feedback: '',
},
{
orderNum: criteria[1].orderNum,
name: criteria[1].name,
selectedOption: '',
},
{
orderNum: criteria[2].orderNum,
name: criteria[2].name,
selectedOption: '',
feedback: '',
},
]);
});
});
});
});