app selectors clean up and unit testing
This commit is contained in:
@@ -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 }),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
233
src/data/selectors/app.test.js
Normal file
233
src/data/selectors/app.test.js
Normal 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: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user