Compare commits

..

12 Commits

Author SHA1 Message Date
Ben Warzeski
4ad034e210 chore: update package-lock to node 18 2023-05-25 14:49:31 +00:00
Ben Warzeski
4125638e65 chore: update snapshot 2023-05-25 14:46:19 +00:00
Ben Warzeski
7f18fc07d1 chore: update package-lock 2023-05-25 14:44:39 +00:00
Ben Warzeski
b6473f1db9 chore: rebase and npm install 2023-05-25 13:53:14 +00:00
Ben Warzeski
5e838c4dee chore: redux hooks 2023-05-25 13:52:43 +00:00
Ben Warzeski
16a77a1c8e chore: add top-level data selectors 2023-05-25 13:52:42 +00:00
Ben Warzeski
9ef0f1b356 chore: redux transform hooks 2023-05-25 13:52:42 +00:00
Ben Warzeski
2895af5012 chore: top-level formatDate util 2023-05-25 13:52:42 +00:00
Ben Warzeski
a1d132ffda chore: add paragon icons and components to mocks 2023-05-25 13:52:42 +00:00
Ben Warzeski
b1281680f0 fix: downgrade jest to avoid a date bug 2023-05-25 13:52:40 +00:00
Bilal Qamar
3be81e02ea feat: upgraded to node v18, added .nvmrc and updated workflows (#317)
* Merge branch 'master' of github.com:edx/frontend-app-gradebook

* feat: upgraded to node v18, added .nvmrc and updated workflows

* build: updated frontend-build, frontend-platform, component-footer & component-header packages

* refactor: updated packages

* fix: resolved test case failure window redefine issue

* Merge branch 'master' of github.com:edx/frontend-app-gradebook into bilalqamar95/node-v18-upgrade

* refactor: pinned node to v18.15 in nvmrc
2023-05-23 19:11:54 +05:00
jszewczulak
ffecce993e feat: added "Get Feedback" widget (#330) 2023-05-11 09:53:48 -04:00
21 changed files with 1707 additions and 29973 deletions

View File

@@ -11,22 +11,18 @@ on:
jobs:
test:
runs-on: ubuntu-20.04
strategy:
matrix:
node: [16]
npm: [8.5.x]
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- name: Install npm 8.5.x
run: npm install -g npm@${{ matrix.npm }}
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
run: npm ci

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

View File

@@ -15,10 +15,13 @@ jobs:
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
run: npm ci

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18.15

31237
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -67,7 +67,7 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "^12.4.15",
"@edx/frontend-build": "12.8.27",
"@testing-library/react": "^12.1.0",
"axios": "0.21.2",
"axios-mock-adapter": "^1.17.0",
@@ -76,7 +76,7 @@
"fetch-mock": "^6.5.2",
"husky": "2.7.0",
"identity-obj-proxy": "^3.0.0",
"jest": "29.3.1",
"jest": "^26.6.3",
"react-dev-utils": "^12.0.1",
"react-test-renderer": "^16.10.1",
"reactifex": "1.1.1",

View File

@@ -16,7 +16,7 @@ exports[`GradebookFilters render snapshot 1`] = `
className="p-1"
iconAs="Icon"
onClick={[MockFunction hook.closeMenu]}
src={[Function]}
src="Close"
/>
</div>
<Collapsible

View File

@@ -4,6 +4,11 @@ import { actionHook } from './utils';
const app = StrictDict({
useSetLocalFilter: actionHook(actions.app.setLocalFilter),
useSetSearchValue: actionHook(actions.app.setSearchValue),
useSetShowImportSuccessToast: actionHook(actions.app.setShowImportSuccessToast),
useSetView: actionHook(actions.app.setView),
useCloseModal: actionHook(actions.app.closeModal),
useSetModalState: actionHook(actions.app.setModalState),
});
const filters = StrictDict({
@@ -14,9 +19,19 @@ const filters = StrictDict({
useUpdateCourseGradeLimits: actionHook(actions.filters.update.courseGradeLimits),
useUpdateIncludeCourseRoleMembers: actionHook(actions.filters.update.includeCourseRoleMembers),
useUpdateTrack: actionHook(actions.filters.update.track),
useResetFilters: actionHook(actions.filters.reset),
});
const grades = StrictDict({
useDoneViewingAssignment: actionHook(actions.grades.doneViewingAssignment),
useDownloadBulkGradesReport: actionHook(actions.grades.downloadReport.bulkGrades),
useDownloadInterventionReport: actionHook(actions.grades.downloadReport.intervention),
useToggleGradeFormat: actionHook(actions.grades.toggleGradeFormat),
useCloseBanner: actionHook(actions.grades.banner.close),
});
export default StrictDict({
app,
filters,
grades,
});

View File

@@ -4,17 +4,6 @@ import actions from 'data/actions';
import { actionHook } from './utils';
import actionHooks from './actions';
jest.mock('data/actions', () => ({
app: {
setLocalFilter: jest.fn(),
},
filters: {
update: {
assignment: jest.fn(),
assignmentLimits: jest.fn(),
},
},
}));
jest.mock('./utils', () => ({
actionHook: (action) => ({ actionHook: action }),
}));
@@ -24,6 +13,7 @@ let hooks;
const testActionHook = (hookKey, action) => {
test(hookKey, () => {
expect(hooks[hookKey]).toEqual(actionHook(action));
expect(hooks[hookKey]).not.toEqual(undefined);
});
};
@@ -32,6 +22,11 @@ describe('action hooks', () => {
const hookKeys = keyStore(actionHooks.app);
beforeEach(() => { hooks = actionHooks.app; });
testActionHook(hookKeys.useSetLocalFilter, actions.app.setLocalFilter);
testActionHook(hookKeys.useSetSearchValue, actions.app.setSearchValue);
testActionHook(hookKeys.useSetShowImportSuccessToast, actions.app.setShowImportSuccessToast);
testActionHook(hookKeys.useSetView, actions.app.setView);
testActionHook(hookKeys.useCloseModal, actions.app.closeModal);
testActionHook(hookKeys.useSetModalState, actions.app.setModalState);
});
describe('filters', () => {
const hookKeys = keyStore(actionHooks.filters);
@@ -39,12 +34,23 @@ describe('action hooks', () => {
beforeEach(() => { hooks = actionHooks.filters; });
testActionHook(hookKeys.useUpdateAssignment, actionGroup.assignment);
testActionHook(hookKeys.useUpdateAssignmentLimits, actionGroup.assignmentLimits);
testActionHook(hookKeys.useUpdateCohort, actionGroup.updateCohort);
testActionHook(hookKeys.useUpdateAssignmentType, actionGroup.assignmentType);
testActionHook(hookKeys.useUpdateCohort, actionGroup.cohort);
testActionHook(hookKeys.useUpdateCourseGradeLimits, actionGroup.courseGradeLimits);
testActionHook(
hookKeys.useUpdateIncludeCourseRoleMembers,
actionGroup.updateIncludeCourseRoleMembers,
actionGroup.includeCourseRoleMembers,
);
testActionHook(hookKeys.useUpdateTrack, actionGroup.updateTrack);
testActionHook(hookKeys.useResetFilters, actions.filters.reset);
});
describe('grades', () => {
const hookKeys = keyStore(actionHooks.grades);
const actionGroup = actions.grades;
beforeEach(() => { hooks = actionHooks.grades; });
testActionHook(hookKeys.useDoneViewingAssignment, actionGroup.doneViewingAssignment);
testActionHook(hookKeys.useDownloadBulkGradesReport, actionGroup.downloadReport.bulkGrades);
testActionHook(hookKeys.useDownloadInterventionReport, actionGroup.downloadReport.intervention);
testActionHook(hookKeys.useToggleGradeFormat, actionGroup.toggleGradeFormat);
testActionHook(hookKeys.useCloseBanner, actionGroup.banner.close);
});
});

View File

@@ -3,40 +3,75 @@ import { useSelector } from 'react-redux';
import { StrictDict } from 'utils';
import selectors from 'data/selectors';
const selectorHook = (selector) => () => useSelector(selector);
export const root = StrictDict({
useGradeExportUrl: () => useSelector(selectors.root.gradeExportUrl),
useSelectedCohortEntry: () => useSelector(selectors.root.selectedCohortEntry),
useSelectedTrackEntry: () => useSelector(selectors.root.selectedTrackEntry),
useEditModalPossibleGrade: selectorHook(selectors.root.editModalPossibleGrade),
useGetHeadings: selectorHook(selectors.root.getHeadings),
useGradeExportUrl: selectorHook(selectors.root.gradeExportUrl),
useInterventionExportUrl: selectorHook(selectors.root.interventionExportUrl),
useSelectedCohortEntry: selectorHook(selectors.root.selectedCohortEntry),
useSelectedTrackEntry: selectorHook(selectors.root.selectedTrackEntry),
useShouldShowSpinner: selectorHook(selectors.root.shouldShowSpinner),
useShowBulkManagement: selectorHook(selectors.root.showBulkManagement),
useFilterBadgeConfig: (filterName) => useSelector(
(state) => selectors.root.filterBadgeConfig(state, filterName),
),
});
export const app = StrictDict({
useAssignmentGradeLimits: () => useSelector(selectors.app.assignmentGradeLimits),
useAreCourseGradeFiltersValid: () => useSelector(selectors.app.areCourseGradeFiltersValid),
useCourseGradeLimits: () => useSelector(selectors.app.courseGradeLimits),
useActiveView: selectorHook(selectors.app.activeView),
useAssignmentGradeLimits: selectorHook(selectors.app.assignmentGradeLimits),
useAreCourseGradeFiltersValid: selectorHook(selectors.app.areCourseGradeFiltersValid),
useCourseGradeLimits: selectorHook(selectors.app.courseGradeLimits),
useCourseGradeFilterValidity: selectorHook(selectors.app.courseGradeFilterValidity),
useCourseId: selectorHook(selectors.app.courseId),
useModalData: selectorHook(selectors.app.modalData),
useSearchValue: selectorHook(selectors.app.searchValue),
useShowImportSuccessToast: selectorHook(selectors.app.showImportSuccessToast),
});
export const assignmentTypes = StrictDict({
useAllAssignmentTypes: () => useSelector(selectors.assignmentTypes.allAssignmentTypes),
useAllAssignmentTypes: selectorHook(selectors.assignmentTypes.allAssignmentTypes),
useAreGradesFrozen: selectorHook(selectors.assignmentTypes.areGradesFrozen),
});
export const cohorts = StrictDict({
useAllCohorts: () => useSelector(selectors.cohorts.allCohorts),
useAllCohorts: selectorHook(selectors.cohorts.allCohorts),
// maybe not needed?
useCohortsByName: () => useSelector(selectors.cohorts.cohortsByName),
useCohortsByName: selectorHook(selectors.cohorts.cohortsByName),
});
export const filters = StrictDict({
useData: () => useSelector(selectors.filters.allFilters),
useIncludeCourseRoleMembers: () => useSelector(selectors.filters.includeCourseRoleMembers),
useSelectableAssignmentLabels: () => useSelector(selectors.filters.selectableAssignmentLabels),
useSelectedAssignmentLabel: () => useSelector(selectors.filters.selectedAssignmentLabel),
useAssignmentType: () => useSelector(selectors.filters.assignmentType),
useData: selectorHook(selectors.filters.allFilters),
useIncludeCourseRoleMembers: selectorHook(selectors.filters.includeCourseRoleMembers),
useSelectableAssignmentLabels: selectorHook(selectors.filters.selectableAssignmentLabels),
useSelectedAssignmentLabel: selectorHook(selectors.filters.selectedAssignmentLabel),
useAssignmentType: selectorHook(selectors.filters.assignmentType),
});
export const grades = StrictDict({
useAllGrades: selectorHook(selectors.grades.allGrades),
useUserCounts: () => ({
filteredUsersCount: useSelector(selectors.grades.filteredUsersCount),
totalUsersCount: useSelector(selectors.grades.totalUsersCount),
}),
useGradeData: selectorHook(selectors.grades.gradeData),
useHasOverrideErrors: selectorHook(selectors.grades.hasOverrideErrors),
useShowSuccess: selectorHook(selectors.grades.showSuccess),
useSubsectionGrade: ({ gradeFormat, subsection }) => () => (
selectors.grades.subsectionGrade[gradeFormat](subsection)
),
});
export const roles = StrictDict({
useCanUserViewGradebook: selectorHook(selectors.roles.canUserViewGradebook),
});
export const tracks = StrictDict({
useAllTracks: () => useSelector(selectors.tracks.allTracks),
useAllTracks: selectorHook(selectors.tracks.allTracks),
// maybe not needed?
useTracksByName: () => useSelector(selectors.tracks.tracksByName),
useTracksByName: selectorHook(selectors.tracks.tracksByName),
});
export default StrictDict({
@@ -44,6 +79,8 @@ export default StrictDict({
assignmentTypes,
cohorts,
filters,
grades,
roles,
tracks,
root,
});

View File

@@ -7,85 +7,102 @@ jest.mock('react-redux', () => ({
useSelector: (selector) => ({ useSelector: selector }),
}));
jest.mock('data/selectors', () => ({
app: {
assignmentGradeLimits: jest.fn(),
areCourseGradeFiltersValid: jest.fn(),
courseGradelimits: jest.fn(),
},
assignmentTypes: { allAssignmentTypes: jest.fn() },
cohorts: {
allCohorts: jest.fn(),
cohortsByName: jest.fn(),
},
filters: {
allFilters: jest.fn(),
includeCourseRoleMembers: jest.fn(),
selectableAssignmentLabels: jest.fn(),
selectedAssignmentLabel: jest.fn(),
assignmentType: jest.fn(),
},
tracks: {
allTracks: jest.fn(),
tracksByName: jest.fn(),
},
root: {
gradeExportUrl: jest.fn(),
selectedCohortEntry: jest.fn(),
selectedTrackEntry: jest.fn(),
},
}));
const testValue = 'test-value';
const testState = { test: 'state value' };
let hookKeys;
let hooks;
const testHook = (hookKey, selector) => {
test(hookKey, () => {
expect(hooks[hookKey]()).toEqual(useSelector(selector));
let selKeys;
let selectorGroup;
const loadSelectorGroup = (hookGroup, selGroup) => {
hookKeys = keyStore(hookGroup);
selKeys = keyStore(selGroup);
beforeEach(() => {
hooks = hookGroup;
selectorGroup = selGroup;
});
};
const testHook = (hookKey, selectorKey) => {
test(hookKey, () => {
expect(hooks[hookKey]()).toEqual(useSelector(selectorGroup[selectorKey]));
});
};
describe('selector hooks', () => {
describe('root selectors', () => {
const hookKeys = keyStore(selectorHooks.root);
beforeEach(() => { hooks = selectorHooks.root; });
testHook(hookKeys.useGradeExportUrl, selectors.root.gradeExportUrl);
testHook(hookKeys.useSelectedCohortEntry, selectors.root.selectedCohortEntry);
testHook(hookKeys.useSelectedTrackEntry, selectors.root.selectedTrackEntry);
loadSelectorGroup(selectorHooks.root, selectors.root);
testHook(hookKeys.useEditModalPossibleGrade, selKeys.editModalPossibleGrade);
testHook(hookKeys.useGetHeadings, selKeys.getHeadings);
testHook(hookKeys.useGradeExportUrl, selKeys.gradeExportUrl);
testHook(hookKeys.useInterventionExportUrl, selKeys.interventionExportUrl);
testHook(hookKeys.useSelectedCohortEntry, selKeys.selectedCohortEntry);
testHook(hookKeys.useSelectedTrackEntry, selKeys.selectedTrackEntry);
testHook(hookKeys.useShouldShowSpinner, selKeys.shouldShowSpinner);
testHook(hookKeys.useShowBulkManagement, selKeys.showBulkManagement);
describe(hookKeys.useFilterBadgeConfig, () => {
test('calls filterBadgeConfig selector with passed filterName', () => {
const filterBadgeConfig = (state, filterName) => ({
filterBadgeConfig: { state, filterName },
});
const rootKeys = keyStore(selectors.root);
jest.spyOn(selectors.root, rootKeys.filterBadgeConfig)
.mockImplementation(filterBadgeConfig);
const out = hooks.useFilterBadgeConfig(testValue);
expect(out.useSelector(testState)).toEqual(filterBadgeConfig(testState, testValue));
});
});
});
describe('app', () => {
const hookKeys = keyStore(selectorHooks.app);
const selGroup = selectors.app;
beforeEach(() => { hooks = selectorHooks.app; });
testHook(hookKeys.useAssignmentGradeLimits, selGroup.assignmentGradeLimits);
testHook(hookKeys.useAreCourseGradeFiltersValid, selGroup.areCourseGradeFiltersValid);
testHook(hookKeys.useCourseGradeLimits, selGroup.courseGradeLimits);
loadSelectorGroup(selectorHooks.app, selectors.app);
testHook(hookKeys.useActiveView, selKeys.activeView);
testHook(hookKeys.useAssignmentGradeLimits, selKeys.assignmentGradeLimits);
testHook(hookKeys.useAreCourseGradeFiltersValid, selKeys.areCourseGradeFiltersValid);
testHook(hookKeys.useCourseGradeLimits, selKeys.courseGradeLimits);
testHook(hookKeys.useCourseId, selKeys.courseId);
testHook(hookKeys.useModalData, selKeys.modalData);
testHook(hookKeys.useSearchValue, selKeys.searchValue);
testHook(hookKeys.useShowImportSuccessToast, selKeys.showImportSuccessToast);
});
describe('assignmentTypes', () => {
const hookKeys = keyStore(selectorHooks.assignmentTypes);
const selGroup = selectors.assignmentTypes;
beforeEach(() => { hooks = selectorHooks.assignmentTypes; });
testHook(hookKeys.useAllAssignmentTypes, selGroup.allAssignmentTypes);
loadSelectorGroup(selectorHooks.assignmentTypes, selectors.assignmentTypes);
testHook(hookKeys.useAllAssignmentTypes, selKeys.allAssignmentTypes);
testHook(hookKeys.useAreGradesFrozen, selKeys.areGradesFrozen);
});
describe('cohorts', () => {
const hookKeys = keyStore(selectorHooks.cohorts);
const selGroup = selectors.cohorts;
beforeEach(() => { hooks = selectorHooks.cohorts; });
testHook(hookKeys.useAllCohorts, selGroup.allCohorts);
testHook(hookKeys.useCohortsByName, selGroup.cohortsByName);
loadSelectorGroup(selectorHooks.cohorts, selectors.cohorts);
testHook(hookKeys.useAllCohorts, selKeys.allCohorts);
testHook(hookKeys.useCohortsByName, selKeys.cohortsByName);
});
describe('filters', () => {
const hookKeys = keyStore(selectorHooks.filters);
const selGroup = selectors.filters;
beforeEach(() => { hooks = selectorHooks.filters; });
testHook(hookKeys.useData, selGroup.allFilters);
testHook(hookKeys.useIncludeCourseRoleMembers, selGroup.includeCourseRoleMembers);
testHook(hookKeys.useSelectableAssignmentLabels, selGroup.selectableAssignmentLabels);
testHook(hookKeys.useSelectedAssignmentLabel, selGroup.selectedAssignmentLabel);
testHook(hookKeys.useAssignmentType, selGroup.assignmentType);
loadSelectorGroup(selectorHooks.filters, selectors.filters);
testHook(hookKeys.useData, selKeys.allFilters);
testHook(hookKeys.useIncludeCourseRoleMembers, selKeys.includeCourseRoleMembers);
testHook(hookKeys.useSelectableAssignmentLabels, selKeys.selectableAssignmentLabels);
testHook(hookKeys.useSelectedAssignmentLabel, selKeys.selectedAssignmentLabel);
testHook(hookKeys.useAssignmentType, selKeys.assignmentType);
});
describe('grades', () => {
loadSelectorGroup(selectorHooks.grades, selectors.grades);
testHook(hookKeys.useAllGrades, selKeys.allGrades);
testHook(hookKeys.useGradeData, selKeys.gradeData);
testHook(hookKeys.useHasOverrideErrors, selKeys.hasOverrideErrors);
testHook(hookKeys.useShowSuccess, selKeys.showSuccess);
test(hookKeys.useUserCounts, () => {
expect(hooks.useUserCounts()).toEqual({
filteredUsersCount: useSelector(selectors.grades.filteredUsersCount),
totalUsersCount: useSelector(selectors.grades.totalUsersCount),
});
});
});
describe('roles', () => {
loadSelectorGroup(selectorHooks.roles, selectors.roles);
testHook(hookKeys.useCanUserViewGradebook, selKeys.canUserViewGradebook);
});
describe('tracks', () => {
const hookKeys = keyStore(selectorHooks.tracks);
const selGroup = selectors.tracks;
beforeEach(() => { hooks = selectorHooks.tracks; });
testHook(hookKeys.useAllTracks, selGroup.allTracks);
testHook(hookKeys.useTracksByName, selGroup.tracksByName);
loadSelectorGroup(selectorHooks.tracks, selectors.tracks);
testHook(hookKeys.useAllTracks, selKeys.allTracks);
testHook(hookKeys.useTracksByName, selKeys.tracksByName);
});
});

View File

@@ -3,15 +3,22 @@ import thunkActions from 'data/thunkActions';
import { actionHook } from './utils';
const app = StrictDict({
useCloseFilterMenu: actionHook(thunkActions.app.filterMenu.close),
filterMenu: {
useCloseMenu: actionHook(thunkActions.app.filterMenu.close),
useHandleTransitionEnd: actionHook(thunkActions.app.filterMenu.handleTransitionEnd),
useToggleMenu: actionHook(thunkActions.app.filterMenu.toggle),
},
useSetModalStateFromTable: actionHook(thunkActions.app.setModalStateFromTable),
});
const grades = StrictDict({
useFetchGradesIfAssignmentGradeFiltersSet: actionHook(
thunkActions.grades.fetchGradesIfAssignmentGradeFiltersSet,
),
useFetchPrevNextGrades: actionHook(thunkActions.grades.fetchPrevNextGrades),
useFetchGrades: actionHook(thunkActions.grades.fetchGrades),
useSubmitImportGradesButtonData: actionHook(thunkActions.grades.submitImportGradesButtonData),
useUpdateGrades: actionHook(thunkActions.grades.updateGrades),
});
export default StrictDict({

View File

@@ -5,7 +5,11 @@ import thunkActionHooks from './thunkActions';
jest.mock('data/thunkActions', () => ({
app: {
filterMenu: { close: jest.fn() },
filterMenu: {
close: jest.fn(),
handleTransitionEnd: jest.fn(),
toggle: jest.fn(),
},
},
grades: {
fetchGrades: jest.fn(),
@@ -25,24 +29,38 @@ const testActionHook = (hookKey, action) => {
expect(hooks[hookKey]).toEqual(actionHook(action));
});
};
let hookKeys;
describe('thunkAction hooks', () => {
describe('app', () => {
const hookKeys = keyStore(thunkActionHooks.app);
hookKeys = keyStore(thunkActionHooks.app);
beforeEach(() => { hooks = thunkActionHooks.app; });
testActionHook(hookKeys.useCloseFilterMenu, thunkActions.app.filterMenu.close);
testActionHook(hookKeys.useSetModalStateFromTable, thunkActions.app.setModalStateFromTable);
describe('filterMenu', () => {
hookKeys = keyStore(thunkActionHooks.app.filterMenu);
beforeEach(() => { hooks = thunkActionHooks.app.filterMenu; });
testActionHook(hookKeys.useCloseMenu, thunkActions.app.filterMenu.close);
testActionHook(
hookKeys.useHandleTransitionEnd,
thunkActions.app.filterMenu.handleTransitionEnd,
);
testActionHook(hookKeys.useToggleMenu, thunkActions.app.filterMenu.toggle);
});
});
describe('grades', () => {
const hookKeys = keyStore(thunkActionHooks.grades);
hookKeys = keyStore(thunkActionHooks.grades);
const actionGroup = thunkActions.grades;
beforeEach(() => { hooks = thunkActionHooks.grades; });
testActionHook(hookKeys.useFetchGrades, actionGroup.fetchGrades);
testActionHook(
hookKeys.useFetchGradesIfAssignmentGradeFiltersSet,
actionGroup.fetchGradesIfAssignmentGradeFiltersSet,
);
testActionHook(hookKeys.useFetchPrevNextGrades, actionGroup.fetchPrevNextGrades);
testActionHook(hookKeys.useFetchGrades, actionGroup.fetchGrades);
testActionHook(
hookKeys.useSubmitImportGradesButtonData,
actionGroup.submitImportGradesButtonData,
);
testActionHook(hookKeys.useUpdateGrades, actionGroup.updateGrades);
});
});

View File

@@ -0,0 +1,13 @@
import { StrictDict } from 'utils';
import selectors from 'data/selectors';
export const grades = StrictDict({
subsectionGrade: ({ gradeFormat, subsection }) => () => (
selectors.grades.subsectionGrade[gradeFormat](subsection)
),
roundGrade: selectors.grades.roundGrade,
});
export default StrictDict({
grades,
});

View File

@@ -0,0 +1,38 @@
import selectors from 'data/selectors';
import { GradeFormats } from 'data/constants/grades';
import transforms from './transforms';
jest.mock('data/selectors', () => {
const {
GradeFormats: { absolute, percent },
} = jest.requireActual('data/constants/grades');
return {
grades: {
subsectionGrade: {
[absolute]: jest.fn(v => ({ absolute: v })),
[percent]: jest.fn(v => ({ percent: v })),
},
roundGrade: jest.fn(),
},
};
});
describe('redux transforms', () => {
describe('grades transforms', () => {
test('subsectionGrade', () => {
const subsection = 'test-subsection';
expect(transforms.grades.subsectionGrade({
gradeFormat: GradeFormats.absolute,
subsection,
})()).toEqual(selectors.grades.subsectionGrade.absolute(subsection));
expect(transforms.grades.subsectionGrade({
gradeFormat: GradeFormats.percent,
subsection,
})()).toEqual(selectors.grades.subsectionGrade.percent(subsection));
});
test('roundGrade', () => {
expect(transforms.grades.roundGrade).toEqual(selectors.grades.roundGrade);
});
});
});

View File

@@ -89,6 +89,16 @@ const modalSelectors = simpleSelectorFactory(
],
);
const modalData = ({ app: { modalState } }) => ({
assignmentName: modalState.assignmentName,
adjustedGradePossible: modalState.adjustedGradePossible,
adjustedGradeValue: modalState.adjustedGradeValue,
open: modalState.open,
reasonForChange: modalState.reasonForChange,
todaysDate: modalState.todaysDate,
updateUserName: modalState.updateUserName,
});
const filterMenuSelectors = simpleSelectorFactory(
({ app: { filterMenu } }) => filterMenu,
['open', 'transitioning'],
@@ -115,6 +125,7 @@ export default StrictDict({
isFilterMenuOpening,
...simpleSelectors,
modalState: StrictDict(modalSelectors),
modalData,
filterMenu: StrictDict({
...filterMenuSelectors,
isClosed: isFilterMenuClosed,

View File

@@ -266,12 +266,26 @@ const simpleSelectors = simpleSelectorFactory(
'gradeOverrideHistoryError',
'gradeOriginalEarnedGraded',
'gradeOriginalPossibleGraded',
'nextPage',
'prevPage',
'showSuccess',
],
);
const gradeData = ({ grades }) => ({
courseId: grades.courseId,
filteredUsersCount: grades.filteredUsersCount,
totalUsersCount: grades.totalUsersCount,
gradeFormat: grades.gradeFormat,
showSpinner: grades.showSpinner,
gradeOverrideCurrentEarnedGradedOverride: grades.gradeOverrideCurrentEarnedGradedOverride,
gradeOverrideHistoryError: grades.gradeOverrideHistoryError,
gradeOverrideHistoryResults: grades.gradeOverrideHistoryResults,
gradeOriginalEarnedGraded: grades.gradeOriginalEarnedGraded,
gradeOriginalPossibleGraded: grades.gradeOriginalPossibleGraded,
nextPage: grades.nextPage,
prevPage: grades.prevPage,
showSuccess: grades.showSuccess,
});
export default StrictDict({
bulkImportError,
formatGradeOverrideForDisplay,
@@ -286,6 +300,7 @@ export default StrictDict({
subsectionGrade,
...simpleSelectors,
gradeData,
allGrades,
bulkManagementHistoryEntries,
getExampleSectionBreakdown,

View File

@@ -10,6 +10,7 @@ import {
mergeConfig,
subscribe,
} from '@edx/frontend-platform';
import lightning from './lightning';
import messages from './i18n';

View File

@@ -39,6 +39,11 @@ jest.mock('@edx/frontend-component-footer', () => ({
messages: ['some', 'messages'],
}));
jest.mock('@edx/paragon/icons', () => ({
FilterAlt: 'FilterAlt',
Close: 'Close',
}));
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
Alert: 'Alert',
ActionRow: 'ActionRow',
@@ -75,6 +80,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Hyperlink: 'Hyperlink',
Icon: 'Icon',
IconButton: 'IconButton',
Input: 'Input',
ModalDialog: {
Body: 'ModalDialog.Body',
CloseButton: 'ModalDialog.CloseButton',
@@ -84,8 +90,10 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
},
OverlayTrigger: 'OverlayTrigger',
Row: 'Row',
StatefulButton: 'StatefulButton',
SearchField: 'SearchField',
Spinner: 'Spinner',
StatefulButton: 'StatefulButton',
Toast: 'Toast',
useCheckboxSetValues: () => jest.fn().mockImplementation((values) => ([values, {
add: jest.fn().mockName('useCheckboxSetValues.add'),

20
src/utils/formatDate.js Normal file
View File

@@ -0,0 +1,20 @@
export const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
};
export const timeOptions = {
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC',
timeZoneName: 'short',
};
const formatDateForDisplay = (inputDate) => {
const date = inputDate.toLocaleDateString('en-US', options);
const time = inputDate.toLocaleTimeString('en-US', timeOptions);
return `${date} at ${time}`;
};
export default formatDateForDisplay;

View File

@@ -1,3 +1,4 @@
/* eslint-disable import/prefer-default-export */
export { default as StrictDict } from './StrictDict';
export { default as keyStore } from './keyStore';
export { default as formatDateForDisplay } from './formatDate';