fix: remove old redux directories and update integration test

This commit is contained in:
Ben Warzeski
2021-11-05 14:04:36 -04:00
parent 3e6eb4b269
commit 2b585f2218
27 changed files with 14 additions and 1350 deletions

View File

@@ -6,7 +6,7 @@ import { ArrowBack, Launch } from '@edx/paragon/icons';
import { Hyperlink, Icon } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import { selectors } from 'data/redux';
import { locationId } from 'data/constants/app';
import urls from 'data/services/lms/urls';
import messages from './messages';

View File

@@ -7,7 +7,7 @@ import {
import * as constants from 'data/constants/app';
import urls from 'data/services/lms/urls';
import selectors from 'data/selectors';
import { selectors } from 'data/redux';
import {
ListViewBreadcrumb,
@@ -23,15 +23,10 @@ jest.mock('@edx/paragon/icons', () => ({
Launch: 'icons.Launch',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {
courseId: (...args) => ({ courseId: args }),
ora: {
name: (...args) => ({ oraName: args }),
},
},
jest.mock('data/redux/app/selectors', () => ({
courseId: (...args) => ({ courseId: args }),
ora: {
name: (...args) => ({ oraName: args }),
},
}));

View File

@@ -1,19 +0,0 @@
import { StrictDict } from 'utils';
import { createActionFactory } from './utils';
export const dataKey = 'app';
const createAction = createActionFactory(dataKey);
export const loadCourseMetadata = createAction('loadCourseMetadata');
export const loadOraMetadata = createAction('loadOraMetadata');
export const setGrading = createAction('setGrading');
export const setShowReview = createAction('setShowReview');
export const toggleShowRubric = createAction('toggleShowRubric');
export default StrictDict({
loadCourseMetadata,
loadOraMetadata,
setGrading,
setShowReview,
toggleShowRubric,
});

View File

@@ -1,22 +0,0 @@
import actions, { dataKey } from './app';
import { testAction, testActionTypes } from './testUtils';
describe('actions', () => {
describe('action types', () => {
const actionTypes = [
actions.loadCourseMetadata,
actions.loadOraMetadata,
actions.setGrading,
actions.setShowReview,
actions.toggleShowRubric,
].map(action => action.toString());
testActionTypes(actionTypes, dataKey);
});
describe('app actions provided', () => {
test('loadCourseMetadata action', () => testAction(actions.loadCourseMetadata));
test('loadOraMetadata action', () => testAction(actions.loadOraMetadata));
test('setGrading action', () => testAction(actions.setGrading));
test('setShowReview action', () => testAction(actions.setShowReview));
test('toggleShowRubric action', () => testAction(actions.toggleShowRubric));
});
});

View File

@@ -1,95 +0,0 @@
import { StrictDict } from 'utils';
import { createActionFactory } from './utils';
export const dataKey = 'grading';
const createAction = createActionFactory(dataKey);
/**
* Load the first of the selected submission list for review, and initializes
* the review pane to the first index.
* @param {obj} submission data for the review/grading view
* {
* {obj} response - api response data
* {obj} gradeData - api grade data
* {str} status - api grade status
* }
*/
const loadSubmission = createAction('loadSubmission');
/**
* Pre-load just the static info about the "next" submission in the review queue.
* Load submission and the learner's response.
* @param {obj} submission ({ response })
*/
const preloadNext = createAction('preloadNext');
/**
* Pre-load just the static info about the "previous" submission in the review queue.
* Load submission and the learner's response.
* @param {obj} submission ({ response })
*/
const preloadPrev = createAction('preloadPrev');
/**
* Load the "next" submission in the selected queue as the current selection, load its current
* status and grade data, and update prev/next accordingly.
* @param {obj} { status, gradeData }
*/
const loadNext = createAction('loadNext');
/**
* Load the "prev" submission in the selected queue as the current selection, load its current
* status and grade data, and update prev/next accordingly.
* @param {obj} { status, gradeData }
*/
const loadPrev = createAction('loadPrev');
/**
* Load the selected submissions, storing their static data in an ordered array and setting the starting
* index at the beginning of the list.
* @param {obj[]} selection - ordered array of submission static data for all selected submissions
*/
const updateSelection = createAction('updateSelection');
// TODO: implement/design data workflow
const rubric = StrictDict({
/*
* update the local version of the rubric-level comment
* @param {string} comment
*/
updateComment: createAction('rubric/comment'),
/*
* update the local version of points for the given criterion
* @param {number} index
* @param {number} points
*/
updateCriterionPoints: createAction('rubric/criterionPoints'),
/*
* update the local version of comment for the given criterion
* @param {number} index
* @param {string} comments
*/
updateCriterionComment: createAction('rubric/criterionComment'),
});
export const startGrading = createAction('grading/start');
export const setRubricFeedback = createAction('grading/setRubricFeedback');
export const setCriterionFeedback = createAction('grading/setCriterionFeedback');
export const setCriterionOption = createAction('grading/setCriterionOption');
export const clearGrade = createAction('grading/clear');
export default StrictDict({
loadSubmission,
preloadNext,
preloadPrev,
loadNext,
loadPrev,
updateSelection,
rubric,
startGrading,
setRubricFeedback,
setCriterionFeedback,
setCriterionOption,
clearGrade,
});

View File

@@ -1,36 +0,0 @@
import actions, { dataKey } from './grading';
import { testAction, testActionTypes } from './testUtils';
describe('actions', () => {
describe('action types', () => {
const actionTypes = [
actions.loadSubmission,
actions.preloadNext,
actions.loadNext,
actions.loadPrev,
actions.updateSelection,
actions.rubric.updateComment,
actions.rubric.updateCriterionPoints,
actions.rubric.updateCriterionComment,
actions.startGrading,
actions.setRubricFeedback,
actions.setCriterionFeedback,
actions.clearGrade,
].map(action => action.toString());
testActionTypes(actionTypes, dataKey);
});
describe('grading actions provided', () => {
test('loadSubmission action', () => testAction(actions.loadSubmission));
test('preloadNext action', () => testAction(actions.preloadNext));
test('loadNext action', () => testAction(actions.loadNext));
test('loadPrev action', () => testAction(actions.loadPrev));
test('updateSelection action', () => testAction(actions.updateSelection));
test('rubric updateComment action', () => testAction(actions.rubric.updateComment));
test('rubric updateCritrionPoints action', () => testAction(actions.rubric.updateCriterionPoints));
test('rubric updateCriterionComment action', () => testAction(actions.rubric.updateCriterionComment));
test('startGrading action', () => testAction(actions.startGrading));
test('setRubricFeedback action', () => testAction(actions.setRubricFeedback));
test('setCriterionFeedback action', () => testAction(actions.setCriterionFeedback));
test('clearGrade action', () => testAction(actions.clearGrade));
});
});

View File

@@ -1,13 +0,0 @@
import { StrictDict } from 'utils';
import app from './app';
import grading from './grading';
import requests from './requests';
import submissions from './submissions';
export default StrictDict({
app,
grading,
requests,
submissions,
});

View File

@@ -1,15 +0,0 @@
import { StrictDict } from 'utils';
import { createActionFactory } from './utils';
export const dataKey = 'requests';
const createAction = createActionFactory(dataKey);
export const startRequest = createAction('startRequest');
export const completeRequest = createAction('completeRequest');
export const failRequest = createAction('failRequest');
export default StrictDict({
startRequest,
completeRequest,
failRequest,
});

View File

@@ -1,18 +0,0 @@
import actions, { dataKey } from './requests';
import { testAction, testActionTypes } from './testUtils';
describe('actions', () => {
describe('action types', () => {
const actionTypes = [
actions.startRequest,
actions.completeRequest,
actions.failRequest,
].map(action => action.toString());
testActionTypes(actionTypes, dataKey);
});
describe('actions provided', () => {
test('startRequest action', () => testAction(actions.startRequest));
test('completeRequest action', () => testAction(actions.completeRequest));
test('failRequest action', () => testAction(actions.failRequest));
});
});

View File

@@ -1,15 +0,0 @@
import { StrictDict } from 'utils';
import { createActionFactory } from './utils';
export const dataKey = 'submissions';
const createAction = createActionFactory(dataKey);
/**
* Load the basic list-level submission data, keyed by submission id
* @param {obj} submissionListData
*/
const loadList = createAction('loadList');
export default StrictDict({
loadList,
});

View File

@@ -1,14 +0,0 @@
import actions, { dataKey } from './submissions';
import { testAction, testActionTypes } from './testUtils';
describe('actions', () => {
describe('action types', () => {
const actionTypes = [
actions.loadList,
].map(action => action.toString());
testActionTypes(actionTypes, dataKey);
});
describe('submissionsactions provided', () => {
test('loadList action', () => testAction(actions.loadList));
});
});

View File

@@ -1,48 +0,0 @@
/**
* testActionTypes(actionTypes, dataKey)
* Takes a list of actionTypes and a module dataKey, and verifies that
* * all actionTypes are unique
* * all actionTypes begin with the dataKey
* @param {string[]} actionTypes - list of action types
* @param {string} dataKey - module data key
*/
export const testActionTypes = (actionTypes, dataKey) => {
test('all types are unique', () => {
expect(actionTypes.length).toEqual((new Set(actionTypes)).size);
});
test('all types begin with the module dataKey', () => {
actionTypes.forEach(type => {
expect(type.startsWith(dataKey)).toEqual(true);
});
});
};
/**
* testAction(action, args, expectedPayload)
* Multi-purpose action creator test function.
* If args/expectedPayload are passed, verifies that it produces the expected output when called
* with the given args.
* If none are passed, (for action creators with basic definition) it tests against a default
* test payload.
* @param {object} action - action creator object/method
* @param {[object]} args - optional payload argument
* @param {[object]} expectedPayload - optional expected payload.
*/
export const testAction = (action, args, expectedPayload) => {
const type = action.toString();
if (args) {
if (Array.isArray(args)) {
expect(action(...args)).toEqual({ type, payload: expectedPayload });
} else {
expect(action(args)).toEqual({ type, payload: expectedPayload });
}
} else {
const payload = { test: 'PAYload' };
expect(action(payload)).toEqual({ type, payload });
}
};
export default {
testAction,
testActionTypes,
};

View File

@@ -1,10 +0,0 @@
import { createAction } from '@reduxjs/toolkit';
const createActionFactory = (dataKey) => (actionKey, ...args) => (
createAction(`${dataKey}/${actionKey}`, ...args)
);
export {
// eslint-disable-next-line import/prefer-default-export
createActionFactory,
};

View File

@@ -1,19 +0,0 @@
import { createAction } from '@reduxjs/toolkit';
import * as utils from './utils';
jest.mock('@reduxjs/toolkit', () => ({
createAction: (key, ...args) => ({ action: key, args }),
}));
describe('redux action utils', () => {
describe('createActionFactory', () => {
it('returns an action creator with the data key', () => {
const dataKey = 'part-of-the-model';
const actionKey = 'an-action';
const args = ['some', 'args'];
expect(utils.createActionFactory(dataKey)(actionKey, ...args)).toEqual(
createAction(`${dataKey}/${actionKey}`, ...args),
);
});
});
});

View File

@@ -1,43 +0,0 @@
import { createReducer } from '@reduxjs/toolkit';
import actions from 'data/actions';
const initialState = {
oraMetadata: {
prompt: '',
name: '',
type: '',
rubricConfig: null,
fileUploadResponseConfig: null,
},
courseMetadata: {
name: '',
number: '',
org: '',
courseId: '',
},
showReview: false,
showRubric: false,
isGrading: false,
};
// eslint-disable-next-line no-unused-vars
const app = createReducer(initialState, {
[actions.app.loadCourseMetadata]: (state, { payload }) => ({ ...state, courseMetadata: payload }),
[actions.app.loadOraMetadata]: (state, { payload }) => ({ ...state, oraMetadata: payload }),
[actions.app.setShowReview]: (state, { payload }) => ({
...state,
showReview: payload,
isReview: state.isGrading && payload, // stop grading when closing review window
showRubric: state.showRubric && payload, // Hide rubric when closing review window
}),
[actions.app.setGrading]: (state, { payload }) => ({
...state,
isGrading: payload,
showRubric: payload || state.showRubric, // open rubric when starting grading
}),
[actions.app.toggleShowRubric]: (state) => ({ ...state, showRubric: !state.showRubric }),
});
export { initialState };
export default app;

View File

@@ -1,159 +0,0 @@
import { createReducer } from '@reduxjs/toolkit';
import { lockStatuses } from 'data/services/lms/constants';
import actions from 'data/actions';
const initialState = {
selected: [
/**
* {
* submissionId: '',
* username: ''
* teamName: ''
* dateSubmitted: 0,
* gradeStatus: '',
* }
*/
],
gradeData: {
/**
* <submissionId>: {
* overallFeedback: '',
* criteria: [{
* orderNum: 0,
* points: 0,
* comments: '',
* }],
* }
*/
},
activeIndex: null,
current: {
/**
* gradeData: {
* score: {
* pointsEarned: 0,
* pointsPossible: 0,
* }
* overallFeedback: '',
* criteria: [{
* name: '',
* feedback: '',
* selectedOption: '',
* }],
* }
* gradeStatus: '',
* response: {
* text: '',
* files: [{
* download_url: '',
* description: '',
* name: '',
* }],
* },
*/
},
prev: null, // { response }
next: null, // { response }
};
/**
* Updates the given state's gradeData entry for the seleted submission,
* overlaying the passed data on top of the existing data for the that
* submission.
* @return {object} - new state
*/
export const updateGradeData = (state, data) => ({
...state,
gradeData: {
...state.gradeData,
[state.current.submissionId]: {
...state.gradeData[state.current.submissionId],
...data,
},
},
});
/**
* Updates the given state's gradeData entry for the seleted submission,
* overlaying the passed data on top of the existing data for the criterion
* at the given index (orderNum) for the rubric.
* @return {object} - new state
*/
export const updateCriterion = (state, orderNum, data) => {
const entry = state.gradeData[state.current.submissionId];
const criteria = {
...entry.criteria,
[orderNum]: { ...entry.criteria[orderNum], ...data },
};
return updateGradeData(state, { ...entry, criteria });
};
// eslint-disable-next-line no-unused-vars
const app = createReducer(initialState, {
[actions.grading.loadSubmission]: (state, { payload }) => ({
...state,
current: { ...payload },
activeIndex: 0,
}),
[actions.grading.preloadNext]: (state, { payload }) => ({ ...state, next: payload }),
[actions.grading.preloadPrev]: (state, { payload }) => ({ ...state, prev: payload }),
[actions.grading.loadNext]: (state, { payload }) => ({
...state,
prev: { response: state.current.response },
current: { response: state.next.response, ...payload },
activeIndex: state.activeIndex + 1,
gradeData: {
...state.gradeData,
[payload.submissionId]: payload.gradeData,
},
next: null,
}),
[actions.grading.loadPrev]: (state, { payload }) => ({
...state,
next: { response: state.current.response },
current: { response: state.prev.response, ...payload },
gradeData: {
...state.gradeData,
[payload.submissionId]: payload.gradeData,
},
activeIndex: state.activeIndex - 1,
prev: null,
}),
[actions.grading.updateSelection]: (state, { payload }) => ({
...state,
selected: payload,
activeIndex: 0,
}),
[actions.grading.startGrading]: (state, { payload }) => updateGradeData(
{
...state,
current: { ...state.current, lockStatus: lockStatuses.inProgress },
},
{ ...payload },
),
[actions.grading.setRubricFeedback]: (state, { payload }) => (
updateGradeData(state, { overallFeedback: payload })
),
[actions.grading.setCriterionOption]: (state, { payload: { orderNum, value } }) => (
updateCriterion(state, orderNum, { selectedOption: value })
),
[actions.grading.setCriterionFeedback]: (state, { payload: { orderNum, value } }) => (
updateCriterion(state, orderNum, { feedback: value })
),
[actions.grading.clearGrade]: (state) => {
const gradeData = { ...state.gradeData };
delete gradeData[state.current.submissionId];
return {
...state,
gradeData,
current: {
...state.current,
lockStatus: lockStatuses.unlocked,
},
};
},
});
export { initialState };
export default app;

View File

@@ -1,16 +0,0 @@
import { combineReducers } from 'redux';
import app from './app';
import grading from './grading';
import requests from './requests';
import submissions from './submissions';
/* istanbul ignore next */
const rootReducer = combineReducers({
app: app.reducer,
grading: grading.reducer,
requests: requests.reducer,
submissions: submissions.reducer,
});
export default rootReducer;

View File

@@ -1,41 +0,0 @@
import { createReducer } from '@reduxjs/toolkit';
import { RequestStates, RequestKeys } from 'data/constants/requests';
import actions from 'data/actions';
const initialState = {
[RequestKeys.initialize]: { status: RequestStates.inactive },
[RequestKeys.fetchSubmission]: { status: RequestStates.inactive },
[RequestKeys.fetchSubmissionStatus]: { status: RequestStates.inactive },
[RequestKeys.setLock]: { status: RequestStates.inactive },
[RequestKeys.prefetchNext]: { status: RequestStates.inactive },
[RequestKeys.prefetchPrev]: { status: RequestStates.inactive },
[RequestKeys.submitGrade]: { status: RequestStates.inactive },
};
// eslint-disable-next-line no-unused-vars
const app = createReducer(initialState, {
[actions.requests.startRequest]: (state, { payload }) => ({
...state,
[payload]: {
status: RequestStates.pending,
},
}),
[actions.requests.completeRequest]: (state, { payload }) => ({
...state,
[payload.requestKey]: {
status: RequestStates.completed,
response: payload.response,
},
}),
[actions.requests.failRequest]: (state, { payload }) => ({
...state,
[payload.requestKey]: {
status: RequestStates.failed,
error: payload.error,
},
}),
});
export { initialState };
export default app;

View File

@@ -1,52 +0,0 @@
import actions from 'data/actions';
import { RequestStates } from 'data/constants/requests';
import requests, { initialState } from './requests';
const testingState = {
...initialState,
arbitraryField: 'arbitrary',
};
describe('requests reducer', () => {
it('has initial state', () => {
expect(requests(undefined, {})).toEqual(initialState);
});
const testValue = 'roll for initiative';
const testKey = 'test-key';
describe('handling actions', () => {
describe('requests.startRequest', () => {
it('adds a pending status for the given key', () => {
expect(requests(
testingState,
actions.requests.startRequest(testKey),
)).toEqual({
...testingState,
[testKey]: { status: RequestStates.pending },
});
});
});
describe('requests.completeRequest', () => {
it('adds a completed status with passed response', () => {
expect(requests(
testingState,
actions.requests.completeRequest({ requestKey: testKey, response: testValue }),
)).toEqual({
...testingState,
[testKey]: { status: RequestStates.completed, response: testValue },
});
});
});
describe('requests.failRequest', () => {
it('adds a failed status with passed error', () => {
expect(requests(
testingState,
actions.requests.failRequest({ requestKey: testKey, error: testValue }),
)).toEqual({
...testingState,
[testKey]: { status: RequestStates.failed, error: testValue },
});
});
});
});
});

View File

@@ -1,32 +0,0 @@
import actions from 'data/actions';
const initialState = {
allSubmissions: {
/**
* <submissionId>: {
* submissionId: '',
* username: ''
* teamName: ''
* dateSubmitted: 0,
* gradeStatus: ''
* grade: {
* pointsEarned: 0,
* pointsPossible: 0,
* }
* }
*/
},
};
// eslint-disable-next-line no-unused-vars
const grades = (state = initialState, { type, payload }) => {
switch (type) {
case actions.submissions.loadList.toString():
return { ...state, allSubmissions: payload };
default:
return state;
}
};
export { initialState };
export default grades;

View File

@@ -21,7 +21,6 @@ const moduleProps = (propName) => Object.keys(modules).reduce(
{},
);
/* istanbul ignore next */
const rootReducer = combineReducers(moduleProps('reducer'));
const actions = StrictDict(moduleProps('actions'));

View File

@@ -1,161 +0,0 @@
import { createSelector } from 'reselect';
import { feedbackRequirement } from 'data/services/lms/constants';
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: 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),
/**
* Return file load response config
* @returns {string} - file load response config
*/
fileUploadResponseConfig: oraMetadataSelector(data => data.fileUploadResponseConfig),
};
/**
* 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
*/
rubric.feedbackConfig = rubricConfigSelector(config => config.feedback);
/**
* Return the criteria feedbase prompt
* @return {string} - criteria feedback prompt
*/
rubric.feedbackPrompt = rubricConfigSelector(config => config.feedbackPrompt);
/**
* Returns a list of rubric criterion config objects for the ORA
* @return {obj[]} - array of criterion config objects
*/
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
*/
rubric.criterionConfig = (state, { orderNum }) => module.rubric.criteria(state)[orderNum];
/**
* Returns the feeback configuration string for tor the criterion at the given index
* (orderNum).
* @param {number} orderNum - rubric criterion index
* @return {string} - criterion feedback config string
*/
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
*/
rubric.criteriaIndices = createSelector(
[module.rubric.criteria],
(rubricCriteria) => rubricCriteria.map(({ orderNum }) => orderNum),
);
/**
* Returns true iff the passed feedback value is required or optional
* @return {bool} - should include feedback?
*/
const shouldIncludeFeedback = (feedback) => ([
feedbackRequirement.required,
feedbackRequirement.optional,
]).includes(feedback);
/**
* Returns an empty grade data object based on the rubric config loaded in the app model.
* @return {obj} - empty grade data object
*/
export const emptyGrade = createSelector(
[module.rubric.hasConfig, module.rubric.criteria, module.rubric.feedbackConfig],
(hasConfig, criteria, feedbackConfig) => {
if (!hasConfig) {
return null;
}
const gradeData = {};
if (shouldIncludeFeedback(feedbackConfig)) {
gradeData.overallFeedback = '';
}
gradeData.criteria = criteria.map(criterion => {
const entry = {
orderNum: criterion.orderNum,
name: criterion.name,
selectedOption: '',
};
if (shouldIncludeFeedback(criterion.feedback)) {
entry.feedback = '';
}
return entry;
});
return gradeData;
},
);
export default StrictDict({
...simpleSelectors,
courseId,
ora,
rubric: StrictDict(rubric),
emptyGrade,
});

View File

@@ -1,232 +0,0 @@
import { feedbackRequirement } from 'data/services/lms/constants';
// import * in order to mock in-file references
import * as selectors 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('ora.fileUploadResponseConfig selector returns file upload config from oraMetadata', () => {
testOraSelector(selectors.ora.fileUploadResponseConfig, oraMetadata.fileUploadResponseConfig);
});
test('rubricConfig selector returns rubricConfig from oraMetadata', () => {
testOraSelector(selectors.rubric.config, oraMetadata.rubricConfig);
});
});
describe('rubricConfig selectors', () => {
const { rubricConfig } = testState.app.oraMetadata;
const testRubricSelector = (selector, expected, args = null) => (
testReselect({
selector,
preSelectors: [selectors.rubric.config],
args: args === null ? rubricConfig : args,
expected,
})
);
test('hasConfig', () => {
testReselect({
selector: rubric.hasConfig,
preSelectors: [selectors.rubric.config],
args: rubricConfig,
expected: true,
});
testReselect({
selector: rubric.hasConfig,
preSelectors: [selectors.rubric.config],
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: '',
},
]);
});
});
});
});

View File

@@ -1,230 +0,0 @@
import { createSelector } from 'reselect';
import { StrictDict } from 'utils';
import { lockStatuses } from 'data/services/lms/constants';
import submissionsSelectors from './submissions';
import * as module from './grading';
export const simpleSelectors = {
selected: state => state.grading.selected,
activeIndex: state => state.grading.activeIndex,
current: state => state.grading.current,
gradeData: state => state.grading.gradeData,
};
/**
* returns the length of the list of selected submissions
* @return {number} selected submission list length
*/
export const selectionLength = createSelector(
[module.simpleSelectors.selected],
(selected) => selected.length,
);
/**************************************************
* Selected Submission Selectors
**************************************************/
export const selected = {};
/**
* returns the selected submission id
* Note: Not loaded from current as this is what sets that value in the
* (current) bucket
* @return {string} selected submission id
*/
selected.submissionId = createSelector(
[
module.simpleSelectors.selected,
submissionsSelectors.allSubmissions,
module.simpleSelectors.activeIndex,
],
(selectedIds, submissions, activeIndex) => submissions[selectedIds[activeIndex]].submissionId,
);
/**
* Returns the grade status for the selected submission
* @return {string} grade status
*/
selected.gradeStatus = createSelector(
[module.simpleSelectors.current],
(current) => current.gradeStatus,
);
/**
* Returns the lock status for the selected submission
* @return {string} lock status
*/
selected.lockStatus = createSelector(
[module.simpleSelectors.current],
(current) => current.lockStatus,
);
/**
* Returns the response data for the selected submission
* @return {obj} response
* { text, files: [] }
*/
selected.response = createSelector(
[module.simpleSelectors.current],
(current) => current.response,
);
/**
* Returns the "grading" status for the selected submission,
* which is a combination of the grade and lock statuses.
* @return {string} grading status
*/
selected.gradingStatus = createSelector(
[module.selected.gradeStatus, module.selected.lockStatus],
(gradeStatus, lockStatus) => (lockStatus === lockStatuses.unlocked ? gradeStatus : lockStatus),
);
/***********************************
* Selected Submission - Static Data
***********************************/
/**
* returns static data from the active selected submission
* @return {obj} - staticData
* { submissionId, username, teamName, dateSubmitted }
*/
selected.staticData = createSelector(
[module.selected.submissionId, submissionsSelectors.allSubmissions],
(submissionId, allSubmissions) => {
const submission = allSubmissions[submissionId];
const { grade, gradeStatus, ...staticData } = submission;
return staticData;
},
);
/**
* Returns the username for the selected submission
* @return {string} username
*/
selected.username = createSelector(
[module.selected.staticData],
(staticData) => staticData.username,
);
/***********************************
* Selected Submission - Grade Data
***********************************/
/**
* Returns the grade data for the selected submission
* @return {obj} grade data
* { score, overallFeedback, criteria }
*/
selected.gradeData = createSelector(
[module.selected.submissionId, module.simpleSelectors.gradeData],
(submissionId, gradeData) => gradeData[submissionId],
);
/**
* Returns list of criterion grade data for the current selection
* @return {obj[]} criterion grade data entries
*/
selected.criteriaGradeData = createSelector(
[module.selected.gradeData],
(data) => (data ? data.criteria : []),
);
/**
* Returns the score object associated with the grade
* @return {obj} score object
*/
selected.score = createSelector(
[module.selected.gradeData],
(data) => ((data && data.score) ? data.score : {}),
);
/**
* Returns the rubric-level feedback for the selected submission
* @return {string} selected submission's associated rubric-level feedback
*/
selected.overallFeedback = createSelector(
[module.selected.gradeData],
(data) => (data ? data.overallFeedback : ''),
);
/**
* Returns the grade data for the given criterion of the current
* selection
* @param {number} orderNum - criterion orderNum (and index)
* @return {obj} - Grade Data associated with the criterion
*/
selected.criterionGradeData = (state, { orderNum }) => {
const data = module.selected.criteriaGradeData(state);
return data ? data[orderNum] : {};
};
/**
* Returns the critierion-level feedback for the selected submission, given the
* orderNum of the criterion.
* @param {number} orderNum - criterion index
* @return {string} - criterion-level feedback response for the given criterion.
*/
selected.criterionFeedback = (state, { orderNum }) => {
const data = module.selected.criterionGradeData(state, { orderNum });
return data ? data.feedback : '';
};
/*************************************************
* Next/Previous Submission Selectors
*************************************************/
const next = {
/**
* Returns true iff there exists a selection after the current selection
* in the queue.
* @return {bool} has next submission?
*/
doesExist: createSelector(
[simpleSelectors.selected, simpleSelectors.activeIndex],
(list, activeIndex) => activeIndex < list.length - 1,
),
/**
* Returns the submissionId for the next submission in the selection queu
* @return {string} next submission id (null if there isn't one)
*/
submissionId: createSelector(
[simpleSelectors.selected, simpleSelectors.activeIndex],
(list, activeIndex) => {
if (activeIndex < list.length - 1) {
return list[activeIndex + 1];
}
return null;
},
),
};
const prev = {
/*
* Returns true iff there exists a selection previous to the current selection
* in the queue.
* @return {bool} has previous submission?
*/
doesExist: createSelector(
[simpleSelectors.activeIndex],
(activeIndex) => activeIndex > 0,
),
/**
* Returns the submissionId for the previous submission in the selection queue
* @return {string} previous submission id (null if there isn't one)
*/
submissionId: createSelector(
[simpleSelectors.selected, simpleSelectors.activeIndex],
(list, activeIndex) => {
if (activeIndex > 0) {
return list[activeIndex - 1];
}
return null;
},
),
};
export default StrictDict({
...simpleSelectors,
next: StrictDict(next),
prev: StrictDict(prev),
selected: StrictDict(selected),
selectionLength,
});

View File

@@ -1,11 +0,0 @@
import { StrictDict } from 'utils';
import app from './app';
import grading from './grading';
import submissions from './submissions';
export default StrictDict({
app,
grading,
submissions,
});

View File

@@ -1,33 +0,0 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
import { StrictDict } from 'utils';
import { lockStatuses } from 'data/services/lms/constants';
export const simpleSelectors = {
allSubmissions: state => state.submissions.allSubmissions,
};
/**
* Returns the submission list in default order for the table.
*/
export const listData = createSelector(
[simpleSelectors.allSubmissions],
(allSubmissions) => {
const submissionIds = Object.keys(allSubmissions);
const submissionList = submissionIds.map(id => {
const { gradeStatus, lockStatus, ...rest } = allSubmissions[id];
const gradingStatus = (lockStatus === lockStatuses.unlocked ? gradeStatus : lockStatus);
return { gradingStatus, ...rest };
});
return _.sortBy(
submissionList,
['submissionDate'],
);
},
);
export default StrictDict({
...simpleSelectors,
listData,
});

View File

@@ -179,11 +179,15 @@ describe('ESG app integration tests', () => {
test('initialState', async () => {
await renderEl();
expect(state.app).toEqual(jest.requireActual('data/reducers/app').initialState);
expect(state.submissions).toEqual(
jest.requireActual('data/reducers/submissions').initialState,
expect(state.app).toEqual(
jest.requireActual('data/redux/app/reducer').initialState,
);
expect(state.submissions).toEqual(
jest.requireActual('data/redux/submissions/reducer').initialState,
);
expect(state.grading).toEqual(
jest.requireActual('data/redux/grading/reducer').initialState,
);
expect(state.grading).toEqual(jest.requireActual('data/reducers/grading').initialState);
});
test('initialization', async () => {