chore: renderer test coverage (#103)

* chore: renderer test coverage

* fix: lint

* chore: api tests

* chore: tests for app reducer and StartGradeButton

* chore: lint

* fix: update reducer tests

* chore: more test coverage

* chore: test coverage

* chore: update test for merge conflicts
This commit is contained in:
Ben Warzeski
2022-04-29 14:54:33 -04:00
committed by GitHub
parent 0a90024de9
commit 5f12c4fb8e
81 changed files with 3814 additions and 36249 deletions

View File

@@ -1,5 +1,4 @@
import { getConfig } from '@edx/frontend-platform';
// eslint-disable-next-line import/prefer-default-export
export const routePath = `${getConfig().PUBLIC_PATH}:courseId`;
export const locationId = window.location.pathname.slice(1);

View File

@@ -0,0 +1,24 @@
import * as platform from '@edx/frontend-platform';
import * as constants from './app';
jest.unmock('./app');
jest.mock('@edx/frontend-platform', () => {
const PUBLIC_PATH = 'test-public-path';
return {
getConfig: () => ({ PUBLIC_PATH }),
PUBLIC_PATH,
};
});
describe('app constants', () => {
test('route path draws from public path and adds courseId', () => {
expect(constants.routePath).toEqual(`${platform.PUBLIC_PATH}:courseId`);
});
test('locationId returns trimmed pathname', () => {
const old = window.location;
window.location = { pathName: '/somePath.jpg' };
expect(constants.locationId).toEqual(window.location.pathname.slice(1));
window.location = old;
});
});

View File

@@ -0,0 +1,73 @@
import { initialState, reducer, actions } from './reducer';
describe('app reducer', () => {
describe('initialState', () => {
test('populated, but empty course metadata', () => {
const data = initialState.courseMetadata;
expect(data.name).toEqual('');
expect(data.number).toEqual('');
expect(data.org).toEqual('');
expect(data.courseId).toEqual('');
});
test('disabled (waffle flag)', () => {
expect(initialState.isEnabled).toEqual(false);
});
test('not grading', () => {
expect(initialState.isGrading).toEqual(false);
});
test('populated, but empty ora metadata', () => {
const data = initialState.oraMetadata;
expect(data.prompt).toEqual('');
expect(data.name).toEqual('');
expect(data.type).toEqual('');
expect(data.rubricConfig).toEqual(null);
});
test('not showing review', () => {
expect(initialState.showReview).toEqual(false);
});
test('not showing rubric', () => {
expect(initialState.showRubric).toEqual(false);
});
});
describe('reducers', () => {
it('returns initial state', () => {
expect(reducer(undefined, {})).toEqual(initialState);
});
const testState = {
...initialState,
showRubric: true,
showReview: true,
arbitrary: 'state',
};
const testValue = 'my-test-value';
const testAction = (action, expected) => {
expect(reducer(testState, action)).toEqual({
...testState,
...expected,
});
};
describe('action handlers', () => {
test('loadIsEnabled loads isEnabled from payload', () => {
testAction(actions.loadIsEnabled(testValue), { isEnabled: testValue });
});
test('loadCourseMetadata loads courseMetadata from payload', () => {
testAction(actions.loadCourseMetadata(testValue), { courseMetadata: testValue });
});
test('loadOraMetadata loads oraMetadata from payload', () => {
testAction(actions.loadOraMetadata(testValue), { oraMetadata: testValue });
});
describe('setShowReview', () => {
it('loads showReview, sets showRubric to false if set to false', () => {
testAction(actions.setShowReview(true), { showReview: true });
testAction(actions.setShowReview(false), { showReview: false, showRubric: false });
});
});
test('setShowRubric loads showRubric from payload', () => {
testAction(actions.setShowRubric(testValue), { showRubric: testValue });
});
test('toggleShowRubric toggles showRubric value', () => {
testAction(actions.toggleShowRubric(), { showRubric: !testState.showRubric });
});
});
});
});

View File

@@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit';
import { StrictDict } from 'utils';
import { lockStatuses } from 'data/services/lms/constants';
import * as module from './reducer';
const initialState = {
selection: [
@@ -60,26 +61,6 @@ const initialState = {
next: null, // { response }
};
export const updateGradeData = (state, data) => ({
...state,
gradeData: {
...state.gradeData,
[state.current.submissionUUID]: { ...data },
},
});
/**
* Updates the given state's gradeData entry for the seleted submission.
* @return {object} - new state
*/
export const loadGradeData = (state, data) => ({
...state,
gradeData: {
...state.gradeData,
[state.current.submissionUUID]: { ...data },
},
});
/**
* Updates the state's gradingData entry for the seleted submission,
* overlaying the passed data on top of the existing data for the that
@@ -101,7 +82,7 @@ export const updateGradingData = (state, data) => {
};
/**
* Updates the given state's localGradeData entry for the seleted submission,
* Updates the given state's gradingData 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
@@ -110,7 +91,7 @@ export const updateCriterion = (state, orderNum, data) => {
const entry = state.gradingData[state.current.submissionUUID];
const criteria = [...entry.criteria];
criteria[orderNum] = { ...entry.criteria[orderNum], ...data };
return updateGradingData(state, {
return module.updateGradingData(state, {
...entry,
criteria,
});
@@ -166,16 +147,16 @@ const grading = createSlice({
current: { ...state.current, lockStatus: payload.lockStatus },
}),
setRubricFeedback: (state, { payload }) => (
updateGradingData(state, { overallFeedback: payload })
module.updateGradingData(state, { overallFeedback: payload })
),
setCriterionOption: (state, { payload: { orderNum, value } }) => (
updateCriterion(state, orderNum, { selectedOption: value })
module.updateCriterion(state, orderNum, { selectedOption: value })
),
setCriterionFeedback: (state, { payload: { orderNum, value } }) => (
updateCriterion(state, orderNum, { feedback: value })
module.updateCriterion(state, orderNum, { feedback: value })
),
setShowValidation: (state, { payload }) => (
updateGradingData(state, { showValidation: payload })
module.updateGradingData(state, { showValidation: payload })
),
completeGrading: (state, { payload }) => {
const gradingData = { ...state.gradingData };
@@ -194,39 +175,23 @@ const grading = createSlice({
},
};
},
loadStatus: (state, { payload }) => {
const gradingData = { ...state.gradingData };
delete gradingData[state.current.submissionUUID];
return {
...state,
gradeData: {
...state.gradeData,
[state.current.submissionUUID]: { ...payload.gradeData },
},
gradingData,
current: {
...state.current,
gradeStatus: payload.gradeStatus,
lockStatus: payload.lockStatus,
},
};
},
stopGrading: (state, { payload }) => {
const { submissionUUID } = state.current;
const localGradeData = { ...state.localGradeData };
delete localGradeData[submissionUUID];
const gradeData = { ...state.gradeData };
let lockStatus = lockStatuses.unlocked;
let { gradeStatus } = state.current;
if (payload) {
const { submissionStatus } = payload;
gradeData[submissionUUID] = submissionStatus.gradeData;
lockStatus = submissionStatus.lockStatus;
gradeStatus = submissionStatus.gradeStatus;
}
const gradingData = { ...state.gradingData };
delete gradingData[submissionUUID];
const gradeData = {
...state.gradeData,
...(payload && { [submissionUUID]: payload.submissionStatus.gradeData }),
};
const { gradeStatus } = payload ? payload.submissionStatus : state.current;
const lockStatus = payload ? payload.submissionStatus.lockStatus : lockStatuses.unlocked;
return {
...state,
localGradeData,
gradingData,
gradeData,
current: {
...state.current,
lockStatus,

View File

@@ -0,0 +1,285 @@
import { keyStore } from 'utils';
import { lockStatuses } from 'data/services/lms/constants';
import * as module from './reducer';
const {
initialState,
updateGradingData,
updateCriterion,
reducer,
actions,
} = module;
const moduleKeys = keyStore(module);
describe('app reducer', () => {
describe('initialState', () => {
test('empty selection list', () => {
expect(initialState.selection).toEqual([]);
});
test('empty gradeData object', () => {
expect(initialState.gradeData).toEqual({});
});
test('empty gradingData object', () => {
expect(initialState.gradingData).toEqual({});
});
test('null activeIndex', () => {
expect(initialState.activeIndex).toEqual(null);
});
test('empty current object', () => {
expect(initialState.current).toEqual({});
});
test('null prev pointer', () => {
expect(initialState.prev).toEqual(null);
});
test('null next pointer', () => {
expect(initialState.next).toEqual(null);
});
});
const submissionUUID = 'test-submission-uuid';
const orderNum = 1;
const criterion = { unique: 'criterion-data' };
const criteria = [{ some: 'test-data' }, criterion, { other: 'fake-data' }];
const baseGradingData = { fakeID: { some: 'test-data' } };
const gradingData = { unique: 'submission-grading-data', criteria };
const lockStatus = 'test-lock-status';
const gradeStatus = 'test-grade-status';
const testState = {
current: { submissionUUID },
gradingData: { ...baseGradingData, [submissionUUID]: gradingData },
gradeData: { ...baseGradingData, [submissionUUID]: gradingData },
activeIndex: 12,
};
const testValue = 'my-test-value';
const testData = { unique: 'test-data' };
describe('helpers', () => {
describe('updateGradingData', () => {
it('returns new state with new grading data for current submission added to model', () => {
expect(updateGradingData(testState, testData)).toEqual({
...testState,
gradingData: {
...baseGradingData,
[submissionUUID]: { ...gradingData, ...testData },
},
});
expect(
updateGradingData({ ...testState, gradingData: baseGradingData }, testData),
).toEqual({
...testState,
gradingData: {
...baseGradingData,
[submissionUUID]: testData,
},
});
});
});
describe('updateCriterion', () => {
it('overlays the given data on a given criterion field', () => {
const mocks = {
updateGradingData: (...args) => ({ updateGradingData: args }),
};
jest.spyOn(module, moduleKeys.updateGradingData)
.mockImplementationOnce(mocks.updateGradingData);
expect(updateCriterion(testState, orderNum, testData)).toEqual(
mocks.updateGradingData(testState, {
...gradingData,
criteria: [criteria[0], { ...criterion, ...testData }, criteria[2]],
}),
);
});
});
});
describe('reducers', () => {
it('returns initial state', () => {
expect(reducer(undefined, {})).toEqual(initialState);
});
describe('action handlers', () => {
describe('loadSubmission', () => {
it('loads payload to current and overlays current grade data', () => {
const payload = { submissionUUID, gradeData: testData };
expect(reducer(testState, actions.loadSubmission(payload))).toEqual({
...testState,
current: payload,
gradeData: {
...testState.gradeData,
[submissionUUID]: testData,
},
});
});
});
describe('loadNext', () => {
it('clears current and increments activeIndex', () => {
expect(reducer(testState, actions.loadNext())).toEqual({
...testState,
current: {},
activeIndex: testState.activeIndex + 1,
});
});
});
describe('loadPrev', () => {
it('clears current and decrements activeIndex', () => {
expect(reducer(testState, actions.loadPrev())).toEqual({
...testState,
current: {},
activeIndex: testState.activeIndex - 1,
});
});
});
describe('updateSelection', () => {
it('loads selection from payload and sets activeIndex to 0', () => {
expect(reducer(testState, actions.updateSelection(testData))).toEqual({
...testState,
selection: testData,
activeIndex: 0,
});
});
});
describe('startGrading', () => {
describe('resulting state', () => {
const action = actions.startGrading({ lockStatus, gradeData: testData });
test('loads current lockStatus from payload', () => {
expect(reducer(testState, action).current).toEqual({
...testState.current,
lockStatus,
});
});
test('loads selected gradeData from payload', () => {
expect(reducer(testState, action).gradeData).toEqual({
...testState.gradeData,
[submissionUUID]: testData,
});
});
test('loads gradingData w/ showValidation: false, overlaying on existing data', () => {
expect(reducer(testState, action).gradingData).toEqual({
...testState.gradingData,
[submissionUUID]: {
showValidation: false,
...testData,
...gradingData,
},
});
expect(reducer({ ...testState, gradingData: {} }, action).gradingData).toEqual({
[submissionUUID]: { showValidation: false, ...testData },
});
});
});
});
describe('failSetLock', () => {
it('loads lockStatus from payload', () => {
expect(reducer(testState, actions.failSetLock({ lockStatus: testValue }))).toEqual({
...testState,
current: { ...testState.current, lockStatus: testValue },
});
});
});
describe('gradingData updaters', () => {
const mocks = {
updateGradingData: args => ({ updateGradingData: args }),
};
beforeEach(() => {
jest.spyOn(module, moduleKeys.updateGradingData)
.mockImplementationOnce(mocks.updateGradingData);
});
describe('setRubricFeedback', () => {
it('loads overallFeedback from payload', () => {
expect(reducer(testState, actions.setRubricFeedback(testValue))).toEqual(
mocks.updateGradingData(testState, { overallFeedback: testValue }),
);
});
});
describe('setShowValidation', () => {
it('loads showValidation from payload', () => {
expect(reducer(testState, actions.setShowValidation(testValue))).toEqual(
mocks.updateGradingData(testState, { showValidation: testValue }),
);
});
});
});
describe('criterion updaters', () => {
const mocks = {
updateCriterion: args => ({ updateCriterion: args }),
};
beforeEach(() => {
jest.spyOn(module, moduleKeys.updateCriterion)
.mockImplementationOnce(mocks.updateCriterion);
});
const args = { orderNum, value: testValue };
describe('setCriterionOption', () => {
it('loads selectedOption by orderNum', () => {
expect(reducer(testState, actions.setCriterionOption(testState, args))).toEqual(
mocks.updateCriterion(testState, orderNum, { selectedOption: testValue }),
);
});
});
describe('setCriterionFeedback', () => {
it('loads feedback by orderNum', () => {
expect(reducer(testState, actions.setCriterionFeedback(testState, args))).toEqual(
mocks.updateCriterion(testState, orderNum, { feecback: testValue }),
);
});
});
});
describe('completeGrading', () => {
describe('resulting state', () => {
const payload = { gradeData: testData, lockStatus, gradeStatus };
let output;
beforeAll(() => {
output = reducer(testState, actions.completeGrading(payload));
});
test('gradeData: loads gradeData from payload', () => {
expect(output.gradeData).toEqual({
...testState.gradeData,
[submissionUUID]: testData,
});
});
test('gradingData: deletes current data', () => {
expect(output.gradingData).toEqual(baseGradingData);
});
test('current: loads gradeStatus and lockStatus from payload', () => {
expect(output.current).toEqual({
...testState.current,
lockStatus,
gradeStatus,
});
});
});
});
describe('stopGrading', () => {
let output;
const args = { submissionStatus: { gradeData: testData, lockStatus, gradeStatus } };
describe('resulting state', () => {
test('gradingData: deletes current data', () => {
output = reducer(testState, actions.stopGrading());
expect(output.gradingData).toEqual(baseGradingData);
});
test('gradeData: appends payload.submissionStatus.gradeData if passed', () => {
output = reducer(testState, actions.stopGrading());
expect(output.gradeData).toEqual(testState.gradeData);
output = reducer(testState, actions.stopGrading(args));
expect(output.gradeData).toEqual({
...testState.gradeData,
[submissionUUID]: testData,
});
});
describe('current: loads lockStatus and gradeStatus', () => {
test('defaults to state.current.gradeStatus and unlocked', () => {
output = reducer(testState, actions.stopGrading());
expect(output.current).toEqual({
...testState.current,
lockStatus: lockStatuses.unlocked,
});
});
test('loads from payload is passed', () => {
output = reducer(testState, actions.stopGrading(args));
expect(output.current).toEqual({ ...testState.current, lockStatus, gradeStatus });
});
});
});
});
});
});
});

View File

@@ -0,0 +1,78 @@
import { RequestStates } from 'data/constants/requests';
import selectors from './selectors';
jest.mock('reselect', () => ({
createSelector: jest.fn((preSelectors, cb) => ({ preSelectors, cb })),
}));
const requestKey = 'my-test-request-key';
const requestData = { some: 'request-data' };
const inactiveRequest = { status: RequestStates.inactive, some: 'request-data' };
const pendingRequest = { status: RequestStates.pending, some: 'request-data' };
const completedRequest = { status: RequestStates.completed, some: 'request-data' };
const failedRequest = { status: RequestStates.failed, some: 'request-data' };
const testValue = 'my-test-value';
const testState = {
requests: {
[requestKey]: requestData,
},
};
const genRequests = (request) => ({
requests: { [requestKey]: request },
});
const select = (selector, request) => (
selector(genRequests(request), { requestKey })
);
describe('requests selectors unit tests', () => {
test('requestStatus returns data associated with given key', () => {
expect(selectors.requestStatus(testState, { requestKey })).toEqual(requestData);
});
describe('allowNavigation', () => {
it('returns false if any requests are pending', () => {
expect(selectors.allowNavigation(testState)).toEqual(true);
expect(selectors.allowNavigation({ requests: { key1: pendingRequest } })).toEqual(false);
});
});
const testStatusSelector = (selector, matchingRequest) => {
expect(selector(testState, { requestKey })).toEqual(false);
expect(selector(
{ requests: { [requestKey]: matchingRequest } },
{ requestKey },
)).toEqual(true);
};
test('isInactive returns true iff the given request is inactive', () => {
testStatusSelector(selectors.isInactive, inactiveRequest);
});
test('isPending returns true iff the given request is pending', () => {
testStatusSelector(selectors.isPending, pendingRequest);
});
test('isCompleted returns true iff the given request is completed', () => {
testStatusSelector(selectors.isCompleted, completedRequest);
});
test('isFailed returns true iff the given request is failed', () => {
testStatusSelector(selectors.isFailed, failedRequest);
});
test('error returns the error from the request', () => {
expect(select(selectors.error, { error: testValue })).toEqual(testValue);
});
test('errorStatus returns the error response status', () => {
expect(select(selectors.errorStatus, {})).toEqual(undefined);
expect(select(selectors.errorStatus, { error: {} })).toEqual(undefined);
expect(select(selectors.errorStatus, { error: { response: {} } })).toEqual(undefined);
expect(select(selectors.errorStatus, { error: { response: { status: testValue } } }))
.toEqual(testValue);
});
test('errorCode returns the error response data', () => {
expect(select(selectors.errorCode, {})).toEqual(undefined);
expect(select(selectors.errorCode, { error: {} })).toEqual(undefined);
expect(select(selectors.errorCode, { error: { response: {} } })).toEqual(undefined);
expect(select(selectors.errorCode, { error: { response: { data: testValue } } }))
.toEqual(testValue);
});
test('data reurns the request data', () => {
expect(select(selectors.data, { data: testValue })).toEqual(testValue);
});
});

View File

@@ -1,17 +1,23 @@
import { locationId } from 'data/constants/app';
import { actions } from 'data/redux';
import thunkActions from './app';
import { selectors, actions } from 'data/redux';
import { keyStore } from 'utils';
import * as thunkActions from './app';
jest.mock('./requests', () => ({
initializeApp: (args) => ({ initializeApp: args }),
batchUnlock: (args) => ({ batchUnlock: args }),
}));
const dispatch = jest.fn((action) => ({ dispatch: action }));
const testState = { my: 'test state' };
const getState = () => testState;
const moduleKeys = keyStore(thunkActions);
describe('app thunkActions', () => {
let dispatch;
let dispatchedAction;
beforeEach(() => {
dispatch = jest.fn((action) => ({ dispatch: action }));
jest.clearAllMocks();
});
describe('initialize', () => {
beforeEach(() => {
@@ -24,13 +30,13 @@ describe('app thunkActions', () => {
});
describe('on success', () => {
test('loads isEnabled, oraMetadata, courseMetadata and list data', () => {
dispatch.mockClear();
const response = {
courseMetadata: { some: 'course-metadata' },
isEnabled: { is: 'enabled?' },
oraMetadata: { some: 'ora-metadata' },
submissions: { some: 'submissions' },
};
dispatch.mockClear();
dispatchedAction.initializeApp.onSuccess(response);
expect(dispatch.mock.calls).toEqual([
[actions.app.loadIsEnabled(response.isEnabled)],
@@ -41,4 +47,30 @@ describe('app thunkActions', () => {
});
});
});
describe('cancelReview', () => {
const gradingSelection = (args) => ({ gradingSelection: args });
const mockInitialize = (args) => ({ initialize: args });
const gradingKeys = keyStore(selectors.grading);
beforeEach(() => {
jest.spyOn(thunkActions, moduleKeys.initialize)
.mockImplementationOnce(mockInitialize);
jest.spyOn(selectors.grading, gradingKeys.selection)
.mockImplementationOnce(gradingSelection);
thunkActions.cancelReview()(dispatch, getState);
[[dispatchedAction]] = dispatch.mock.calls;
});
it('dispatches batchUnlock with submissionUUIDs and onSuccess', () => {
expect(dispatchedAction.batchUnlock.submissionUUIDs)
.toEqual(gradingSelection(testState));
expect(typeof dispatchedAction.batchUnlock.onSuccess).toEqual('function');
});
it('clears show review state and calls dispatches initialize thunkAction on success', () => {
dispatch.mockClear();
dispatchedAction.batchUnlock.onSuccess();
expect(dispatch.mock.calls).toEqual([
[actions.app.setShowReview(false)],
[mockInitialize()],
]);
});
});
});

View File

@@ -107,7 +107,8 @@ export const cancelGrading = () => (dispatch, getState) => {
export const submitGrade = () => (dispatch, getState) => {
const gradeData = selectors.grading.selected.gradingData(getState());
const submissionUUID = selectors.grading.selected.submissionUUID(getState());
if (selectors.grading.validation.isValidForSubmit(getState())) {
const isValid = selectors.grading.validation.isValidForSubmit(getState());
if (isValid) {
dispatch(actions.grading.setShowValidation(false));
dispatch(requests.submitGrade({
submissionUUID,

View File

@@ -1,5 +1,5 @@
import { actions, selectors } from 'data/redux';
import { RequestKeys } from 'data/constants/requests';
import { ErrorStatuses, RequestKeys } from 'data/constants/requests';
import * as thunkActions from './grading';
jest.mock('./requests', () => ({
@@ -22,27 +22,31 @@ jest.mock('data/redux/grading/selectors', () => ({
},
selected: {
gradeData: jest.fn((state) => ({ gradeData: state })),
gradingData: jest.fn((state) => ({ gradingData: state })),
isGrading: jest.fn((state) => ({ isGrading: state })),
submissionUUID: (state) => ({ selectedsubmissionUUID: state }),
lockStatus: (state) => ({ lockStatus: state }),
submissionUUID: jest.fn((state) => ({ selectedsubmissionUUID: state })),
lockStatus: jest.fn((state) => ({ lockStatus: state })),
},
validation: {
isValidForSubmit: jest.fn((state) => ({ isValidForSubmit: state })),
},
}));
const testState = { some: 'testy-state' };
const selectedUUID = selectors.grading.selected.submissionUUID(testState);
const response = 'test-response';
const objResponse = { response };
let actionArgs;
const dispatch = jest.fn((action) => ({ dispatch: action }));
const getState = () => testState;
const getDispatched = (calledAction) => {
calledAction(dispatch, getState);
};
describe('grading thunkActions', () => {
const testState = { some: 'testy-state' };
const selectedUUID = selectors.grading.selected.submissionUUID(testState);
const response = 'test-response';
const objResponse = { response };
let dispatch;
let actionArgs;
const getState = () => testState;
const getDispatched = (calledAction) => {
calledAction(dispatch, getState);
};
beforeEach(() => {
dispatch = jest.fn((action) => ({ dispatch: action }));
jest.clearAllMocks();
});
describe('loadSubmission', () => {
@@ -137,10 +141,8 @@ describe('grading thunkActions', () => {
describe('onSuccess', () => {
const gradeData = { some: 'test grade data' };
const startResponse = { other: 'fields', gradeData };
beforeEach(() => {
dispatch.mockClear();
});
const fillString = 'selectors.app.fillGradeData based on selected gradeData';
beforeEach(() => { dispatch.mockClear(); });
test(`dispatches startGrading w/ ${fillString}`, () => {
actionArgs.onSuccess(startResponse);
expect(dispatch.mock.calls).toContainEqual([
@@ -157,6 +159,16 @@ describe('grading thunkActions', () => {
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
});
});
describe('onFailure', () => {
beforeEach(() => { dispatch.mockClear(); });
it('dispatches action to fail setting the lock if error status is Forbidden', () => {
const data = { some: 'data' };
actionArgs.onFailure({ response: { status: 'arbitrary-status', data } });
expect(dispatch).not.toHaveBeenCalled();
actionArgs.onFailure({ response: { status: ErrorStatuses.forbidden, data } });
expect(dispatch).toHaveBeenCalledWith(actions.grading.failSetLock(data));
});
});
});
describe('cancelGrading', () => {
@@ -182,5 +194,69 @@ describe('grading thunkActions', () => {
expect(dispatch.mock.calls).toContainEqual([actions.grading.stopGrading()]);
});
});
describe('onFailure', () => {
beforeEach(() => { dispatch.mockClear(); });
it('dispatches action to fail setting the lock if error status is Forbidden', () => {
const data = { some: 'data' };
actionArgs.onFailure({ response: { status: 'arbitrary-status', data } });
expect(dispatch).not.toHaveBeenCalled();
actionArgs.onFailure({ response: { status: ErrorStatuses.forbidden, data } });
expect(dispatch).toHaveBeenCalledWith(actions.grading.failSetLock(data));
});
});
});
describe('submitGrade', () => {
const mockGradingData = (args) => ({ gradingData: args });
const mockSubmissionUUID = (args) => ({ submissionUUID: args });
beforeEach(() => {
selectors.grading.selected.gradingData.mockImplementationOnce(mockGradingData);
selectors.grading.selected.submissionUUID.mockImplementationOnce(mockSubmissionUUID);
});
describe('if grade data is valid for submission', () => {
beforeEach(() => {
selectors.grading.validation.isValidForSubmit.mockReturnValueOnce(true);
getDispatched(thunkActions.submitGrade());
});
it('hides validation and submits grade', () => {
expect(dispatch.mock.calls[0][0]).toEqual(actions.grading.setShowValidation(false));
expect(dispatch.mock.calls[1][0].submitGrade).not.toEqual(undefined);
});
describe('submitGrade args', () => {
let submitGrade;
beforeEach(() => {
([, [{ submitGrade }]] = dispatch.mock.calls);
});
it('loads submissionUUID and gradeData from selected submission', () => {
expect(submitGrade.submissionUUID).toEqual(mockSubmissionUUID(testState));
expect(submitGrade.gradeData).toEqual(mockGradingData(testState));
});
test('on success, dispatches completeGrading action with response', () => {
dispatch.mockClear();
submitGrade.onSuccess(response);
expect(dispatch.mock.calls).toEqual([[actions.grading.completeGrading(response)]]);
});
test('on failure, dispatches stopGrading action w/ error if status is Conflict', () => {
dispatch.mockClear();
submitGrade.onFailure({ response: { status: 'arbitrary', data: response } });
expect(dispatch).not.toHaveBeenCalled();
submitGrade.onFailure({
response: { status: ErrorStatuses.conflict, data: response },
});
expect(dispatch.mock.calls).toEqual([[actions.grading.stopGrading(response)]]);
});
});
});
describe('if grade data is invalid for submission', () => {
beforeEach(() => {
selectors.grading.validation.isValidForSubmit.mockReturnValueOnce(false);
getDispatched(thunkActions.submitGrade());
});
it('sets showValidation to false', () => {
expect(dispatch.mock.calls).toEqual([[
actions.grading.setShowValidation(true),
]]);
});
});
});
});

View File

@@ -4,6 +4,7 @@ import api from 'data/services/lms/api';
import * as requests from './requests';
jest.mock('data/services/lms/api', () => ({
batchUnlockSubmissions: (submissionUUIDs) => ({ batchUnlockSubmissions: submissionUUIDs }),
initializeApp: (locationId) => ({ initializeApp: locationId }),
fetchSubmissionStatus: (submissionUUID) => ({ fetchSubmissionStatus: submissionUUID }),
fetchSubmission: (submissionUUID) => ({ fetchSubmission: submissionUUID }),
@@ -12,66 +13,81 @@ jest.mock('data/services/lms/api', () => ({
updateGrade: (submissionUUID, gradeData) => ({ updateGrade: { submissionUUID, gradeData } }),
}));
let dispatch;
let onSuccess;
let onFailure;
const dispatch = jest.fn();
const onSuccess = jest.fn();
const onFailure = jest.fn();
describe('requests thunkActions module', () => {
beforeEach(() => {
dispatch = jest.fn();
onSuccess = jest.fn();
onFailure = jest.fn();
});
beforeEach(jest.clearAllMocks);
describe('networkRequest', () => {
const requestKey = 'test-request';
const testData = { some: 'test data' };
let resolveFn;
let rejectFn;
beforeEach(() => {
onSuccess = jest.fn();
onFailure = jest.fn();
requests.networkRequest({
requestKey,
promise: new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
}),
onSuccess,
onFailure,
})(dispatch);
});
test('calls startRequest action with requestKey', async () => {
expect(dispatch.mock.calls).toEqual([[actions.requests.startRequest(requestKey)]]);
});
describe('on success', () => {
beforeEach(async () => {
await resolveFn(testData);
describe('with both handlers', () => {
beforeEach(() => {
requests.networkRequest({
requestKey,
promise: new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
}),
onSuccess,
onFailure,
})(dispatch);
});
it('dispatches completeRequest', async () => {
test('calls startRequest action with requestKey', async () => {
expect(dispatch.mock.calls).toEqual([[actions.requests.startRequest(requestKey)]]);
});
describe('on success', () => {
beforeEach(async () => {
await resolveFn(testData);
});
it('dispatches completeRequest', async () => {
expect(dispatch.mock.calls).toEqual([
[actions.requests.startRequest(requestKey)],
[actions.requests.completeRequest({ requestKey, response: testData })],
]);
});
it('calls onSuccess with response', async () => {
expect(onSuccess).toHaveBeenCalledWith(testData);
expect(onFailure).not.toHaveBeenCalled();
});
});
describe('on failure', () => {
beforeEach(async () => {
await rejectFn(testData);
});
test('dispatches completeRequest', async () => {
expect(dispatch.mock.calls).toEqual([
[actions.requests.startRequest(requestKey)],
[actions.requests.failRequest({ requestKey, error: testData })],
]);
});
test('calls onSuccess with response', async () => {
expect(onFailure).toHaveBeenCalledWith(testData);
expect(onSuccess).not.toHaveBeenCalled();
});
});
});
describe('without onSuccess and onFailure', () => {
test('calls startRequest action with requestKey', async () => {
requests.networkRequest({ requestKey, promise: Promise.resolve(testData) })(dispatch);
expect(dispatch.mock.calls).toEqual([[actions.requests.startRequest(requestKey)]]);
});
it('on success dispatches completeRequest', async () => {
await requests.networkRequest({ requestKey, promise: Promise.resolve(testData) })(dispatch);
expect(dispatch.mock.calls).toEqual([
[actions.requests.startRequest(requestKey)],
[actions.requests.completeRequest({ requestKey, response: testData })],
]);
});
it('calls onSuccess with response', async () => {
expect(onSuccess).toHaveBeenCalledWith(testData);
expect(onFailure).not.toHaveBeenCalled();
});
});
describe('on failure', () => {
beforeEach(async () => {
await rejectFn(testData);
});
test('dispatches completeRequest', async () => {
it('on failure disaptches completeRequest', async () => {
await requests.networkRequest({ requestKey, promise: Promise.reject(testData) })(dispatch);
expect(dispatch.mock.calls).toEqual([
[actions.requests.startRequest(requestKey)],
[actions.requests.failRequest({ requestKey, error: testData })],
]);
});
test('calls onSuccess with response', async () => {
expect(onFailure).toHaveBeenCalledWith(testData);
expect(onSuccess).not.toHaveBeenCalled();
});
});
});
@@ -163,6 +179,19 @@ describe('requests thunkActions module', () => {
},
});
});
describe('batchUnlock', () => {
const submissionUUIDs = [1, 2, 3, 4, 5];
testNetworkRequestAction({
action: requests.batchUnlock,
args: { submissionUUIDs, value: false },
expectedString: 'with batchUnlock promise',
expectedData: {
requestKey: RequestKeys.batchUnlock,
promise: api.batchUnlockSubmissions(submissionUUIDs),
value: false,
},
});
});
describe('submitGrade', () => {
const gradeData = 'test-grade-data';
testNetworkRequestAction({

View File

@@ -0,0 +1,140 @@
import { StrictDict, keyStore } from 'utils';
import { locationId } from 'data/constants/app';
import { paramKeys } from './constants';
import urls from './urls';
import api from './api';
import { stringifyUrl } from './utils';
jest.mock('./utils', () => ({
client: () => ({ delete: (url) => Promise.resolve({ data: { delete: { url } } }) }),
get: (url) => Promise.resolve({ data: { get: { url } } }),
post: (url, data) => Promise.resolve({ data: { post: { url, data } } }),
stringifyUrl: args => ({ stringifyUrl: args }),
}));
jest.mock('data/constants/app', () => ({
locationId: 'test-location-id',
}));
const gradeData = 'test-grade-data';
const submissionUUID = 'test-submission-uuid';
const submissionUUIDs = ['some', 'submission', 'uuid'];
const methodKeys = StrictDict({
get: 'get',
post: 'post',
delete: 'delete',
});
const urlKeys = keyStore(urls);
const testAPI = ({
promise,
method,
expected: {
urlKey,
urlParams,
...otherExpected
},
}) => {
it(`returns ${method}(${urlKey}) with correct args and reoslves with response data`, () => (
promise.then((data) => {
expect(data[method]).toEqual({
url: stringifyUrl(urls[urlKey], urlParams),
...otherExpected,
});
})
));
};
describe('lms service api methods', () => {
describe('initializeApp', () => {
testAPI({
promise: api.initializeApp(),
method: methodKeys.get,
expected: {
urlKey: urlKeys.oraInitializeUrl,
urlParams: { [paramKeys.oraLocation]: locationId },
},
});
});
describe('fetchSubmission', () => {
testAPI({
promise: api.fetchSubmission(submissionUUID),
method: methodKeys.get,
expected: {
urlKey: urlKeys.fetchSubmissionUrl,
urlParams: {
[paramKeys.oraLocation]: locationId,
[paramKeys.submissionUUID]: submissionUUID,
},
},
});
});
describe('fetchSubmissionStatus', () => {
testAPI({
promise: api.fetchSubmissionStatus(submissionUUID),
method: methodKeys.get,
expected: {
urlKey: urlKeys.fetchSubmissionStatusUrl,
urlParams: {
[paramKeys.oraLocation]: locationId,
[paramKeys.submissionUUID]: submissionUUID,
},
},
});
});
describe('lockSubmission', () => {
testAPI({
promise: api.lockSubmission(submissionUUID),
method: methodKeys.post,
expected: {
urlKey: urlKeys.fetchSubmissionLockUrl,
urlParams: {
[paramKeys.oraLocation]: locationId,
[paramKeys.submissionUUID]: submissionUUID,
},
},
});
});
describe('unlockSubmission', () => {
testAPI({
promise: api.unlockSubmission(submissionUUID),
method: methodKeys.delete,
expected: {
urlKey: urlKeys.fetchSubmissionLockUrl,
urlParams: {
[paramKeys.oraLocation]: locationId,
[paramKeys.submissionUUID]: submissionUUID,
},
},
});
});
describe('batchUnlockSubmissions', () => {
testAPI({
promise: api.batchUnlockSubmissions(submissionUUIDs),
method: methodKeys.post,
expected: {
urlKey: urlKeys.batchUnlockSubmissionsUrl,
urlParams: {
[paramKeys.oraLocation]: locationId,
},
data: { submissionUUIDs },
},
});
});
describe('updateGrade', () => {
testAPI({
promise: api.updateGrade(submissionUUID, gradeData),
method: methodKeys.post,
expected: {
urlKey: urlKeys.updateSubmissionGradeUrl,
urlParams: {
[paramKeys.oraLocation]: locationId,
[paramKeys.submissionUUID]: submissionUUID,
},
data: gradeData,
},
});
});
});