thunkActions tests
This commit is contained in:
@@ -122,6 +122,7 @@ const formatGradeOverrideForDisplay = historyArray => historyArray.map(item => (
|
||||
const simpleSelectors = simpleSelectorFactory(
|
||||
({ grades }) => grades,
|
||||
[
|
||||
'courseId',
|
||||
'filteredUsersCount',
|
||||
'totalUsersCount',
|
||||
'gradeFormat',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { StrictDict } from 'utils';
|
||||
import actions from '../actions';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
@@ -6,7 +7,7 @@ import LmsApiService from '../services/LmsApiService';
|
||||
const { fetching, gotGradesFrozen } = actions.assignmentTypes;
|
||||
const { gotBulkManagementConfig } = actions.config;
|
||||
|
||||
const fetchAssignmentTypes = courseId => (
|
||||
export const fetchAssignmentTypes = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(fetching.started());
|
||||
return LmsApiService.fetchAssignmentTypes(courseId)
|
||||
@@ -22,4 +23,4 @@ const fetchAssignmentTypes = courseId => (
|
||||
}
|
||||
);
|
||||
|
||||
export { fetchAssignmentTypes };
|
||||
export default StrictDict({ fetchAssignmentTypes });
|
||||
|
||||
@@ -1,99 +1,55 @@
|
||||
import axios from 'axios';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
import actions from '../actions';
|
||||
import * as thunkActions from './assignmentTypes';
|
||||
import { createTestFetcher } from './testUtils';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
jest.mock('../services/LmsApiService', () => ({
|
||||
fetchAssignmentTypes: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
getAuthenticatedHttpClient.mockReturnValue(axios);
|
||||
axios.isAccessTokenExpired = jest.fn();
|
||||
axios.isAccessTokenExpired.mockReturnValue(false);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
const responseData = {
|
||||
assignment_types: {
|
||||
some: 'types',
|
||||
other: 'TYpeS',
|
||||
},
|
||||
grades_frozen: 'bOOl',
|
||||
can_see_bulk_management: 'BooL',
|
||||
};
|
||||
|
||||
describe('assignmentType thunkActions', () => {
|
||||
describe('fetchAssignmentTypes', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const responseData = {
|
||||
assignment_types: {
|
||||
Exam: {
|
||||
drop_count: 0,
|
||||
min_count: 1,
|
||||
short_label: 'Exam',
|
||||
type: 'Exam',
|
||||
weight: 0.25,
|
||||
},
|
||||
Homework: {
|
||||
drop_count: 1,
|
||||
min_count: 3,
|
||||
short_label: 'Ex',
|
||||
type: 'Homework',
|
||||
weight: 0.75,
|
||||
},
|
||||
},
|
||||
grades_frozen: false,
|
||||
can_see_bulk_management: true,
|
||||
};
|
||||
it('dispatches success action after fetching fetchAssignmentTypes', () => {
|
||||
const expectedActions = [
|
||||
actions.assignmentTypes.fetching.started(),
|
||||
actions.assignmentTypes.fetching.received(
|
||||
Object.keys(responseData.assignment_types),
|
||||
),
|
||||
actions.assignmentTypes.gotGradesFrozen(responseData.grades_frozen),
|
||||
actions.assignmentTypes.config.gotBulkManagementConfig(true),
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.fetchAssignmentTypes,
|
||||
thunkActions.fetchAssignmentTypes,
|
||||
[courseId],
|
||||
);
|
||||
describe('actions dispatched on valid response', () => {
|
||||
const actionNames = [
|
||||
'fetching.started',
|
||||
'fetching.received with data.assignment_types',
|
||||
'gotGradesFrozen with data.grades_frozen',
|
||||
'config.gotBulkManagement with data.can_see_bulk_management',
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchAssignmentTypes(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
test(actionNames.join(', '), () => testFetch(
|
||||
(resolve) => resolve({ data: responseData }),
|
||||
[
|
||||
actions.assignmentTypes.fetching.started(),
|
||||
actions.assignmentTypes.fetching.received(Object.keys(responseData.assignment_types)),
|
||||
actions.assignmentTypes.gotGradesFrozen(responseData.grades_frozen),
|
||||
actions.config.gotBulkManagementConfig(responseData.can_see_bulk_management),
|
||||
],
|
||||
));
|
||||
});
|
||||
|
||||
it('dispatches failure action after fetching cohorts', () => {
|
||||
const expectedActions = [
|
||||
actions.assignmentTypes.fetching.started(),
|
||||
actions.assignmentTypes.fetching.error(),
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchAssignmentTypes(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches frozen grade action with True value after fetching', () => {
|
||||
const expectedActions = [
|
||||
actions.assignmentTypes.fetching.started(),
|
||||
actions.assignmentTypes.fetching.received(
|
||||
Object.keys(responseData.assignment_types),
|
||||
),
|
||||
actions.assignmentTypes.gotGradesFrozen(true),
|
||||
actions.assignmentTypes.config.gotBulkManagementConfig(true),
|
||||
];
|
||||
const store = mockStore();
|
||||
responseData.grades_frozen = true;
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/api/grades/v1/gradebook/${courseId}/grading-info?graded_only=true`)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchAssignmentTypes(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
describe('actions dispatched on api error', () => {
|
||||
test('fetching.started, fetching.error', () => testFetch(
|
||||
(resolve, reject) => reject(),
|
||||
[
|
||||
actions.assignmentTypes.fetching.started(),
|
||||
actions.assignmentTypes.fetching.error(),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import cohorts from '../actions/cohorts';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const fetchCohorts = courseId => (
|
||||
export const fetchCohorts = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(cohorts.fetching.started());
|
||||
return LmsApiService.fetchCohorts(courseId)
|
||||
@@ -18,4 +18,4 @@ const fetchCohorts = courseId => (
|
||||
}
|
||||
);
|
||||
|
||||
export { fetchCohorts };
|
||||
export default StrictDict({ fetchCohorts });
|
||||
|
||||
@@ -1,76 +1,44 @@
|
||||
import axios from 'axios';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { configuration } from '../../config';
|
||||
import { fetchCohorts } from './cohorts';
|
||||
import actions from '../actions';
|
||||
import * as thunkActions from './cohorts';
|
||||
import { createTestFetcher } from './testUtils';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
jest.mock('../services/LmsApiService', () => ({
|
||||
fetchCohorts: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
getAuthenticatedHttpClient.mockReturnValue(axios);
|
||||
axios.isAccessTokenExpired = jest.fn();
|
||||
axios.isAccessTokenExpired.mockReturnValue(false);
|
||||
|
||||
describe('cohort thunkActions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
const responseData = {
|
||||
cohorts: {
|
||||
some: 'COHorts',
|
||||
other: 'cohORT$',
|
||||
},
|
||||
};
|
||||
|
||||
describe('cohorts thunkActions', () => {
|
||||
describe('fetchCohorts', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
|
||||
it('dispatches success action after fetching cohorts', () => {
|
||||
const responseData = {
|
||||
cohorts: [
|
||||
{
|
||||
assignment_type: 'manual',
|
||||
group_id: null,
|
||||
id: 1,
|
||||
name: 'default_group',
|
||||
user_count: 2,
|
||||
user_partition_id: null,
|
||||
},
|
||||
{
|
||||
assignment_type: 'auto',
|
||||
group_id: null,
|
||||
id: 2,
|
||||
name: 'auto_group',
|
||||
user_count: 5,
|
||||
user_partition_id: null,
|
||||
}],
|
||||
};
|
||||
const expectedActions = [
|
||||
actions.cohorts.fetching.started(),
|
||||
actions.cohorts.fetching.received(responseData.cohorts),
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/courses/${courseId}/cohorts/`)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchCohorts(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.fetchCohorts,
|
||||
thunkActions.fetchCohorts,
|
||||
[courseId],
|
||||
);
|
||||
describe('actions dispatched on valid response', () => {
|
||||
test('fetching.started, fetching.received', () => {
|
||||
return testFetch((resolve) => resolve({ data: responseData }), [
|
||||
actions.cohorts.fetching.started(),
|
||||
actions.cohorts.fetching.received(responseData.cohorts),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches failure action after fetching cohorts', () => {
|
||||
const expectedActions = [
|
||||
actions.cohorts.fetching.started(),
|
||||
actions.cohorts.fetching.error(),
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(`${configuration.LMS_BASE_URL}/courses/${courseId}/cohorts/`)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchCohorts(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
describe('actions dispatched on api error', () => {
|
||||
test('fetching.started, fetching.error', () => testFetch(
|
||||
(resolve, reject) => reject(),
|
||||
[
|
||||
actions.cohorts.fetching.started(),
|
||||
actions.cohorts.fetching.error(),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import filters from '../actions/filters';
|
||||
import selectors from '../selectors';
|
||||
|
||||
import { fetchGrades } from './grades';
|
||||
|
||||
const updateIncludeCourseRoleMembers = (includeCourseRoleMembers) => (dispatch, getState) => {
|
||||
export const updateIncludeCourseRoleMembers = (includeCourseRoleMembers) => (dispatch, getState) => {
|
||||
dispatch(filters.update.includeCourseRoleMembers(includeCourseRoleMembers));
|
||||
const state = getState();
|
||||
const { cohort, track, assignmentType } = selectors.filters.allFilters(state);
|
||||
dispatch(fetchGrades(state.grades.courseId, cohort, track, assignmentType));
|
||||
const courseId = selectors.grades.courseId(state);
|
||||
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
|
||||
};
|
||||
|
||||
export {
|
||||
export default StrictDict({
|
||||
updateIncludeCourseRoleMembers,
|
||||
|
||||
};
|
||||
});
|
||||
|
||||
47
src/data/thunkActions/filters.test.js
Normal file
47
src/data/thunkActions/filters.test.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import selectors from '../selectors';
|
||||
import actions from '../actions';
|
||||
import { fetchGrades } from './grades';
|
||||
import { updateIncludeCourseRoleMembers } from './filters';
|
||||
|
||||
jest.mock('./grades', () => ({
|
||||
fetchGrades: jest.fn((...args) => ({ type: 'fetchGrades', args })),
|
||||
}));
|
||||
|
||||
jest.mock('../selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: { courseId: jest.fn() },
|
||||
filters: { allFilters: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
describe('filters thunkActions', () => {
|
||||
describe('updateIncludeCourseRoleMembers', () => {
|
||||
const getState = () => ({});
|
||||
const testVal = 'Hawaii';
|
||||
const filters = {
|
||||
cohort: 'COHort',
|
||||
track: 'TRacK',
|
||||
assignmentType: 'Prague',
|
||||
};
|
||||
const courseId = 'Some Course ID';
|
||||
let dispatch;
|
||||
beforeEach(() => {
|
||||
dispatch = jest.fn();
|
||||
selectors.filters.allFilters.mockReturnValue(filters);
|
||||
selectors.grades.courseId.mockReturnValue(courseId);
|
||||
updateIncludeCourseRoleMembers(testVal)(dispatch, getState);
|
||||
});
|
||||
it('dispatches filters.update.includeCoruseRoleMembers with passed value', () => {
|
||||
expect(dispatch.mock.calls[0][0]).toEqual(actions.filters.update.includeCourseRoleMembers(testVal));
|
||||
});
|
||||
it('dispatches fetchGrades with courseId, cohort, track, and assignmentType', () => {
|
||||
expect(dispatch.mock.calls[1][0]).toEqual(fetchGrades(
|
||||
courseId,
|
||||
filters.cohort,
|
||||
filters.track,
|
||||
filters.assignmentType,
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable import/no-self-import */
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import grades from '../actions/grades';
|
||||
import { sortAlphaAsc } from '../actions/utils';
|
||||
|
||||
@@ -19,16 +20,16 @@ const {
|
||||
formatGradeOverrideForDisplay,
|
||||
} = selectors.grades;
|
||||
|
||||
const defaultAssignmentFilter = 'All';
|
||||
export const defaultAssignmentFilter = 'All';
|
||||
|
||||
const fetchBulkUpgradeHistory = courseId => (
|
||||
export const fetchBulkUpgradeHistory = courseId => (
|
||||
// todo add loading effect
|
||||
dispatch => LmsApiService.fetchGradeBulkOperationHistory(courseId).then(
|
||||
(response) => { dispatch(grades.bulkHistory.received(response)); },
|
||||
).catch(() => dispatch(grades.bulkHistory.error()))
|
||||
);
|
||||
|
||||
const fetchGrades = (
|
||||
export const fetchGrades = (
|
||||
courseId,
|
||||
cohort,
|
||||
track,
|
||||
@@ -88,7 +89,7 @@ const fetchGrades = (
|
||||
}
|
||||
);
|
||||
|
||||
const fetchGradeOverrideHistory = (subsectionId, userId) => (
|
||||
export const fetchGradeOverrideHistory = (subsectionId, userId) => (
|
||||
dispatch => LmsApiService.fetchGradeOverrideHistory(subsectionId, userId)
|
||||
.then(response => response.data)
|
||||
.then((data) => {
|
||||
@@ -116,7 +117,7 @@ const fetchGradeOverrideHistory = (subsectionId, userId) => (
|
||||
})
|
||||
);
|
||||
|
||||
const fetchMatchingUserGrades = (
|
||||
export const fetchMatchingUserGrades = (
|
||||
courseId,
|
||||
searchText,
|
||||
cohort,
|
||||
@@ -129,7 +130,7 @@ const fetchMatchingUserGrades = (
|
||||
return module.fetchGrades(courseId, cohort, track, assignmentType, newOptions);
|
||||
};
|
||||
|
||||
const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
|
||||
export const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType) => (
|
||||
(dispatch) => {
|
||||
dispatch(grades.fetching.started());
|
||||
return getAuthenticatedHttpClient().get(endpoint)
|
||||
@@ -154,7 +155,7 @@ const fetchPrevNextGrades = (endpoint, courseId, cohort, track, assignmentType)
|
||||
}
|
||||
);
|
||||
|
||||
const submitFileUploadFormData = (courseId, formData) => (
|
||||
export const submitFileUploadFormData = (courseId, formData) => (
|
||||
(dispatch) => {
|
||||
dispatch(grades.csvUpload.started());
|
||||
return LmsApiService.uploadGradeCsv(courseId, formData).then(() => {
|
||||
@@ -171,7 +172,7 @@ const submitFileUploadFormData = (courseId, formData) => (
|
||||
}
|
||||
);
|
||||
|
||||
const updateGrades = (courseId, updateData, searchText, cohort, track) => (
|
||||
export const updateGrades = (courseId, updateData, searchText, cohort, track) => (
|
||||
(dispatch) => {
|
||||
dispatch(grades.update.request());
|
||||
return LmsApiService.updateGradebookData(courseId, updateData)
|
||||
@@ -194,7 +195,7 @@ const updateGrades = (courseId, updateData, searchText, cohort, track) => (
|
||||
}
|
||||
);
|
||||
|
||||
const updateGradesIfAssignmentGradeFiltersSet = (
|
||||
export const updateGradesIfAssignmentGradeFiltersSet = (
|
||||
courseId,
|
||||
cohort,
|
||||
track,
|
||||
@@ -213,8 +214,7 @@ const updateGradesIfAssignmentGradeFiltersSet = (
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
defaultAssignmentFilter,
|
||||
export default StrictDict({
|
||||
fetchBulkUpgradeHistory,
|
||||
fetchGrades,
|
||||
fetchGradeOverrideHistory,
|
||||
@@ -223,4 +223,4 @@ export {
|
||||
submitFileUploadFormData,
|
||||
updateGrades,
|
||||
updateGradesIfAssignmentGradeFiltersSet,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ import { sortAlphaAsc } from '../actions/utils';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import selectors from '../selectors';
|
||||
|
||||
import { createTestFetcher } from './testUtils';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
@@ -105,29 +107,6 @@ jest.mock('../selectors', () => ({
|
||||
|
||||
selectors.filters.allFilters.mockReturnValue(allFilters);
|
||||
|
||||
const createTestFetcher = (
|
||||
mockedMethod,
|
||||
thunkAction,
|
||||
args,
|
||||
onDispatch,
|
||||
) => (
|
||||
resolveFn,
|
||||
expectedActions,
|
||||
verifyFn,
|
||||
) => {
|
||||
const store = mockStore({});
|
||||
mockedMethod.mockReturnValue(new Promise(resolve => {
|
||||
resolve(new Promise(resolveFn));
|
||||
}));
|
||||
return store.dispatch(thunkAction(...args)).then(() => {
|
||||
onDispatch();
|
||||
if (verifyFn) {
|
||||
verifyFn();
|
||||
}
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
};
|
||||
|
||||
describe('grades thunkActions', () => {
|
||||
let oldSelectors;
|
||||
const mockSelectors = () => {
|
||||
|
||||
16
src/data/thunkActions/index.js
Normal file
16
src/data/thunkActions/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import assignmentTypes from './assignmentTypes';
|
||||
import cohorts from './cohorts';
|
||||
import filters from './filters';
|
||||
import grades from './grades';
|
||||
import roles from './roles';
|
||||
import tracks from './tracks';
|
||||
|
||||
export default StrictDict({
|
||||
assignmentTypes,
|
||||
cohorts,
|
||||
filters,
|
||||
grades,
|
||||
roles,
|
||||
tracks,
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { StrictDict } from 'utils';
|
||||
import roles from '../actions/roles';
|
||||
import selectors from '../selectors';
|
||||
|
||||
@@ -11,23 +12,20 @@ import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const allowedRoles = ['staff', 'instructor', 'support'];
|
||||
export const allowedRoles = ['staff', 'instructor', 'support'];
|
||||
|
||||
const getRoles = courseId => (
|
||||
export const fetchRoles = courseId => (
|
||||
(dispatch, getState) => LmsApiService.fetchUserRoles(courseId)
|
||||
.then(response => response.data)
|
||||
.then((response) => {
|
||||
const isAllowedRole = (role) => (
|
||||
(role.course_id === courseId) && allowedRoles.includes(role.role)
|
||||
);
|
||||
|
||||
const canUserViewGradebook = (response.is_staff || (response.roles.some(isAllowedRole)));
|
||||
|
||||
dispatch(roles.received({ canUserViewGradebook, courseId }));
|
||||
const {
|
||||
cohort,
|
||||
track,
|
||||
assignmentType,
|
||||
} = selectors.filters.allFilters(getState());
|
||||
|
||||
const { cohort, track, assignmentType } = selectors.filters.allFilters(getState());
|
||||
if (canUserViewGradebook) {
|
||||
dispatch(fetchGrades(courseId, cohort, track, assignmentType));
|
||||
dispatch(fetchTracks(courseId));
|
||||
@@ -39,6 +37,7 @@ const getRoles = courseId => (
|
||||
dispatch(roles.errorFetching());
|
||||
}));
|
||||
|
||||
export {
|
||||
getRoles,
|
||||
};
|
||||
export default StrictDict({
|
||||
allowedRoles,
|
||||
fetchRoles,
|
||||
});
|
||||
|
||||
@@ -1,166 +1,112 @@
|
||||
import axios from 'axios';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { createTestFetcher } from './testUtils';
|
||||
|
||||
import { configuration } from '../../config';
|
||||
import { getRoles } from './roles';
|
||||
import {
|
||||
GOT_ROLES,
|
||||
ERROR_FETCHING_ROLES,
|
||||
} from '../constants/actionTypes/roles';
|
||||
import { STARTED_FETCHING_GRADES } from '../constants/actionTypes/grades';
|
||||
import { STARTED_FETCHING_TRACKS } from '../constants/actionTypes/tracks';
|
||||
import { STARTED_FETCHING_COHORTS } from '../constants/actionTypes/cohorts';
|
||||
import { STARTED_FETCHING_ASSIGNMENT_TYPES } from '../constants/actionTypes/assignmentTypes';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import actions from '../actions';
|
||||
import selectors from '../selectors';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
import { fetchCohorts } from './cohorts';
|
||||
import { fetchGrades } from './grades';
|
||||
import { fetchTracks } from './tracks';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
getAuthenticatedHttpClient.mockReturnValue(axios);
|
||||
axios.isAccessTokenExpired = jest.fn();
|
||||
axios.isAccessTokenExpired.mockReturnValue(false);
|
||||
import { allowedRoles, fetchRoles } from './roles';
|
||||
|
||||
const course1Id = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const course2Id = 'course-v1:edX+DemoX+Demo_Course_2';
|
||||
const rolesUrl = `${configuration.LMS_BASE_URL}/api/enrollment/v1/roles/?course_id=${encodeURIComponent(course1Id)}`;
|
||||
jest.mock('../selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
filters: {
|
||||
allFilters: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('../services/LmsApiService', () => ({
|
||||
fetchUserRoles: jest.fn(),
|
||||
}));
|
||||
jest.mock('./assignmentTypes', () => ({
|
||||
fetchAssignmentTypes: jest.fn((...args) => ({ type: 'fetchAssignmentTypes', args })),
|
||||
}));
|
||||
jest.mock('./cohorts', () => ({
|
||||
fetchCohorts: jest.fn((...args) => ({ type: 'fetchCohorts', args })),
|
||||
}));
|
||||
jest.mock('./grades', () => ({
|
||||
fetchGrades: jest.fn((...args) => ({ type: 'fetchGrades', args })),
|
||||
}));
|
||||
jest.mock('./tracks', () => ({
|
||||
fetchTracks: jest.fn((...args) => ({ type: 'fetchTracks', args })),
|
||||
}));
|
||||
|
||||
function makeRoleListObj(roles, isGlobalStaff) {
|
||||
return {
|
||||
roles,
|
||||
is_staff: isGlobalStaff,
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const allowedRole = { course_id: courseId, role: allowedRoles[0] };
|
||||
const responseData = {
|
||||
roles: [
|
||||
{ course_id: 'fakeCourseId', role: 'fakeROLE' },
|
||||
{ couse_id: 'anotherId', role: 'STuff' },
|
||||
],
|
||||
is_staff: false,
|
||||
};
|
||||
|
||||
describe('roles thunkActions', () => {
|
||||
const filters = {
|
||||
cohort: 'COHort',
|
||||
track: 'traCK',
|
||||
assignmentType: 23,
|
||||
};
|
||||
}
|
||||
function makeRoleObj(courseId, role) {
|
||||
return {
|
||||
course_id: courseId,
|
||||
role,
|
||||
};
|
||||
}
|
||||
|
||||
const course1StaffRole = makeRoleObj(course1Id, 'staff');
|
||||
const course1DummyRole = makeRoleObj(course1Id, 'dummy');
|
||||
const course2StaffRole = makeRoleObj(course2Id, 'staff');
|
||||
const course2DummyRole = makeRoleObj(course2Id, 'dummy');
|
||||
const urlParams = { cohort: null, track: null };
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
beforeAll(() => {
|
||||
selectors.filters.allFilters.mockReturnValue(filters);
|
||||
});
|
||||
|
||||
describe('getRoles', () => {
|
||||
it('dispatches got_roles action and subsequent actions after fetching role that allows gradebook', () => {
|
||||
const expectedActions = [
|
||||
{ type: GOT_ROLES, canUserViewGradebook: true, courseId: course1Id },
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: STARTED_FETCHING_COHORTS },
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
];
|
||||
const store = mockStore();
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([course1StaffRole, course2DummyRole], false)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
describe('fetchRoles', () => {
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.fetchUserRoles,
|
||||
fetchRoles,
|
||||
[courseId],
|
||||
);
|
||||
describe('valid response', () => {
|
||||
describe('cannot view gradebook (not is_staff, and no allowed roles)', () => {
|
||||
it('dispatches received with canUserViewGradeBook=false and the courseId', () => (
|
||||
testFetch((resolve) => resolve({ data: responseData }), [
|
||||
actions.roles.received({
|
||||
canUserViewGradebook: false,
|
||||
courseId,
|
||||
}),
|
||||
])
|
||||
));
|
||||
});
|
||||
describe('canUserViewGradebook (is_staff or some role is allowed)', () => {
|
||||
const testCanUserViewGradebookOutput = (resolveData) => {
|
||||
const resolveFn = (resolve) => resolve({ data: resolveData });
|
||||
const expectedActions = [
|
||||
'received with canUserViewGradebook=false and the courseId',
|
||||
'fetchGrades thunkAction with courseId and filters(cohort, track, and assignmentType)',
|
||||
'fetchTracks thunkAction with courseId',
|
||||
'fetchCohorts thunkAction with courseId',
|
||||
'fetchAssignmentTypes thunkAction with courseId',
|
||||
];
|
||||
it(`dispatches the appropriate actions: [\n ${expectedActions.join('\n ')}\n]`, () => testFetch(
|
||||
resolveFn,
|
||||
[
|
||||
actions.roles.received({ canUserViewGradebook: true, courseId }),
|
||||
fetchGrades(courseId, filters.cohort, filters.track, filters.assignmentType),
|
||||
fetchTracks(courseId),
|
||||
fetchCohorts(courseId),
|
||||
fetchAssignmentTypes(courseId),
|
||||
],
|
||||
));
|
||||
};
|
||||
describe('is_staff', () => testCanUserViewGradebookOutput({
|
||||
...responseData,
|
||||
is_staff: true,
|
||||
}));
|
||||
describe('has allowed role', () => testCanUserViewGradebookOutput({
|
||||
...responseData,
|
||||
roles: [...responseData.roles, allowedRole],
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches got_roles action and other actions after fetching irrelevent roles but user is global staff', () => {
|
||||
const expectedActions = [
|
||||
{ type: GOT_ROLES, canUserViewGradebook: true, courseId: course1Id },
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: STARTED_FETCHING_COHORTS },
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([course1DummyRole, course2DummyRole], true)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches got_roles action and no other actions after fetching role that disallows gradebook', () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: GOT_ROLES, canUserViewGradebook: false, courseId: course1Id,
|
||||
},
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([course1DummyRole, course2StaffRole], false)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches got_roles action and no other actions after fetching empty roles', () => {
|
||||
const expectedActions = [
|
||||
{ type: GOT_ROLES, canUserViewGradebook: false, courseId: course1Id },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([], false)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches got_roles action and other actions after fetching empty roles but user is global staff', () => {
|
||||
const expectedActions = [
|
||||
{ type: GOT_ROLES, canUserViewGradebook: true, courseId: course1Id },
|
||||
{ type: STARTED_FETCHING_GRADES },
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: STARTED_FETCHING_COHORTS },
|
||||
{ type: STARTED_FETCHING_ASSIGNMENT_TYPES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl)
|
||||
.replyOnce(
|
||||
200,
|
||||
JSON.stringify(makeRoleListObj([], true)),
|
||||
);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches error action after getting an error when trying to get roles', () => {
|
||||
const expectedActions = [
|
||||
{ type: ERROR_FETCHING_ROLES },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(rolesUrl).replyOnce(400);
|
||||
|
||||
return store.dispatch(getRoles(course1Id, urlParams)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
describe('actions dispatched on api error', () => {
|
||||
test('errorFetching', () => testFetch(
|
||||
(resolve, reject) => reject(),
|
||||
[actions.roles.errorFetching()],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
32
src/data/thunkActions/testUtils.js
Normal file
32
src/data/thunkActions/testUtils.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
export const createTestFetcher = (
|
||||
mockedMethod,
|
||||
thunkAction,
|
||||
args,
|
||||
onDispatch,
|
||||
) => (
|
||||
resolveFn,
|
||||
expectedActions,
|
||||
verifyFn,
|
||||
) => {
|
||||
const store = mockStore({});
|
||||
mockedMethod.mockReturnValue(new Promise(resolve => {
|
||||
resolve(new Promise(resolveFn));
|
||||
}));
|
||||
return store.dispatch(thunkAction(...args)).then(() => {
|
||||
if (onDispatch) { onDispatch(); }
|
||||
if (verifyFn) { verifyFn(); }
|
||||
if (expectedActions !== undefined) {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
createTestFetcher,
|
||||
};
|
||||
@@ -1,15 +1,14 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { StrictDict } from 'utils';
|
||||
import tracks from '../actions/tracks';
|
||||
|
||||
import selectors from '../selectors';
|
||||
|
||||
import {
|
||||
fetchBulkUpgradeHistory,
|
||||
} from './grades';
|
||||
import { fetchBulkUpgradeHistory } from './grades';
|
||||
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
|
||||
const fetchTracks = courseId => (
|
||||
export const fetchTracks = courseId => (
|
||||
(dispatch) => {
|
||||
dispatch(tracks.fetching.started());
|
||||
return LmsApiService.fetchTracks(courseId)
|
||||
@@ -26,6 +25,6 @@ const fetchTracks = courseId => (
|
||||
}
|
||||
);
|
||||
|
||||
export {
|
||||
export default StrictDict({
|
||||
fetchTracks,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,87 +1,85 @@
|
||||
import axios from 'axios';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import thunk from 'redux-thunk';
|
||||
import { createTestFetcher } from './testUtils';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { configuration } from '../../config';
|
||||
import LmsApiService from '../services/LmsApiService';
|
||||
import actions from '../actions';
|
||||
import selectors from '../selectors';
|
||||
|
||||
import { fetchBulkUpgradeHistory } from './grades';
|
||||
import { fetchTracks } from './tracks';
|
||||
import {
|
||||
STARTED_FETCHING_TRACKS,
|
||||
GOT_TRACKS,
|
||||
ERROR_FETCHING_TRACKS,
|
||||
} from '../constants/actionTypes/tracks';
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
jest.mock('../services/LmsApiService', () => ({
|
||||
fetchTracks: jest.fn(),
|
||||
}));
|
||||
jest.mock('../selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
tracks: { hasMastersTrack: jest.fn(() => false) },
|
||||
},
|
||||
}));
|
||||
jest.mock('./grades', () => ({
|
||||
fetchBulkUpgradeHistory: jest.fn((...args) => ({ type: 'fetchBulkUpgradeHistory', args })),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
getAuthenticatedHttpClient.mockReturnValue(axios);
|
||||
axios.isAccessTokenExpired = jest.fn();
|
||||
axios.isAccessTokenExpired.mockReturnValue(false);
|
||||
|
||||
describe('actions', () => {
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const responseData = {
|
||||
couse_modes: ['some', 'course', 'modes'],
|
||||
};
|
||||
|
||||
describe('tracjs thunkActions', () => {
|
||||
describe('fetchTracks', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const trackUrl = `${configuration.LMS_BASE_URL}/api/enrollment/v1/course/${courseId}?include_expired=1`;
|
||||
|
||||
it('dispatches success action after fetching tracks', () => {
|
||||
const responseData = {
|
||||
course_modes: [
|
||||
{
|
||||
slug: 'audit',
|
||||
name: 'Audit',
|
||||
min_price: 0,
|
||||
suggested_prices: '',
|
||||
currency: 'usd',
|
||||
expiration_datetime: null,
|
||||
description: null,
|
||||
sku: '68EFFFF',
|
||||
bulk_sku: null,
|
||||
},
|
||||
{
|
||||
slug: 'verified',
|
||||
name: 'Verified Certificate',
|
||||
min_price: 100,
|
||||
suggested_prices: '',
|
||||
currency: 'usd',
|
||||
expiration_datetime: '2021-05-04T18:08:12.644361Z',
|
||||
description: null,
|
||||
sku: '8CF08E5',
|
||||
bulk_sku: 'A5B6DBE',
|
||||
}],
|
||||
};
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: GOT_TRACKS, tracks: responseData.course_modes },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(trackUrl)
|
||||
.replyOnce(200, JSON.stringify(responseData));
|
||||
|
||||
return store.dispatch(fetchTracks(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
const testFetch = createTestFetcher(
|
||||
LmsApiService.fetchTracks,
|
||||
fetchTracks,
|
||||
[courseId],
|
||||
);
|
||||
describe('valid response', () => {
|
||||
describe('if not hasMastersTrack(data.course_modes)', () => {
|
||||
describe('dispatched actions', () => {
|
||||
beforeEach(() => {
|
||||
selectors.tracks.hasMastersTrack.mockReturnValue(false);
|
||||
});
|
||||
const expectedActions = [
|
||||
'tracks.fetching.started',
|
||||
'tracks.fetching.received with course_modes'
|
||||
];
|
||||
it(`dispatches [${expectedActions.join(', ')}]`, () => testFetch(
|
||||
(resolve) => resolve({ data: responseData }),
|
||||
[
|
||||
actions.tracks.fetching.started(),
|
||||
actions.tracks.fetching.received(responseData.course_modes),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
describe('if hasMastersTrack(data.course_modes)', () => {
|
||||
describe('dispatched actions', () => {
|
||||
beforeEach(() => {
|
||||
selectors.tracks.hasMastersTrack.mockReturnValue(true);
|
||||
});
|
||||
const expectedActions = [
|
||||
'fetching.started',
|
||||
'fetching.received with course_modes',
|
||||
'fetchBulkUpgradeHistory thunkAction with courseId',
|
||||
];
|
||||
test(`[${expectedActions.join(', ')}]`, () => testFetch(
|
||||
(resolve) => resolve({ data: responseData }),
|
||||
[
|
||||
actions.tracks.fetching.started(),
|
||||
actions.tracks.fetching.received(responseData.course_modes),
|
||||
fetchBulkUpgradeHistory(courseId),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches failure action after fetching tracks', () => {
|
||||
const expectedActions = [
|
||||
{ type: STARTED_FETCHING_TRACKS },
|
||||
{ type: ERROR_FETCHING_TRACKS },
|
||||
];
|
||||
const store = mockStore();
|
||||
|
||||
axiosMock.onGet(trackUrl)
|
||||
.replyOnce(500, JSON.stringify({}));
|
||||
|
||||
return store.dispatch(fetchTracks(courseId)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
describe('actions dispatched on api error', () => {
|
||||
test('errorFetching', () => testFetch(
|
||||
(resolve, reject) => reject(),
|
||||
[
|
||||
actions.tracks.fetching.started(),
|
||||
actions.tracks.fetching.error(),
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user