Compare commits
23 Commits
master
...
bw/compone
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e19b5774c | ||
|
|
f7a4309888 | ||
|
|
3e3a73e2bb | ||
|
|
af8d7182ef | ||
|
|
d898a9cc2f | ||
|
|
f4b839f4d8 | ||
|
|
f41b237d08 | ||
|
|
6dd2fb3dd6 | ||
|
|
b173681edb | ||
|
|
5fcde3b9e8 | ||
|
|
35ee68ea9d | ||
|
|
dde8e759b6 | ||
|
|
6b149e9ce0 | ||
|
|
4cf5ba7a07 | ||
|
|
7a506324a8 | ||
|
|
7f54cc4917 | ||
|
|
134ace9483 | ||
|
|
0e6f52fca9 | ||
|
|
ca64cc614a | ||
|
|
1ad297c46c | ||
|
|
f2bb0d7c2a | ||
|
|
f76f3d64c9 | ||
|
|
db56d76d37 |
15189
package-lock.json
generated
15189
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,7 @@
|
|||||||
"fetch-mock": "^6.5.2",
|
"fetch-mock": "^6.5.2",
|
||||||
"husky": "2.7.0",
|
"husky": "2.7.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "29.3.1",
|
"jest": "^26.6.3",
|
||||||
"react-dev-utils": "^12.0.1",
|
"react-dev-utils": "^12.0.1",
|
||||||
"react-test-renderer": "^16.10.1",
|
"react-test-renderer": "^16.10.1",
|
||||||
"reactifex": "1.1.1",
|
"reactifex": "1.1.1",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ exports[`GradebookFilters render snapshot 1`] = `
|
|||||||
className="p-1"
|
className="p-1"
|
||||||
iconAs="Icon"
|
iconAs="Icon"
|
||||||
onClick={[MockFunction hook.closeMenu]}
|
onClick={[MockFunction hook.closeMenu]}
|
||||||
src={[Function]}
|
src="Close"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { actions, selectors, thunkActions } from 'data/redux/hooks';
|
|||||||
export const useGradebookFiltersData = ({ updateQueryParams }) => {
|
export const useGradebookFiltersData = ({ updateQueryParams }) => {
|
||||||
const includeCourseRoleMembers = selectors.filters.useIncludeCourseRoleMembers();
|
const includeCourseRoleMembers = selectors.filters.useIncludeCourseRoleMembers();
|
||||||
const updateIncludeCourseRoleMembers = actions.filters.useUpdateIncludeCourseRoleMembers();
|
const updateIncludeCourseRoleMembers = actions.filters.useUpdateIncludeCourseRoleMembers();
|
||||||
const closeMenu = thunkActions.app.useCloseFilterMenu();
|
const closeMenu = thunkActions.app.filterMenu.useCloseMenu();
|
||||||
const fetchGrades = thunkActions.grades.useFetchGrades();
|
const fetchGrades = thunkActions.grades.useFetchGrades();
|
||||||
|
|
||||||
const handleIncludeTeamMembersChange = ({ target: { checked } }) => {
|
const handleIncludeTeamMembersChange = ({ target: { checked } }) => {
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ jest.mock('data/redux/hooks', () => ({
|
|||||||
filters: { useIncludeCourseRoleMembers: jest.fn() },
|
filters: { useIncludeCourseRoleMembers: jest.fn() },
|
||||||
},
|
},
|
||||||
thunkActions: {
|
thunkActions: {
|
||||||
app: { useCloseFilterMenu: jest.fn() },
|
app: {
|
||||||
|
filterMenu: { useCloseMenu: jest.fn() },
|
||||||
|
},
|
||||||
grades: { useFetchGrades: jest.fn() },
|
grades: { useFetchGrades: jest.fn() },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -18,7 +20,7 @@ selectors.filters.useIncludeCourseRoleMembers.mockReturnValue(true);
|
|||||||
const updateIncludeCourseRoleMembers = jest.fn();
|
const updateIncludeCourseRoleMembers = jest.fn();
|
||||||
actions.filters.useUpdateIncludeCourseRoleMembers.mockReturnValue(updateIncludeCourseRoleMembers);
|
actions.filters.useUpdateIncludeCourseRoleMembers.mockReturnValue(updateIncludeCourseRoleMembers);
|
||||||
const closeFilterMenu = jest.fn();
|
const closeFilterMenu = jest.fn();
|
||||||
thunkActions.app.useCloseFilterMenu.mockReturnValue(closeFilterMenu);
|
thunkActions.app.filterMenu.useCloseMenu.mockReturnValue(closeFilterMenu);
|
||||||
const fetchGrades = jest.fn();
|
const fetchGrades = jest.fn();
|
||||||
thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades);
|
thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades);
|
||||||
|
|
||||||
@@ -34,7 +36,7 @@ describe('GradebookFiltersData component hooks', () => {
|
|||||||
it('initializes hooks', () => {
|
it('initializes hooks', () => {
|
||||||
expect(actions.filters.useUpdateIncludeCourseRoleMembers).toHaveBeenCalledWith();
|
expect(actions.filters.useUpdateIncludeCourseRoleMembers).toHaveBeenCalledWith();
|
||||||
expect(selectors.filters.useIncludeCourseRoleMembers).toHaveBeenCalledWith();
|
expect(selectors.filters.useIncludeCourseRoleMembers).toHaveBeenCalledWith();
|
||||||
expect(thunkActions.app.useCloseFilterMenu).toHaveBeenCalledWith();
|
expect(thunkActions.app.filterMenu.useCloseMenu).toHaveBeenCalledWith();
|
||||||
expect(thunkActions.grades.useFetchGrades).toHaveBeenCalledWith();
|
expect(thunkActions.grades.useFetchGrades).toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
131
src/components/GradebookHeader/__snapshots__/index.test.jsx.snap
Normal file
131
src/components/GradebookHeader/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`GradebookHeader component render default view shapshot 1`] = `
|
||||||
|
<div
|
||||||
|
className="gradebook-header"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="mb-3"
|
||||||
|
href="test-dashboard-url"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<<
|
||||||
|
</span>
|
||||||
|
Back to Dashboard
|
||||||
|
</a>
|
||||||
|
<h1>
|
||||||
|
Gradebook
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
test-course-id
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`GradebookHeader component render frozen grades snapshot: show frozen warning 1`] = `
|
||||||
|
<div
|
||||||
|
className="gradebook-header"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="mb-3"
|
||||||
|
href="test-dashboard-url"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<<
|
||||||
|
</span>
|
||||||
|
Back to Dashboard
|
||||||
|
</a>
|
||||||
|
<h1>
|
||||||
|
Gradebook
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
test-course-id
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="alert alert-warning"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
The grades for this course are now frozen. Editing of grades is no longer allowed.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`GradebookHeader component render show bulk management snapshot: show toggle view message button with handleToggleViewClick method 1`] = `
|
||||||
|
<div
|
||||||
|
className="gradebook-header"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="mb-3"
|
||||||
|
href="test-dashboard-url"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<<
|
||||||
|
</span>
|
||||||
|
Back to Dashboard
|
||||||
|
</a>
|
||||||
|
<h1>
|
||||||
|
Gradebook
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
test-course-id
|
||||||
|
</h2>
|
||||||
|
<Button
|
||||||
|
onClick={[MockFunction hooks.handleToggleViewClick]}
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
toggle-view-message
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`GradebookHeader component render user cannot view gradebook snapshot: show unauthorized warning 1`] = `
|
||||||
|
<div
|
||||||
|
className="gradebook-header"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="mb-3"
|
||||||
|
href="test-dashboard-url"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<<
|
||||||
|
</span>
|
||||||
|
Back to Dashboard
|
||||||
|
</a>
|
||||||
|
<h1>
|
||||||
|
Gradebook
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
test-course-id
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="alert alert-warning"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
You are not authorized to view the gradebook for this course.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`GradebookHeader component snapshots default values (grades frozen, cannot view). unauthorized warning, but no grades frozen warning 1`] = `
|
|
||||||
<div
|
|
||||||
className="gradebook-header"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className="mb-3"
|
|
||||||
href="http://localhost:18000/courses/fakeID/instructor"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<<
|
|
||||||
</span>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Back to Dashboard"
|
|
||||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
|
||||||
id="gradebook.GradebookHeader.backButton"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<h1>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Gradebook"
|
|
||||||
description="Top-level app title in Gradebook Header component"
|
|
||||||
id="gradebook.GradebookHeader.appLabel"
|
|
||||||
/>
|
|
||||||
</h1>
|
|
||||||
<div
|
|
||||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
fakeID
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="alert alert-warning"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
|
||||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
|
||||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`GradebookHeader component snapshots grades frozen, can view. grades frozen warning but no unauthorized warning 1`] = `
|
|
||||||
<div
|
|
||||||
className="gradebook-header"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className="mb-3"
|
|
||||||
href="http://localhost:18000/courses/fakeID/instructor"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<<
|
|
||||||
</span>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Back to Dashboard"
|
|
||||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
|
||||||
id="gradebook.GradebookHeader.backButton"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<h1>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Gradebook"
|
|
||||||
description="Top-level app title in Gradebook Header component"
|
|
||||||
id="gradebook.GradebookHeader.appLabel"
|
|
||||||
/>
|
|
||||||
</h1>
|
|
||||||
<div
|
|
||||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
fakeID
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="alert alert-warning"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
|
|
||||||
description="Warning message in Gradebook Header for frozen messages"
|
|
||||||
id="gradebook.GradebookHeader.frozenWarning"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`GradebookHeader component snapshots grades frozen, cannot view unauthorized warning, and grades frozen warning. 1`] = `
|
|
||||||
<div
|
|
||||||
className="gradebook-header"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className="mb-3"
|
|
||||||
href="http://localhost:18000/courses/fakeID/instructor"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<<
|
|
||||||
</span>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Back to Dashboard"
|
|
||||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
|
||||||
id="gradebook.GradebookHeader.backButton"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<h1>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Gradebook"
|
|
||||||
description="Top-level app title in Gradebook Header component"
|
|
||||||
id="gradebook.GradebookHeader.appLabel"
|
|
||||||
/>
|
|
||||||
</h1>
|
|
||||||
<div
|
|
||||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
fakeID
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="alert alert-warning"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="The grades for this course are now frozen. Editing of grades is no longer allowed."
|
|
||||||
description="Warning message in Gradebook Header for frozen messages"
|
|
||||||
id="gradebook.GradebookHeader.frozenWarning"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="alert alert-warning"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
|
||||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
|
||||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`GradebookHeader component snapshots show bulk management, active view is bulkManagementHistory view toggle view button to grades 1`] = `
|
|
||||||
<div
|
|
||||||
className="gradebook-header"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className="mb-3"
|
|
||||||
href="http://localhost:18000/courses/fakeID/instructor"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<<
|
|
||||||
</span>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Back to Dashboard"
|
|
||||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
|
||||||
id="gradebook.GradebookHeader.backButton"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<h1>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Gradebook"
|
|
||||||
description="Top-level app title in Gradebook Header component"
|
|
||||||
id="gradebook.GradebookHeader.appLabel"
|
|
||||||
/>
|
|
||||||
</h1>
|
|
||||||
<div
|
|
||||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
fakeID
|
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
onClick={[MockFunction this.handleToggleViewClick]}
|
|
||||||
variant="tertiary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Return to Gradebook"
|
|
||||||
description="Button text for button navigating to Grades view."
|
|
||||||
id="gradebook.GradebookHeader.toGradesView"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="alert alert-warning"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
|
||||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
|
||||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`GradebookHeader component snapshots show bulk management, active view is grades view toggle view button to activity log 1`] = `
|
|
||||||
<div
|
|
||||||
className="gradebook-header"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
className="mb-3"
|
|
||||||
href="http://localhost:18000/courses/fakeID/instructor"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<<
|
|
||||||
</span>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Back to Dashboard"
|
|
||||||
description="Button text to take user back to LMS dashboard in Gradebook Header"
|
|
||||||
id="gradebook.GradebookHeader.backButton"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<h1>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Gradebook"
|
|
||||||
description="Top-level app title in Gradebook Header component"
|
|
||||||
id="gradebook.GradebookHeader.appLabel"
|
|
||||||
/>
|
|
||||||
</h1>
|
|
||||||
<div
|
|
||||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
fakeID
|
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
onClick={[MockFunction this.handleToggleViewClick]}
|
|
||||||
variant="tertiary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="View Bulk Management History"
|
|
||||||
description="Button text for button navigating to Bulk Managment Activity Log"
|
|
||||||
id="gradebook.GradebookHeader.toActivityLogButton"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="alert alert-warning"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="You are not authorized to view the gradebook for this course."
|
|
||||||
description="Warning message in Gradebook Header when user is not allowed to view the app"
|
|
||||||
id="gradebook.GradebookHeader.unauthorizedWarning"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
35
src/components/GradebookHeader/hooks.js
Normal file
35
src/components/GradebookHeader/hooks.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { views } from 'data/constants/app';
|
||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
export const useGradebookHeaderData = () => {
|
||||||
|
const activeView = selectors.app.useActiveView();
|
||||||
|
const courseId = selectors.app.useCourseId();
|
||||||
|
const areGradesFrozen = selectors.assignmentTypes.useAreGradesFrozen();
|
||||||
|
const canUserViewGradebook = selectors.roles.useCanUserViewGradebook();
|
||||||
|
const showBulkManagement = selectors.root.useShowBulkManagement();
|
||||||
|
const setView = actions.app.useSetView();
|
||||||
|
|
||||||
|
const handleToggleViewClick = () => setView(
|
||||||
|
activeView === views.grades
|
||||||
|
? views.bulkManagementHistory
|
||||||
|
: views.grades,
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleViewMessage = activeView === views.grades
|
||||||
|
? messages.toActivityLog
|
||||||
|
: messages.toGradesView;
|
||||||
|
|
||||||
|
return {
|
||||||
|
areGradesFrozen,
|
||||||
|
canUserViewGradebook,
|
||||||
|
courseId,
|
||||||
|
showBulkManagement,
|
||||||
|
|
||||||
|
handleToggleViewClick,
|
||||||
|
toggleViewMessage,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGradebookHeaderData;
|
||||||
90
src/components/GradebookHeader/hooks.test.js
Normal file
90
src/components/GradebookHeader/hooks.test.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { views } from 'data/constants/app';
|
||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
import useGradebookHeaderData from './hooks';
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
actions: {
|
||||||
|
app: {
|
||||||
|
useSetView: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
app: {
|
||||||
|
useActiveView: jest.fn(),
|
||||||
|
useCourseId: jest.fn(),
|
||||||
|
},
|
||||||
|
assignmentTypes: {
|
||||||
|
useAreGradesFrozen: jest.fn(),
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
useCanUserViewGradebook: jest.fn(),
|
||||||
|
},
|
||||||
|
root: {
|
||||||
|
useShowBulkManagement: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const activeView = 'test-active-view';
|
||||||
|
selectors.app.useActiveView.mockReturnValue(activeView);
|
||||||
|
const courseId = 'test-course-id';
|
||||||
|
selectors.app.useCourseId.mockReturnValue(courseId);
|
||||||
|
const areGradesFrozen = 'test-are-grades-frozen';
|
||||||
|
selectors.assignmentTypes.useAreGradesFrozen.mockReturnValue(areGradesFrozen);
|
||||||
|
const canUserViewGradebook = 'test-can-user-view-gradebook';
|
||||||
|
selectors.roles.useCanUserViewGradebook.mockReturnValue(canUserViewGradebook);
|
||||||
|
const showBulkManagement = 'test-show-bulk-management';
|
||||||
|
selectors.root.useShowBulkManagement.mockReturnValue(showBulkManagement);
|
||||||
|
|
||||||
|
const setView = jest.fn();
|
||||||
|
actions.app.useSetView.mockReturnValue(setView);
|
||||||
|
|
||||||
|
let out;
|
||||||
|
describe('useGradebookHeaderData hooks', () => {
|
||||||
|
describe('initialization', () => {
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
out = useGradebookHeaderData();
|
||||||
|
expect(selectors.app.useActiveView).toHaveBeenCalled();
|
||||||
|
expect(selectors.app.useCourseId).toHaveBeenCalled();
|
||||||
|
expect(selectors.assignmentTypes.useAreGradesFrozen).toHaveBeenCalled();
|
||||||
|
expect(selectors.roles.useCanUserViewGradebook).toHaveBeenCalled();
|
||||||
|
expect(selectors.root.useShowBulkManagement).toHaveBeenCalled();
|
||||||
|
expect(actions.app.useSetView).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('output', () => {
|
||||||
|
test('redux fields', () => {
|
||||||
|
out = useGradebookHeaderData();
|
||||||
|
expect(out.areGradesFrozen).toEqual(areGradesFrozen);
|
||||||
|
expect(out.canUserViewGradebook).toEqual(canUserViewGradebook);
|
||||||
|
expect(out.courseId).toEqual(courseId);
|
||||||
|
expect(out.showBulkManagement).toEqual(showBulkManagement);
|
||||||
|
});
|
||||||
|
describe('handleToggleViewClick', () => {
|
||||||
|
it('calls setView with bulkManagemnetHistory message if grades view is active', () => {
|
||||||
|
selectors.app.useActiveView.mockReturnValueOnce(views.grades);
|
||||||
|
out = useGradebookHeaderData();
|
||||||
|
out.handleToggleViewClick();
|
||||||
|
expect(setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||||
|
});
|
||||||
|
it('calls setView with grades view if grades view is not active', () => {
|
||||||
|
out = useGradebookHeaderData();
|
||||||
|
out.handleToggleViewClick();
|
||||||
|
expect(setView).toHaveBeenCalledWith(views.grades);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('toggleViewMessage', () => {
|
||||||
|
it('returns toActivityLog message if grades view is active', () => {
|
||||||
|
selectors.app.useActiveView.mockReturnValueOnce(views.grades);
|
||||||
|
out = useGradebookHeaderData();
|
||||||
|
expect(out.toggleViewMessage).toEqual(messages.toActivityLog);
|
||||||
|
});
|
||||||
|
it('returns toGradesView message if grades view is not active', () => {
|
||||||
|
out = useGradebookHeaderData();
|
||||||
|
expect(out.toggleViewMessage).toEqual(messages.toGradesView);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,106 +1,50 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Button } from '@edx/paragon';
|
import { Button } from '@edx/paragon';
|
||||||
|
|
||||||
import { views } from 'data/constants/app';
|
import { instructorDashboardUrl } from 'data/services/lms/urls';
|
||||||
import actions from 'data/actions';
|
import useGradebookHeaderData from './hooks';
|
||||||
import selectors from 'data/selectors';
|
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export class GradebookHeader extends React.Component {
|
export const GradebookHeader = () => {
|
||||||
constructor(props) {
|
const { formatMessage } = useIntl();
|
||||||
super(props);
|
const {
|
||||||
this.handleToggleViewClick = this.handleToggleViewClick.bind(this);
|
areGradesFrozen,
|
||||||
}
|
canUserViewGradebook,
|
||||||
|
courseId,
|
||||||
handleToggleViewClick() {
|
handleToggleViewClick,
|
||||||
const newView = this.props.activeView === views.grades ? views.bulkManagementHistory : views.grades;
|
showBulkManagement,
|
||||||
this.props.setView(newView);
|
toggleViewMessage,
|
||||||
}
|
} = useGradebookHeaderData();
|
||||||
|
const dashboardUrl = instructorDashboardUrl();
|
||||||
get toggleViewMessage() {
|
return (
|
||||||
return this.props.activeView === views.grades
|
<div className="gradebook-header">
|
||||||
? messages.toActivityLog
|
<a href={dashboardUrl} className="mb-3">
|
||||||
: messages.toGradesView;
|
<span aria-hidden="true">{'<< '}</span>
|
||||||
}
|
{formatMessage(messages.backToDashboard)}
|
||||||
|
</a>
|
||||||
lmsInstructorDashboardUrl = courseId => (
|
<h1>{formatMessage(messages.gradebook)}</h1>
|
||||||
`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor`
|
<div className="subtitle-row d-flex justify-content-between align-items-center">
|
||||||
);
|
<h2>{courseId}</h2>
|
||||||
|
{showBulkManagement && (
|
||||||
render() {
|
<Button variant="tertiary" onClick={handleToggleViewClick}>
|
||||||
return (
|
{formatMessage(toggleViewMessage)}
|
||||||
<div className="gradebook-header">
|
</Button>
|
||||||
<a
|
|
||||||
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
|
|
||||||
className="mb-3"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">{'<< '}</span>
|
|
||||||
<FormattedMessage {...messages.backToDashboard} />
|
|
||||||
</a>
|
|
||||||
<h1>
|
|
||||||
<FormattedMessage {...messages.gradebook} />
|
|
||||||
</h1>
|
|
||||||
<div className="subtitle-row d-flex justify-content-between align-items-center">
|
|
||||||
<h2>{this.props.courseId}</h2>
|
|
||||||
{ this.props.showBulkManagement && (
|
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
onClick={this.handleToggleViewClick}
|
|
||||||
>
|
|
||||||
<FormattedMessage {...this.toggleViewMessage} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{this.props.areGradesFrozen
|
|
||||||
&& (
|
|
||||||
<div className="alert alert-warning" role="alert">
|
|
||||||
<FormattedMessage {...messages.frozenWarning} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(this.props.canUserViewGradebook === false) && (
|
|
||||||
<div className="alert alert-warning" role="alert">
|
|
||||||
<FormattedMessage {...messages.unauthorizedWarning} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
{areGradesFrozen && (
|
||||||
}
|
<div className="alert alert-warning" role="alert">
|
||||||
}
|
{formatMessage(messages.frozenWarning)}
|
||||||
|
</div>
|
||||||
GradebookHeader.defaultProps = {
|
)}
|
||||||
// redux
|
{(canUserViewGradebook === false) && (
|
||||||
courseId: '',
|
<div className="alert alert-warning" role="alert">
|
||||||
areGradesFrozen: false,
|
{formatMessage(messages.unauthorizedWarning)}
|
||||||
canUserViewGradebook: false,
|
</div>
|
||||||
showBulkManagement: false,
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
GradebookHeader.propTypes = {
|
export default GradebookHeader;
|
||||||
// redux
|
|
||||||
activeView: PropTypes.string.isRequired,
|
|
||||||
courseId: PropTypes.string,
|
|
||||||
areGradesFrozen: PropTypes.bool,
|
|
||||||
canUserViewGradebook: PropTypes.bool,
|
|
||||||
setView: PropTypes.func.isRequired,
|
|
||||||
showBulkManagement: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
|
||||||
activeView: selectors.app.activeView(state),
|
|
||||||
courseId: selectors.app.courseId(state),
|
|
||||||
areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
|
|
||||||
canUserViewGradebook: selectors.roles.canUserViewGradebook(state),
|
|
||||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mapDispatchToProps = {
|
|
||||||
setView: actions.app.setView,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(GradebookHeader);
|
|
||||||
|
|||||||
77
src/components/GradebookHeader/index.test.jsx
Normal file
77
src/components/GradebookHeader/index.test.jsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Button } from '@edx/paragon';
|
||||||
|
|
||||||
|
import { formatMessage } from 'testUtils';
|
||||||
|
import { instructorDashboardUrl } from 'data/services/lms/urls';
|
||||||
|
|
||||||
|
import useGradebookHeaderData from './hooks';
|
||||||
|
import GradebookHeader from '.';
|
||||||
|
|
||||||
|
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||||
|
jest.mock('data/services/lms/urls', () => ({
|
||||||
|
instructorDashboardUrl: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
instructorDashboardUrl.mockReturnValue('test-dashboard-url');
|
||||||
|
|
||||||
|
const hookProps = {
|
||||||
|
areGradesFrozen: false,
|
||||||
|
canUserViewGradebook: true,
|
||||||
|
courseId: 'test-course-id',
|
||||||
|
handleToggleViewClick: jest.fn().mockName('hooks.handleToggleViewClick'),
|
||||||
|
showBulkManagement: false,
|
||||||
|
toggleViewMessage: { defaultMessage: 'toggle-view-message' },
|
||||||
|
};
|
||||||
|
useGradebookHeaderData.mockReturnValue(hookProps);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('GradebookHeader component', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
el = shallow(<GradebookHeader />);
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes hooks', () => {
|
||||||
|
expect(useGradebookHeaderData).toHaveBeenCalledWith();
|
||||||
|
expect(useIntl).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('render', () => {
|
||||||
|
describe('default view', () => {
|
||||||
|
test('shapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('show bulk management', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true });
|
||||||
|
el = shallow(<GradebookHeader />);
|
||||||
|
});
|
||||||
|
test('snapshot: show toggle view message button with handleToggleViewClick method', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
const { onClick, children } = el.find(Button).props();
|
||||||
|
expect(onClick).toEqual(hookProps.handleToggleViewClick);
|
||||||
|
expect(children).toEqual(formatMessage(hookProps.toggleViewMessage));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('frozen grades', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true });
|
||||||
|
el = shallow(<GradebookHeader />);
|
||||||
|
});
|
||||||
|
test('snapshot: show frozen warning', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('user cannot view gradebook', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false });
|
||||||
|
el = shallow(<GradebookHeader />);
|
||||||
|
});
|
||||||
|
test('snapshot: show unauthorized warning', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import { Button } from '@edx/paragon';
|
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import { views } from 'data/constants/app';
|
|
||||||
import messages from './messages';
|
|
||||||
import { GradebookHeader, mapDispatchToProps, mapStateToProps } from '.';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({
|
|
||||||
Button: () => 'Button',
|
|
||||||
}));
|
|
||||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
|
||||||
defineMessages: m => m,
|
|
||||||
FormattedMessage: () => 'FormattedMessage',
|
|
||||||
}));
|
|
||||||
jest.mock('data/actions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: { setView: jest.fn() },
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: {
|
|
||||||
activeView: jest.fn(state => ({ aciveView: state })),
|
|
||||||
courseId: jest.fn(state => ({ courseId: state })),
|
|
||||||
},
|
|
||||||
assignmentTypes: { areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })) },
|
|
||||||
roles: { canUserViewGradebook: jest.fn(state => ({ canUserViewGradebook: state })) },
|
|
||||||
root: { showBulkManagement: jest.fn(state => ({ showBulkManagement: state })) },
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const courseId = 'fakeID';
|
|
||||||
describe('GradebookHeader component', () => {
|
|
||||||
const props = {
|
|
||||||
activeView: views.grades,
|
|
||||||
areGradesFrozen: false,
|
|
||||||
canUserViewGradebook: false,
|
|
||||||
courseId,
|
|
||||||
showBulkManagement: false,
|
|
||||||
};
|
|
||||||
beforeEach(() => {
|
|
||||||
props.setView = jest.fn();
|
|
||||||
});
|
|
||||||
describe('snapshots', () => {
|
|
||||||
let el;
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<GradebookHeader {...props} />);
|
|
||||||
el.instance().handleToggleViewClick = jest.fn().mockName('this.handleToggleViewClick');
|
|
||||||
});
|
|
||||||
describe('default values (grades frozen, cannot view).', () => {
|
|
||||||
test('unauthorized warning, but no grades frozen warning', () => {
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('grades frozen, cannot view', () => {
|
|
||||||
test('unauthorized warning, and grades frozen warning.', () => {
|
|
||||||
el.setProps({ areGradesFrozen: true });
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('grades frozen, can view.', () => {
|
|
||||||
test('grades frozen warning but no unauthorized warning', () => {
|
|
||||||
el.setProps({ areGradesFrozen: true, canUserViewGradebook: true });
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('show bulk management, active view is grades view', () => {
|
|
||||||
test('toggle view button to activity log', () => {
|
|
||||||
el.setProps({ showBulkManagement: true });
|
|
||||||
expect(el.find(Button).getElement()).toEqual((
|
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
onClick={el.instance().handleToggleViewClick}
|
|
||||||
>
|
|
||||||
<FormattedMessage {...messages.toActivityLog} />
|
|
||||||
</Button>
|
|
||||||
));
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('show bulk management, active view is bulkManagementHistory view', () => {
|
|
||||||
test('toggle view button to grades', () => {
|
|
||||||
el.setProps({ showBulkManagement: true, activeView: views.bulkManagementHistory });
|
|
||||||
expect(el.find(Button).getElement()).toEqual((
|
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
onClick={el.instance().handleToggleViewClick}
|
|
||||||
>
|
|
||||||
<FormattedMessage {...messages.toGradesView} />
|
|
||||||
</Button>
|
|
||||||
));
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('behavior', () => {
|
|
||||||
let el;
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<GradebookHeader {...props} />);
|
|
||||||
});
|
|
||||||
describe('handleToggleViewClick', () => {
|
|
||||||
test('calls setView with activity view if activeView is grades', () => {
|
|
||||||
el.instance().handleToggleViewClick();
|
|
||||||
expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
|
||||||
});
|
|
||||||
test('calls setView with grades view if activeView is bulkManagementHistory', () => {
|
|
||||||
el.setProps({ activeView: views.bulkManagementHistory });
|
|
||||||
el.instance().handleToggleViewClick();
|
|
||||||
expect(props.setView).toHaveBeenCalledWith(views.grades);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
let mapped;
|
|
||||||
const testState = { a: 'test', example: 'state' };
|
|
||||||
beforeEach(() => {
|
|
||||||
mapped = mapStateToProps(testState);
|
|
||||||
});
|
|
||||||
test('activeView from app.activeView', () => {
|
|
||||||
expect(mapped.activeView).toEqual(selectors.app.activeView(testState));
|
|
||||||
});
|
|
||||||
test('courseId from app.courseId', () => {
|
|
||||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
|
||||||
});
|
|
||||||
test('areGradesFrozen from assignmentTypes selector', () => {
|
|
||||||
expect(
|
|
||||||
mapped.areGradesFrozen,
|
|
||||||
).toEqual(selectors.assignmentTypes.areGradesFrozen(testState));
|
|
||||||
});
|
|
||||||
test('canUserViewGradebook from roles selector', () => {
|
|
||||||
expect(
|
|
||||||
mapped.canUserViewGradebook,
|
|
||||||
).toEqual(selectors.roles.canUserViewGradebook(testState));
|
|
||||||
});
|
|
||||||
test('showBulkManagement from root showBulkManagement selector', () => {
|
|
||||||
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('mapDispatchToProps', () => {
|
|
||||||
test('setView from actions.app.setView', () => {
|
|
||||||
expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { views } from 'data/constants/app';
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
|
|
||||||
import NetworkButton from 'components/NetworkButton';
|
|
||||||
import ImportGradesButton from './ImportGradesButton';
|
|
||||||
|
|
||||||
import messages from './BulkManagementControls.messages';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <BulkManagementControls />
|
|
||||||
* Provides download buttons for Bulk Management and Intervention reports, only if
|
|
||||||
* showBulkManagement is set in redus.
|
|
||||||
*/
|
|
||||||
export class BulkManagementControls extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.handleClickExportGrades = this.handleClickExportGrades.bind(this);
|
|
||||||
this.handleViewActivityLog = this.handleViewActivityLog.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClickExportGrades() {
|
|
||||||
this.props.downloadBulkGradesReport();
|
|
||||||
window.location.assign(this.props.gradeExportUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleViewActivityLog() {
|
|
||||||
this.props.setView(views.bulkManagementHistory);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return this.props.showBulkManagement && (
|
|
||||||
<div className="d-flex">
|
|
||||||
<NetworkButton
|
|
||||||
label={messages.downloadGradesBtn}
|
|
||||||
onClick={this.handleClickExportGrades}
|
|
||||||
/>
|
|
||||||
<ImportGradesButton />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BulkManagementControls.defaultProps = {
|
|
||||||
showBulkManagement: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
BulkManagementControls.propTypes = {
|
|
||||||
// redux
|
|
||||||
downloadBulkGradesReport: PropTypes.func.isRequired,
|
|
||||||
gradeExportUrl: PropTypes.string.isRequired,
|
|
||||||
showBulkManagement: PropTypes.bool,
|
|
||||||
setView: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
|
||||||
gradeExportUrl: selectors.root.gradeExportUrl(state),
|
|
||||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mapDispatchToProps = {
|
|
||||||
downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
|
|
||||||
setView: actions.app.setView,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import { views } from 'data/constants/app';
|
|
||||||
|
|
||||||
import {
|
|
||||||
BulkManagementControls,
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
} from './BulkManagementControls';
|
|
||||||
|
|
||||||
jest.mock('./ImportGradesButton', () => 'ImportGradesButton');
|
|
||||||
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
root: {
|
|
||||||
gradeExportUrl: (state) => ({ gradeExportUrl: state }),
|
|
||||||
interventionExportUrl: (state) => ({ interventionExportUrl: state }),
|
|
||||||
showBulkManagement: (state) => ({ showBulkManagement: state }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('data/actions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: { setView: jest.fn() },
|
|
||||||
grades: {
|
|
||||||
downloadReport: {
|
|
||||||
bulkGrades: jest.fn(),
|
|
||||||
intervention: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('BulkManagementControls', () => {
|
|
||||||
describe('component', () => {
|
|
||||||
let el;
|
|
||||||
let props = {
|
|
||||||
gradeExportUrl: 'gradesGoHere',
|
|
||||||
interventionExportUrl: 'interventionsGoHere',
|
|
||||||
};
|
|
||||||
beforeEach(() => {
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
downloadBulkGradesReport: jest.fn(),
|
|
||||||
downloadInterventionReport: jest.fn(),
|
|
||||||
setView: jest.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
test('snapshot - empty if showBulkManagement is not truthy', () => {
|
|
||||||
expect(shallow(<BulkManagementControls {...props} />)).toEqual({});
|
|
||||||
});
|
|
||||||
describe('behavior', () => {
|
|
||||||
const oldWindowLocation = window.location;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
delete window.location;
|
|
||||||
window.location = Object.defineProperties(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
...Object.getOwnPropertyDescriptors(oldWindowLocation),
|
|
||||||
assign: {
|
|
||||||
configurable: true,
|
|
||||||
value: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
beforeEach(() => {
|
|
||||||
window.location.assign.mockReset();
|
|
||||||
el = shallow(<BulkManagementControls {...props} showBulkManagement />);
|
|
||||||
});
|
|
||||||
afterAll(() => {
|
|
||||||
// restore `window.location` to the `jsdom` `Location` object
|
|
||||||
window.location = oldWindowLocation;
|
|
||||||
});
|
|
||||||
describe('handleViewActivityLog', () => {
|
|
||||||
it('calls props.setView(views.bulkManagementHistory)', () => {
|
|
||||||
el.instance().handleViewActivityLog();
|
|
||||||
expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('handleClickExportGrades', () => {
|
|
||||||
const assertions = [
|
|
||||||
'calls props.downloadBulkGradesReport',
|
|
||||||
'sets location to props.gradeExportUrl',
|
|
||||||
];
|
|
||||||
it(assertions.join(' and '), () => {
|
|
||||||
el.instance().handleClickExportGrades();
|
|
||||||
expect(props.downloadBulkGradesReport).toHaveBeenCalled();
|
|
||||||
expect(window.location.assign).toHaveBeenCalledWith(props.gradeExportUrl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
let mapped;
|
|
||||||
const testState = { do: 'not', test: 'me' };
|
|
||||||
beforeEach(() => {
|
|
||||||
mapped = mapStateToProps(testState);
|
|
||||||
});
|
|
||||||
test('gradeExportUrl from root.gradeExportUrl', () => {
|
|
||||||
expect(mapped.gradeExportUrl).toEqual(selectors.root.gradeExportUrl(testState));
|
|
||||||
});
|
|
||||||
test('showBulkManagement from root.showBulkManagement', () => {
|
|
||||||
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapDispatchToProps', () => {
|
|
||||||
test('downloadBulkGradesReport from actions.grades.downloadReport.bulkGrades', () => {
|
|
||||||
expect(
|
|
||||||
mapDispatchToProps.downloadBulkGradesReport,
|
|
||||||
).toEqual(actions.grades.downloadReport.bulkGrades);
|
|
||||||
});
|
|
||||||
test('setView from actions.app.setView', () => {
|
|
||||||
expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = `
|
||||||
|
<div
|
||||||
|
className="d-flex"
|
||||||
|
>
|
||||||
|
<NetworkButton
|
||||||
|
label={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "Download Grades",
|
||||||
|
"description": "A labeled button that allows an admin user to download course grades all at once (in bulk).",
|
||||||
|
"id": "gradebook.GradesView.BulkManagementControls.bulkManagementLabel",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[MockFunction]}
|
||||||
|
/>
|
||||||
|
<ImportGradesButton />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
18
src/components/GradesView/BulkManagementControls/hooks.js
Normal file
18
src/components/GradesView/BulkManagementControls/hooks.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
export const useBulkManagementControlsData = () => {
|
||||||
|
const gradeExportUrl = selectors.root.useGradeExportUrl();
|
||||||
|
const showBulkManagement = selectors.root.useShowBulkManagement();
|
||||||
|
const downloadBulkGradesReport = actions.grades.useDownloadBulkGradesReport();
|
||||||
|
|
||||||
|
const handleClickExportGrades = () => {
|
||||||
|
downloadBulkGradesReport();
|
||||||
|
window.location.assign(gradeExportUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: showBulkManagement,
|
||||||
|
handleClickExportGrades,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export default useBulkManagementControlsData;
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import useBulkManagementControlsData from './hooks';
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
actions: {
|
||||||
|
grades: {
|
||||||
|
useDownloadBulkGradesReport: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
root: {
|
||||||
|
useGradeExportUrl: jest.fn(),
|
||||||
|
useShowBulkManagement: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const downloadBulkGrades = jest.fn();
|
||||||
|
actions.grades.useDownloadBulkGradesReport.mockReturnValue(downloadBulkGrades);
|
||||||
|
const gradeExportUrl = 'test-grade-export-url';
|
||||||
|
selectors.root.useGradeExportUrl.mockReturnValue(gradeExportUrl);
|
||||||
|
selectors.root.useShowBulkManagement.mockReturnValue(true);
|
||||||
|
|
||||||
|
let hook;
|
||||||
|
describe('useBulkManagementControlsData', () => {
|
||||||
|
const oldWindowLocation = window.location;
|
||||||
|
beforeAll(() => {
|
||||||
|
delete window.location;
|
||||||
|
window.location = Object.defineProperties(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
...Object.getOwnPropertyDescriptors(oldWindowLocation),
|
||||||
|
assign: { configurable: true, value: jest.fn() },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
beforeEach(() => {
|
||||||
|
window.location.assign.mockReset();
|
||||||
|
hook = useBulkManagementControlsData();
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
// restore `window.location` to the `jsdom` `Location` object
|
||||||
|
window.location = oldWindowLocation;
|
||||||
|
});
|
||||||
|
describe('initialization', () => {
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
expect(selectors.root.useGradeExportUrl).toHaveBeenCalledWith();
|
||||||
|
expect(selectors.root.useShowBulkManagement).toHaveBeenCalledWith();
|
||||||
|
expect(actions.grades.useDownloadBulkGradesReport).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('output', () => {
|
||||||
|
it('forwards show from showBulkManagement', () => {
|
||||||
|
expect(hook.show).toEqual(true);
|
||||||
|
selectors.root.useShowBulkManagement.mockReturnValue(false);
|
||||||
|
hook = useBulkManagementControlsData();
|
||||||
|
expect(hook.show).toEqual(false);
|
||||||
|
});
|
||||||
|
describe('handleClickExportGrades', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
hook.handleClickExportGrades();
|
||||||
|
});
|
||||||
|
it('downloads bulk grades report', () => {
|
||||||
|
expect(downloadBulkGrades).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
it('sets window location to grade export url', () => {
|
||||||
|
expect(window.location.assign).toHaveBeenCalledWith(gradeExportUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
33
src/components/GradesView/BulkManagementControls/index.jsx
Normal file
33
src/components/GradesView/BulkManagementControls/index.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import NetworkButton from 'components/NetworkButton';
|
||||||
|
import ImportGradesButton from '../ImportGradesButton';
|
||||||
|
|
||||||
|
import useBulkManagementControlsData from './hooks';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <BulkManagementControls />
|
||||||
|
* Provides download buttons for Bulk Management and Intervention reports, only if
|
||||||
|
* showBulkManagement is set in redus.
|
||||||
|
*/
|
||||||
|
export const BulkManagementControls = () => {
|
||||||
|
const {
|
||||||
|
show,
|
||||||
|
handleClickExportGrades,
|
||||||
|
} = useBulkManagementControlsData();
|
||||||
|
|
||||||
|
if (!show) { return null; }
|
||||||
|
return (
|
||||||
|
<div className="d-flex">
|
||||||
|
<NetworkButton
|
||||||
|
label={messages.downloadGradesBtn}
|
||||||
|
onClick={handleClickExportGrades}
|
||||||
|
/>
|
||||||
|
<ImportGradesButton />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BulkManagementControls;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import useBulkManagementControlsData from './hooks';
|
||||||
|
import BulkManagementControls from '.';
|
||||||
|
|
||||||
|
jest.mock('../ImportGradesButton', () => 'ImportGradesButton');
|
||||||
|
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
||||||
|
|
||||||
|
jest.mock('./hooks', () => jest.fn());
|
||||||
|
|
||||||
|
const hookProps = {
|
||||||
|
show: true,
|
||||||
|
handleClickExportGrades: jest.fn(),
|
||||||
|
};
|
||||||
|
useBulkManagementControlsData.mockReturnValue(hookProps);
|
||||||
|
|
||||||
|
describe('BulkManagementControls', () => {
|
||||||
|
describe('behavior', () => {
|
||||||
|
shallow(<BulkManagementControls />);
|
||||||
|
expect(useBulkManagementControlsData).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
describe('render', () => {
|
||||||
|
test('snapshot - show - network and import buttons', () => {
|
||||||
|
expect(shallow(<BulkManagementControls />)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
test('snapshot - empty if show is not truthy', () => {
|
||||||
|
useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false });
|
||||||
|
expect(shallow(<BulkManagementControls />).isEmptyRender()).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,68 +1,53 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
import { StrictDict } from 'utils';
|
||||||
|
import { selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import HistoryHeader from './HistoryHeader';
|
import HistoryHeader from './HistoryHeader';
|
||||||
|
|
||||||
|
export const HistoryKeys = StrictDict({
|
||||||
|
assignment: 'assignment',
|
||||||
|
student: 'student',
|
||||||
|
originalGrade: 'original-grade',
|
||||||
|
currentGrade: 'current-grade',
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <ModalHeaders />
|
* <ModalHeaders />
|
||||||
* Provides a list of HistoryHeaders for the student name, assignment,
|
* Provides a list of HistoryHeaders for the student name, assignment,
|
||||||
* original grade, and current override grade.
|
* original grade, and current override grade.
|
||||||
*/
|
*/
|
||||||
export const ModalHeaders = ({
|
export const ModalHeaders = () => {
|
||||||
modalState,
|
const { assignmentName, updateUserName } = selectors.app.useModalData();
|
||||||
originalGrade,
|
const { currentGrade, originalGrade } = selectors.grades.useGradeData();
|
||||||
currentGrade,
|
const { formatMessage } = useIntl();
|
||||||
}) => (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<HistoryHeader
|
<HistoryHeader
|
||||||
id="assignment"
|
id={HistoryKeys.assignment}
|
||||||
label={<FormattedMessage {...messages.assignmentHeader} />}
|
label={formatMessage(messages.assignmentHeader)}
|
||||||
value={modalState.assignmentName}
|
value={assignmentName}
|
||||||
/>
|
/>
|
||||||
<HistoryHeader
|
<HistoryHeader
|
||||||
id="student"
|
id={HistoryKeys.student}
|
||||||
label={<FormattedMessage {...messages.studentHeader} />}
|
label={formatMessage(messages.studentHeader)}
|
||||||
value={modalState.updateUserName}
|
value={updateUserName}
|
||||||
/>
|
/>
|
||||||
<HistoryHeader
|
<HistoryHeader
|
||||||
id="original-grade"
|
id={HistoryKeys.originalGrade}
|
||||||
label={<FormattedMessage {...messages.originalGradeHeader} />}
|
label={formatMessage(messages.originalGradeHeader)}
|
||||||
value={originalGrade}
|
value={originalGrade}
|
||||||
/>
|
/>
|
||||||
<HistoryHeader
|
<HistoryHeader
|
||||||
id="current-grade"
|
id={HistoryKeys.currentGrade}
|
||||||
label={<FormattedMessage {...messages.currentGradeHeader} />}
|
label={formatMessage(messages.currentGradeHeader)}
|
||||||
value={currentGrade}
|
value={currentGrade}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
ModalHeaders.defaultProps = {
|
|
||||||
currentGrade: null,
|
|
||||||
originalGrade: null,
|
|
||||||
};
|
|
||||||
ModalHeaders.propTypes = {
|
|
||||||
// redux
|
|
||||||
currentGrade: PropTypes.number,
|
|
||||||
originalGrade: PropTypes.number,
|
|
||||||
modalState: PropTypes.shape({
|
|
||||||
assignmentName: PropTypes.string.isRequired,
|
|
||||||
updateUserName: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
export default ModalHeaders;
|
||||||
modalState: {
|
|
||||||
assignmentName: selectors.app.modalState.assignmentName(state),
|
|
||||||
updateUserName: selectors.app.modalState.updateUserName(state),
|
|
||||||
},
|
|
||||||
currentGrade: selectors.grades.gradeOverrideCurrentEarnedGradedOverride(state),
|
|
||||||
originalGrade: selectors.grades.gradeOriginalEarnedGraded(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(ModalHeaders);
|
|
||||||
|
|||||||
@@ -1,93 +1,84 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
import {
|
import { formatMessage } from 'testUtils';
|
||||||
ModalHeaders,
|
|
||||||
mapStateToProps,
|
import HistoryHeader from './HistoryHeader';
|
||||||
} from './ModalHeaders';
|
import ModalHeaders, { HistoryKeys } from './ModalHeaders';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
jest.mock('./HistoryHeader', () => 'HistoryHeader');
|
jest.mock('./HistoryHeader', () => 'HistoryHeader');
|
||||||
|
|
||||||
jest.mock('data/selectors', () => ({
|
jest.mock('data/redux/hooks', () => ({
|
||||||
__esModule: true,
|
selectors: {
|
||||||
default: {
|
app: { useModalData: jest.fn() },
|
||||||
app: {
|
grades: { useGradeData: jest.fn() },
|
||||||
editUpdateData: jest.fn(state => ({ editUpdateData: state })),
|
|
||||||
modalState: {
|
|
||||||
assignmentName: jest.fn(state => ({ assignmentName: state })),
|
|
||||||
updateUserName: jest.fn(state => ({ updateUserName: state })),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grades: {
|
|
||||||
gradeOverrideCurrentEarnedGradedOverride: jest.fn(state => ({ currentGrade: state })),
|
|
||||||
gradeOriginalEarnedGraded: jest.fn(state => ({ originalGrade: state })),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
describe('ModalHeaders', () => {
|
|
||||||
let el;
|
|
||||||
const props = {
|
|
||||||
currentGrade: 2,
|
|
||||||
originalGrade: 20,
|
|
||||||
modalState: {
|
|
||||||
assignmentName: 'Qwerty',
|
|
||||||
updateUserName: 'Uiop',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Component', () => {
|
const modalData = {
|
||||||
describe('snapshots', () => {
|
assignmentName: 'test-assignment-name',
|
||||||
beforeEach(() => {
|
updateUserName: 'test-user-name',
|
||||||
});
|
};
|
||||||
describe('gradeOverrideHistoryError is and empty and open is true', () => {
|
selectors.app.useModalData.mockReturnValue(modalData);
|
||||||
test('modal open and StatusAlert showing', () => {
|
const gradeData = {
|
||||||
el = shallow(<ModalHeaders {...props} />);
|
currentGrade: 'test-current-grade',
|
||||||
expect(el).toMatchSnapshot();
|
originalGrade: 'test-original-grade',
|
||||||
});
|
};
|
||||||
});
|
selectors.grades.useGradeData.mockReturnValue(gradeData);
|
||||||
describe('gradeOverrideHistoryError is empty and open is false', () => {
|
|
||||||
test('modal closed and StatusAlert closed', () => {
|
let el;
|
||||||
el = shallow(
|
describe('ModalHeaders', () => {
|
||||||
<ModalHeaders {...props} open={false} gradeOverrideHistoryError="" />,
|
beforeEach(() => {
|
||||||
);
|
jest.clearAllMocks();
|
||||||
expect(el).toMatchSnapshot();
|
el = shallow(<ModalHeaders />);
|
||||||
});
|
});
|
||||||
});
|
describe('behavior', () => {
|
||||||
|
it('initializes intl', () => {
|
||||||
|
expect(useIntl).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||||
|
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('render', () => {
|
||||||
describe('mapStateToProps', () => {
|
test('snapshot', () => {
|
||||||
const testState = { he: 'lives in a', pineapple: 'under the sea' };
|
expect(el).toMatchSnapshot();
|
||||||
let mapped;
|
|
||||||
beforeEach(() => {
|
|
||||||
mapped = mapStateToProps(testState);
|
|
||||||
});
|
});
|
||||||
describe('modalState', () => {
|
test('assignment header', () => {
|
||||||
test('assignmentName from app.modalState.assignmentName', () => {
|
const headerProps = el.find(HistoryHeader).at(0).props();
|
||||||
expect(
|
expect(headerProps).toMatchObject({
|
||||||
mapped.modalState.assignmentName,
|
id: HistoryKeys.assignment,
|
||||||
).toEqual(selectors.app.modalState.assignmentName(testState));
|
label: formatMessage(messages.assignmentHeader),
|
||||||
});
|
value: modalData.assignmentName,
|
||||||
test('updateUserName from app.modalState.updateUserName', () => {
|
|
||||||
expect(
|
|
||||||
mapped.modalState.updateUserName,
|
|
||||||
).toEqual(selectors.app.modalState.updateUserName(testState));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('originalGrade', () => {
|
test('student header', () => {
|
||||||
test('from grades.gradeOverrideCurrentEarnedGradedOverride', () => {
|
const headerProps = el.find(HistoryHeader).at(1).props();
|
||||||
expect(mapped.currentGrade).toEqual(
|
expect(headerProps).toMatchObject({
|
||||||
selectors.grades.gradeOverrideCurrentEarnedGradedOverride(testState),
|
id: HistoryKeys.student,
|
||||||
);
|
label: formatMessage(messages.studentHeader),
|
||||||
|
value: modalData.updateUserName,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('originalGrade', () => {
|
test('originalGrade header', () => {
|
||||||
test('from grades.gradeOriginalEarnedGrades', () => {
|
const headerProps = el.find(HistoryHeader).at(2).props();
|
||||||
expect(mapped.originalGrade).toEqual(
|
expect(headerProps).toMatchObject({
|
||||||
selectors.grades.gradeOriginalEarnedGraded(testState),
|
id: HistoryKeys.originalGrade,
|
||||||
);
|
label: formatMessage(messages.originalGradeHeader),
|
||||||
|
value: gradeData.originalGrade,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('currentGrade header', () => {
|
||||||
|
const headerProps = el.find(HistoryHeader).at(3).props();
|
||||||
|
expect(headerProps).toMatchObject({
|
||||||
|
id: HistoryKeys.currentGrade,
|
||||||
|
label: formatMessage(messages.currentGradeHeader),
|
||||||
|
value: gradeData.currentGrade,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { Form } from '@edx/paragon';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import { getLocalizedSlash } from 'i18n/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <AdjustedGradeInput />
|
|
||||||
* Input control for adjusting the grade of a unit
|
|
||||||
* displays an "/ ${possibleGrade} if there is one in the data model.
|
|
||||||
*/
|
|
||||||
export class AdjustedGradeInput extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.onChange = this.onChange.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = ({ target }) => {
|
|
||||||
this.props.setModalState({ adjustedGradeValue: target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<Form.Control
|
|
||||||
type="text"
|
|
||||||
name="adjustedGradeValue"
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
{this.props.possibleGrade && ` ${getLocalizedSlash()} ${this.props.possibleGrade}`}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AdjustedGradeInput.defaultProps = {
|
|
||||||
possibleGrade: null,
|
|
||||||
};
|
|
||||||
AdjustedGradeInput.propTypes = {
|
|
||||||
value: PropTypes.oneOfType([
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
]).isRequired,
|
|
||||||
possibleGrade: PropTypes.oneOfType([
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
]),
|
|
||||||
setModalState: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
|
||||||
possibleGrade: selectors.root.editModalPossibleGrade(state),
|
|
||||||
value: selectors.app.modalState.adjustedGradeValue(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mapDispatchToProps = {
|
|
||||||
setModalState: actions.app.setModalState,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(AdjustedGradeInput);
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AdjustedGradeInput,
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
} from './AdjustedGradeInput';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({
|
|
||||||
Form: { Control: () => 'Form.Control' },
|
|
||||||
}));
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
root: {
|
|
||||||
editModalPossibleGrade: jest.fn(state => ({ updateUserName: state })),
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
modalState: { adjustedGradeValue: jest.fn(state => ({ adjustedGradeValue: state })) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('data/actions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: { setModalState: jest.fn() },
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
describe('AdjustedGradeInput', () => {
|
|
||||||
let el;
|
|
||||||
let props = {
|
|
||||||
value: 1,
|
|
||||||
possibleGrade: 5,
|
|
||||||
};
|
|
||||||
beforeEach(() => {
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
setModalState: jest.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
describe('Component', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<AdjustedGradeInput {...props} />);
|
|
||||||
});
|
|
||||||
describe('snapshots', () => {
|
|
||||||
test('displays input control and "out of possible grade" label', () => {
|
|
||||||
el.instance().onChange = jest.fn().mockName('this.onChange');
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('behavior', () => {
|
|
||||||
describe('onChange', () => {
|
|
||||||
it('calls props.setModalState event target value', () => {
|
|
||||||
const value = 42;
|
|
||||||
el.instance().onChange({ target: { value } });
|
|
||||||
expect(props.setModalState).toHaveBeenCalledWith({
|
|
||||||
adjustedGradeValue: value,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
const testState = { like: 'no one', ever: 'was' };
|
|
||||||
let mapped;
|
|
||||||
beforeEach(() => {
|
|
||||||
mapped = mapStateToProps(testState);
|
|
||||||
});
|
|
||||||
describe('modalState', () => {
|
|
||||||
test('possibleGrade from root.editModalPossibleGrade', () => {
|
|
||||||
expect(
|
|
||||||
mapped.possibleGrade,
|
|
||||||
).toEqual(selectors.root.editModalPossibleGrade(testState));
|
|
||||||
});
|
|
||||||
test('updateUserName from app.modalState.updateUserName', () => {
|
|
||||||
expect(
|
|
||||||
mapped.value,
|
|
||||||
).toEqual(selectors.app.modalState.adjustedGradeValue(testState));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapDispatchToProps', () => {
|
|
||||||
test('setModalState from actions.app.setModalState', () => {
|
|
||||||
expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`AdjustedGradeInput component render snapshot 1`] = `
|
||||||
|
<span>
|
||||||
|
<Form.Control
|
||||||
|
name="adjustedGradeValue"
|
||||||
|
onChange={[MockFunction hook.onChange]}
|
||||||
|
type="text"
|
||||||
|
value="test-value"
|
||||||
|
/>
|
||||||
|
some-hint-text
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
import { getLocalizedSlash } from 'i18n/utils';
|
||||||
|
|
||||||
|
const useAdjustedGradeInputData = () => {
|
||||||
|
const possibleGrade = selectors.root.useEditModalPossibleGrade();
|
||||||
|
const value = selectors.app.useModalData().adjustedGradeValue;
|
||||||
|
const setModalState = actions.app.useSetModalState();
|
||||||
|
const hintText = possibleGrade && ` ${getLocalizedSlash()} ${possibleGrade}`;
|
||||||
|
|
||||||
|
const onChange = ({ target }) => {
|
||||||
|
setModalState({ adjustedGradeValue: target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
hintText,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAdjustedGradeInputData;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { getLocalizedSlash } from 'i18n/utils';
|
||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
import useAdjustedGradeInputData from './hooks';
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
selectors: {
|
||||||
|
root: {
|
||||||
|
useEditModalPossibleGrade: jest.fn(),
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
useModalData: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
app: {
|
||||||
|
useSetModalState: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
jest.mock('i18n/utils', () => ({ getLocalizedSlash: jest.fn() }));
|
||||||
|
|
||||||
|
const localizedSlash = 'localized-slash';
|
||||||
|
getLocalizedSlash.mockReturnValue(localizedSlash);
|
||||||
|
|
||||||
|
const possibleGrade = 105;
|
||||||
|
selectors.root.useEditModalPossibleGrade.mockReturnValue(possibleGrade);
|
||||||
|
const modalData = { adjustedGradeValue: 70 };
|
||||||
|
const setModalState = jest.fn();
|
||||||
|
selectors.app.useModalData.mockReturnValue(modalData);
|
||||||
|
actions.app.useSetModalState.mockReturnValue(setModalState);
|
||||||
|
|
||||||
|
let out;
|
||||||
|
describe('useAdjustedGradeInputData hook', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
out = useAdjustedGradeInputData();
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
expect(selectors.root.useEditModalPossibleGrade).toHaveBeenCalled();
|
||||||
|
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||||
|
expect(actions.app.useSetModalState).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('output', () => {
|
||||||
|
it('forwards adjusted grade value as value from modal data', () => {
|
||||||
|
expect(out.value).toEqual(modalData.adjustedGradeValue);
|
||||||
|
});
|
||||||
|
describe('hintText', () => {
|
||||||
|
it('passes an undefined value if possibleGrade is not available', () => {
|
||||||
|
selectors.root.useEditModalPossibleGrade.mockReturnValueOnce(undefined);
|
||||||
|
out = useAdjustedGradeInputData();
|
||||||
|
expect(out.hintText).toEqual(undefined);
|
||||||
|
});
|
||||||
|
it('passes localized slash and possible grade if available', () => {
|
||||||
|
expect(out.hintText).toEqual(` ${localizedSlash} ${possibleGrade}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('onChange', () => {
|
||||||
|
it('sets modal state with event target value', () => {
|
||||||
|
const testValue = 'test-value';
|
||||||
|
out.onChange({ target: { value: testValue } });
|
||||||
|
expect(setModalState).toHaveBeenCalledWith({ adjustedGradeValue: testValue });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Form } from '@edx/paragon';
|
||||||
|
|
||||||
|
import useAdjustedGradeInputData from './hooks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <AdjustedGradeInput />
|
||||||
|
* Input control for adjusting the grade of a unit
|
||||||
|
* displays an "/ ${possibleGrade} if there is one in the data model.
|
||||||
|
*/
|
||||||
|
export const AdjustedGradeInput = () => {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
hintText,
|
||||||
|
} = useAdjustedGradeInputData();
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
name="adjustedGradeValue"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
{hintText}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AdjustedGradeInput.propTypes = {};
|
||||||
|
|
||||||
|
export default AdjustedGradeInput;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { Form } from '@edx/paragon';
|
||||||
|
|
||||||
|
import useAdjustedGradeInputData from './hooks';
|
||||||
|
import AdjustedGradeInput from '.';
|
||||||
|
|
||||||
|
jest.mock('./hooks', () => jest.fn());
|
||||||
|
|
||||||
|
const hookProps = {
|
||||||
|
hintText: 'some-hint-text',
|
||||||
|
onChange: jest.fn().mockName('hook.onChange'),
|
||||||
|
value: 'test-value',
|
||||||
|
};
|
||||||
|
useAdjustedGradeInputData.mockReturnValue(hookProps);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('AdjustedGradeInput component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
el = shallow(<AdjustedGradeInput />);
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes hook data', () => {
|
||||||
|
expect(useAdjustedGradeInputData).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('render', () => {
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
const control = el.find(Form.Control);
|
||||||
|
expect(control.props().value).toEqual(hookProps.value);
|
||||||
|
expect(control.props().onChange).toEqual(hookProps.onChange);
|
||||||
|
expect(el.contains(hookProps.hintText)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { Form } from '@edx/paragon';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import actions from 'data/actions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <ReasonInput />
|
|
||||||
* Input control for the "reason for change" field in the Edit modal.
|
|
||||||
*/
|
|
||||||
export class ReasonInput extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.ref = React.createRef();
|
|
||||||
this.onChange = this.onChange.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.ref.current.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = (event) => {
|
|
||||||
this.props.setModalState({ reasonForChange: event.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Form.Control
|
|
||||||
type="text"
|
|
||||||
name="reasonForChange"
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.onChange}
|
|
||||||
ref={this.ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ReasonInput.propTypes = {
|
|
||||||
// redux
|
|
||||||
setModalState: PropTypes.func.isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
|
||||||
value: selectors.app.modalState.reasonForChange(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mapDispatchToProps = {
|
|
||||||
setModalState: actions.app.setModalState,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ReasonInput);
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ReasonInput,
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
} from './ReasonInput';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({
|
|
||||||
Form: { Control: () => 'Form.Control' },
|
|
||||||
}));
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: {
|
|
||||||
modalState: { reasonForChange: jest.fn(state => ({ reasonForChange: state })) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('data/actions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: { setModalState: jest.fn() },
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
describe('ReasonInput', () => {
|
|
||||||
let el;
|
|
||||||
let props = {
|
|
||||||
value: 'did not answer the question',
|
|
||||||
};
|
|
||||||
beforeEach(() => {
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
setModalState: jest.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
describe('Component', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<ReasonInput {...props} />, { disableLifecycleMethods: true });
|
|
||||||
});
|
|
||||||
describe('snapshots', () => {
|
|
||||||
test('displays reason for change input control', () => {
|
|
||||||
el.instance().onChange = jest.fn().mockName('this.onChange');
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('behavior', () => {
|
|
||||||
describe('onChange', () => {
|
|
||||||
it('calls props.setModalState event target value', () => {
|
|
||||||
const value = 42;
|
|
||||||
el.instance().onChange({ target: { value } });
|
|
||||||
expect(props.setModalState).toHaveBeenCalledWith({
|
|
||||||
reasonForChange: value,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('componentDidMount', () => {
|
|
||||||
it('focuses the input ref', () => {
|
|
||||||
const focus = jest.fn();
|
|
||||||
expect(el.instance().ref).toEqual({ current: null });
|
|
||||||
el.instance().ref.current = { focus };
|
|
||||||
el.instance().componentDidMount();
|
|
||||||
expect(el.instance().ref.current.focus).toHaveBeenCalledWith();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
const testState = { to: { catchThem: 'my real test', trainThem: 'my cause!' } };
|
|
||||||
let mapped;
|
|
||||||
beforeEach(() => {
|
|
||||||
mapped = mapStateToProps(testState);
|
|
||||||
});
|
|
||||||
describe('modalState', () => {
|
|
||||||
test('value from app.modalState.reasonForChange', () => {
|
|
||||||
expect(mapped.value).toEqual(selectors.app.modalState.reasonForChange(testState));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapDispatchToProps', () => {
|
|
||||||
test('setModalState from actions.app.setModalState', () => {
|
|
||||||
expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ReasonInput component render snapshot 1`] = `
|
||||||
|
<Form.Control
|
||||||
|
data-testid="reason-input-control"
|
||||||
|
name="reasonForChange"
|
||||||
|
onChange={[MockFunction hook.onChange]}
|
||||||
|
type="text"
|
||||||
|
value="test-value"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
const useReasonInputData = () => {
|
||||||
|
const ref = React.useRef();
|
||||||
|
const { reasonForChange } = selectors.app.useModalData();
|
||||||
|
const setModalState = actions.app.useSetModalState();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
ref.current.focus();
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
const onChange = (event) => {
|
||||||
|
setModalState({ reasonForChange: event.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: reasonForChange,
|
||||||
|
onChange,
|
||||||
|
ref,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useReasonInputData;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
import useReasonInputData from './hooks';
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
selectors: {
|
||||||
|
app: {
|
||||||
|
useModalData: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
app: {
|
||||||
|
useSetModalState: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const modalData = { reasonForChange: 'test-reason-for-change' };
|
||||||
|
const setModalState = jest.fn();
|
||||||
|
selectors.app.useModalData.mockReturnValue(modalData);
|
||||||
|
actions.app.useSetModalState.mockReturnValue(setModalState);
|
||||||
|
|
||||||
|
const ref = { current: { focus: jest.fn() }, useRef: true };
|
||||||
|
React.useRef.mockReturnValue(ref);
|
||||||
|
|
||||||
|
let out;
|
||||||
|
describe('useReasonInputData hook', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
out = useReasonInputData();
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes ref', () => {
|
||||||
|
expect(React.useRef).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||||
|
expect(actions.app.useSetModalState).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('focuses ref on load', () => {
|
||||||
|
const [[cb, prereqs]] = React.useEffect.mock.calls;
|
||||||
|
expect(prereqs).toEqual([ref]);
|
||||||
|
cb();
|
||||||
|
expect(ref.current.focus).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('output', () => {
|
||||||
|
it('forwards reasonForChange as value from modal data', () => {
|
||||||
|
expect(out.value).toEqual(modalData.reasonForChange);
|
||||||
|
});
|
||||||
|
it('forwards ref', () => {
|
||||||
|
expect(out.ref).toEqual(ref);
|
||||||
|
});
|
||||||
|
describe('onChange', () => {
|
||||||
|
it('sets modal state with event target value', () => {
|
||||||
|
const testValue = 'test-value';
|
||||||
|
out.onChange({ target: { value: testValue } });
|
||||||
|
expect(setModalState).toHaveBeenCalledWith({ reasonForChange: testValue });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Form } from '@edx/paragon';
|
||||||
|
|
||||||
|
import useReasonInputData from './hooks';
|
||||||
|
|
||||||
|
export const controlTestId = 'reason-input-control';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <ReasonInput />
|
||||||
|
* Input control for the "reason for change" field in the Edit modal.
|
||||||
|
*/
|
||||||
|
export const ReasonInput = () => {
|
||||||
|
const { ref, value, onChange } = useReasonInputData();
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
name="reasonForChange"
|
||||||
|
data-testid={controlTestId}
|
||||||
|
{...{ value, onChange, ref }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ReasonInput.propTypes = {};
|
||||||
|
|
||||||
|
export default ReasonInput;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { Form } from '@edx/paragon';
|
||||||
|
|
||||||
|
import useReasonInputData from './hooks';
|
||||||
|
import ReasonInput from '.';
|
||||||
|
|
||||||
|
jest.mock('./hooks', () => jest.fn());
|
||||||
|
|
||||||
|
const hookProps = {
|
||||||
|
ref: 'reason-input-ref',
|
||||||
|
onChange: jest.fn().mockName('hook.onChange'),
|
||||||
|
value: 'test-value',
|
||||||
|
};
|
||||||
|
useReasonInputData.mockReturnValue(hookProps);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('ReasonInput component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
el = shallow(<ReasonInput />);
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes hook data', () => {
|
||||||
|
expect(useReasonInputData).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('render', () => {
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
const control = el.find(Form.Control);
|
||||||
|
expect(control.props().value).toEqual(hookProps.value);
|
||||||
|
expect(control.props().onChange).toEqual(hookProps.onChange);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
|
import useReasonInputData from './hooks';
|
||||||
|
import ReasonInput, { controlTestId } from '.';
|
||||||
|
|
||||||
|
jest.unmock('react');
|
||||||
|
jest.unmock('@edx/paragon');
|
||||||
|
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
|
||||||
|
|
||||||
|
const focus = jest.fn();
|
||||||
|
const props = {
|
||||||
|
value: 'test-value',
|
||||||
|
onChange: jest.fn(),
|
||||||
|
ref: { current: { focus }, useRef: jest.fn() },
|
||||||
|
};
|
||||||
|
useReasonInputData.mockReturnValue(props);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('ReasonInput ref', () => {
|
||||||
|
it('loads ref from hook', () => {
|
||||||
|
el = render(<ReasonInput />);
|
||||||
|
const control = el.getByTestId(controlTestId);
|
||||||
|
expect(control).toEqual(props.ref.current);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`AdjustedGradeInput Component snapshots displays input control and "out of possible grade" label 1`] = `
|
|
||||||
<span>
|
|
||||||
<Control
|
|
||||||
name="adjustedGradeValue"
|
|
||||||
onChange={[MockFunction this.onChange]}
|
|
||||||
type="text"
|
|
||||||
value={1}
|
|
||||||
/>
|
|
||||||
/ 5
|
|
||||||
</span>
|
|
||||||
`;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`ReasonInput Component snapshots displays reason for change input control 1`] = `
|
|
||||||
<Control
|
|
||||||
name="reasonForChange"
|
|
||||||
onChange={[MockFunction this.onChange]}
|
|
||||||
type="text"
|
|
||||||
value="did not answer the question"
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`OverrideTable component render snapshot 1`] = `
|
||||||
|
<DataTable
|
||||||
|
columns="test-columns"
|
||||||
|
data={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"test": "data",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"andOther": "test-data",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"adjustedGrade": <AdjustedGradeInput />,
|
||||||
|
"date": Object {
|
||||||
|
"formatted": 2000-01-01T00:00:00.000Z,
|
||||||
|
},
|
||||||
|
"reason": <ReasonInput />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
itemCount={2}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = `
|
|
||||||
<DataTable
|
|
||||||
columns={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"Header": <FormattedMessage
|
|
||||||
defaultMessage="Date"
|
|
||||||
description="Edit Modal Override Table Date column header"
|
|
||||||
id="gradebook.GradesView.EditModal.Overrides.dateHeader"
|
|
||||||
/>,
|
|
||||||
"accessor": "date",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"Header": <FormattedMessage
|
|
||||||
defaultMessage="Grader"
|
|
||||||
description="Edit Modal Override Table Grader column header"
|
|
||||||
id="gradebook.GradesView.EditModal.Overrides.graderHeader"
|
|
||||||
/>,
|
|
||||||
"accessor": "grader",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"Header": <FormattedMessage
|
|
||||||
defaultMessage="Reason"
|
|
||||||
description="Edit Modal Override Table Reason column header"
|
|
||||||
id="gradebook.GradesView.EditModal.Overrides.reasonHeader"
|
|
||||||
/>,
|
|
||||||
"accessor": "reason",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"Header": <FormattedMessage
|
|
||||||
defaultMessage="Adjusted grade"
|
|
||||||
description="Edit Modal Override Table Adjusted grade column header"
|
|
||||||
id="gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader"
|
|
||||||
/>,
|
|
||||||
"accessor": "adjustedGrade",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
data={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"adjustedGrade": 0,
|
|
||||||
"date": "yesterday",
|
|
||||||
"grader": "me",
|
|
||||||
"reason": "you ate my sandwich",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"adjustedGrade": 20,
|
|
||||||
"date": "today",
|
|
||||||
"grader": "me",
|
|
||||||
"reason": "you brought me a new sandwich",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"adjustedGrade": <AdjustedGradeInput />,
|
|
||||||
"date": "todaaaaaay",
|
|
||||||
"reason": <ReasonInput />,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
itemCount={2}
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
26
src/components/GradesView/EditModal/OverrideTable/hooks.js
Normal file
26
src/components/GradesView/EditModal/OverrideTable/hooks.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||||
|
import { selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const useOverrideTableData = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const hide = selectors.grades.useHasOverrideErrors();
|
||||||
|
const gradeOverrides = selectors.grades.useGradeData().gradeOverrideHistoryResults;
|
||||||
|
const tableProps = {};
|
||||||
|
if (!hide) {
|
||||||
|
tableProps.columns = [
|
||||||
|
{ Header: formatMessage(messages.dateHeader), accessor: columns.date },
|
||||||
|
{ Header: formatMessage(messages.graderHeader), accessor: columns.grader },
|
||||||
|
{ Header: formatMessage(messages.reasonHeader), accessor: columns.reason },
|
||||||
|
{ Header: formatMessage(messages.adjustedGradeHeader), accessor: columns.adjustedGrade },
|
||||||
|
];
|
||||||
|
tableProps.data = gradeOverrides;
|
||||||
|
}
|
||||||
|
return { hide, ...tableProps };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useOverrideTableData;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { formatMessage } from 'testUtils';
|
||||||
|
|
||||||
|
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||||
|
import { selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import useOverrideTableData from './hooks';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
selectors: {
|
||||||
|
grades: {
|
||||||
|
useHasOverrideErrors: jest.fn(),
|
||||||
|
useGradeData: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
selectors.grades.useHasOverrideErrors.mockReturnValue(false);
|
||||||
|
const gradeOverrides = ['some', 'override', 'data'];
|
||||||
|
const gradeData = { gradeOverrideHistoryResults: gradeOverrides };
|
||||||
|
selectors.grades.useGradeData.mockReturnValue(gradeData);
|
||||||
|
|
||||||
|
let out;
|
||||||
|
describe('useOverrideTableData', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
out = useOverrideTableData();
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes intl hook', () => {
|
||||||
|
expect(useIntl).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
expect(selectors.grades.useHasOverrideErrors).toHaveBeenCalled();
|
||||||
|
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('output', () => {
|
||||||
|
describe('no errors', () => {
|
||||||
|
test('hide is false', () => {
|
||||||
|
expect(out.hide).toEqual(false);
|
||||||
|
});
|
||||||
|
describe('columns', () => {
|
||||||
|
test('date column', () => {
|
||||||
|
const { Header, accessor } = out.columns[0];
|
||||||
|
expect(Header).toEqual(formatMessage(messages.dateHeader));
|
||||||
|
expect(accessor).toEqual(columns.date);
|
||||||
|
});
|
||||||
|
test('grader column', () => {
|
||||||
|
const { Header, accessor } = out.columns[1];
|
||||||
|
expect(Header).toEqual(formatMessage(messages.graderHeader));
|
||||||
|
expect(accessor).toEqual(columns.grader);
|
||||||
|
});
|
||||||
|
test('reason column', () => {
|
||||||
|
const { Header, accessor } = out.columns[2];
|
||||||
|
expect(Header).toEqual(formatMessage(messages.reasonHeader));
|
||||||
|
expect(accessor).toEqual(columns.reason);
|
||||||
|
});
|
||||||
|
test('adjustedGrade column', () => {
|
||||||
|
const { Header, accessor } = out.columns[3];
|
||||||
|
expect(Header).toEqual(formatMessage(messages.adjustedGradeHeader));
|
||||||
|
expect(accessor).toEqual(columns.adjustedGrade);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('data passed from grade data', () => {
|
||||||
|
expect(out.data).toEqual(gradeOverrides);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('with errors', () => {
|
||||||
|
it('returns hide true and no other fields', () => {
|
||||||
|
selectors.grades.useHasOverrideErrors.mockReturnValue(true);
|
||||||
|
out = useOverrideTableData();
|
||||||
|
expect(out).toEqual({ hide: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,73 +1,40 @@
|
|||||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { DataTable } from '@edx/paragon';
|
import { DataTable } from '@edx/paragon';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
import { formatDateForDisplay } from 'utils';
|
||||||
import selectors from 'data/selectors';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import ReasonInput from './ReasonInput';
|
import ReasonInput from './ReasonInput';
|
||||||
import AdjustedGradeInput from './AdjustedGradeInput';
|
import AdjustedGradeInput from './AdjustedGradeInput';
|
||||||
|
import useOverrideTableData from './hooks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <OverrideTable />
|
* <OverrideTable />
|
||||||
* Table containing previous grade override entries, and an "edit" row
|
* Table containing previous grade override entries, and an "edit" row
|
||||||
* with todays date, an AdjustedGradeInput and a ReasonInput
|
* with todays date, an AdjustedGradeInput and a ReasonInput
|
||||||
*/
|
*/
|
||||||
export const OverrideTable = ({
|
|
||||||
hide,
|
export const OverrideTable = () => {
|
||||||
gradeOverrides,
|
const { hide, columns, data } = useOverrideTableData();
|
||||||
todaysDate,
|
|
||||||
}) => {
|
if (hide) { return null; }
|
||||||
if (hide) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={[
|
columns={columns}
|
||||||
{ Header: <FormattedMessage {...messages.dateHeader} />, accessor: columns.date },
|
|
||||||
{ Header: <FormattedMessage {...messages.graderHeader} />, accessor: columns.grader },
|
|
||||||
{ Header: <FormattedMessage {...messages.reasonHeader} />, accessor: columns.reason },
|
|
||||||
{
|
|
||||||
Header: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
|
||||||
accessor: columns.adjustedGrade,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
data={[
|
data={[
|
||||||
...gradeOverrides,
|
...data,
|
||||||
{
|
{
|
||||||
adjustedGrade: <AdjustedGradeInput />,
|
adjustedGrade: <AdjustedGradeInput />,
|
||||||
date: todaysDate,
|
date: formatDateForDisplay(new Date()),
|
||||||
reason: <ReasonInput />,
|
reason: <ReasonInput />,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
itemCount={gradeOverrides.length}
|
itemCount={data.length}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
OverrideTable.defaultProps = {
|
OverrideTable.propTypes = {};
|
||||||
gradeOverrides: [],
|
|
||||||
};
|
|
||||||
OverrideTable.propTypes = {
|
|
||||||
// redux
|
|
||||||
gradeOverrides: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
date: PropTypes.string,
|
|
||||||
grader: PropTypes.string,
|
|
||||||
reason: PropTypes.string,
|
|
||||||
adjustedGrade: PropTypes.number,
|
|
||||||
})),
|
|
||||||
hide: PropTypes.bool.isRequired,
|
|
||||||
todaysDate: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
export default OverrideTable;
|
||||||
hide: selectors.grades.hasOverrideErrors(state),
|
|
||||||
gradeOverrides: selectors.grades.gradeOverrides(state),
|
|
||||||
todaysDate: selectors.app.modalState.todaysDate(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(OverrideTable);
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { DataTable } from '@edx/paragon';
|
||||||
|
|
||||||
|
import { formatDateForDisplay } from 'utils';
|
||||||
|
|
||||||
|
import AdjustedGradeInput from './AdjustedGradeInput';
|
||||||
|
import ReasonInput from './ReasonInput';
|
||||||
|
import useOverrideTableData from './hooks';
|
||||||
|
import OverrideTable from '.';
|
||||||
|
|
||||||
|
jest.mock('utils', () => ({
|
||||||
|
formatDateForDisplay: (date) => ({ formatted: date }),
|
||||||
|
}));
|
||||||
|
jest.mock('./hooks', () => jest.fn());
|
||||||
|
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
|
||||||
|
jest.mock('./ReasonInput', () => 'ReasonInput');
|
||||||
|
|
||||||
|
const hookProps = {
|
||||||
|
hide: false,
|
||||||
|
data: [
|
||||||
|
{ test: 'data' },
|
||||||
|
{ andOther: 'test-data' },
|
||||||
|
],
|
||||||
|
columns: 'test-columns',
|
||||||
|
};
|
||||||
|
useOverrideTableData.mockReturnValue(hookProps);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('OverrideTable component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest
|
||||||
|
.clearAllMocks()
|
||||||
|
.useFakeTimers('modern')
|
||||||
|
.setSystemTime(new Date('2000-01-01').getTime());
|
||||||
|
el = shallow(<OverrideTable />);
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes hook data', () => {
|
||||||
|
expect(useOverrideTableData).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('render', () => {
|
||||||
|
test('null render if hide', () => {
|
||||||
|
useOverrideTableData.mockReturnValueOnce({ ...hookProps, hide: true });
|
||||||
|
el = shallow(<OverrideTable />);
|
||||||
|
expect(el.isEmptyRender()).toEqual(true);
|
||||||
|
});
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
const table = el.find(DataTable);
|
||||||
|
expect(table.props().columns).toEqual(hookProps.columns);
|
||||||
|
const data = [...table.props().data];
|
||||||
|
const inputRow = data.pop();
|
||||||
|
const formattedDate = formatDateForDisplay(new Date());
|
||||||
|
expect(data).toEqual(hookProps.data);
|
||||||
|
expect(inputRow).toMatchObject({
|
||||||
|
adjustedGrade: <AdjustedGradeInput />,
|
||||||
|
date: formattedDate,
|
||||||
|
reason: <ReasonInput />,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
|
|
||||||
import {
|
|
||||||
OverrideTable,
|
|
||||||
mapStateToProps,
|
|
||||||
} from '.';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
|
||||||
jest.mock('./ReasonInput', () => 'ReasonInput');
|
|
||||||
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
|
|
||||||
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: {
|
|
||||||
modalState: {
|
|
||||||
todaysDate: jest.fn(state => ({ todaysDate: state })),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grades: {
|
|
||||||
hasOverrideErrors: jest.fn(state => ({ hasOverrideErrors: state })),
|
|
||||||
gradeOverrides: jest.fn(state => ({ gradeOverrides: state })),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('OverrideTable', () => {
|
|
||||||
const props = {
|
|
||||||
gradeOverrides: [
|
|
||||||
{
|
|
||||||
date: 'yesterday',
|
|
||||||
grader: 'me',
|
|
||||||
reason: 'you ate my sandwich',
|
|
||||||
adjustedGrade: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: 'today',
|
|
||||||
grader: 'me',
|
|
||||||
reason: 'you brought me a new sandwich',
|
|
||||||
adjustedGrade: 20,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hide: false,
|
|
||||||
todaysDate: 'todaaaaaay',
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Component', () => {
|
|
||||||
describe('snapshots', () => {
|
|
||||||
it('returns null if hide is true', () => {
|
|
||||||
expect(shallow(<OverrideTable {...props} hide />)).toEqual({});
|
|
||||||
});
|
|
||||||
describe('basic snapshot', () => {
|
|
||||||
test('shows a row for each entry and one editable row', () => {
|
|
||||||
expect(shallow(<OverrideTable {...props} />)).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
const testState = { I: 'wanna', be: 'the', very: 'best' };
|
|
||||||
let mapped;
|
|
||||||
beforeEach(() => {
|
|
||||||
mapped = mapStateToProps(testState);
|
|
||||||
});
|
|
||||||
describe('modalState', () => {
|
|
||||||
test('hide from grades.hasOverrideErrors', () => {
|
|
||||||
expect(mapped.hide).toEqual(selectors.grades.hasOverrideErrors(testState));
|
|
||||||
});
|
|
||||||
test('gradeOverrides from grades.gradeOverrides', () => {
|
|
||||||
expect(mapped.gradeOverrides).toEqual(selectors.grades.gradeOverrides(testState));
|
|
||||||
});
|
|
||||||
test('todaysData from app.modalState.todaysDate', () => {
|
|
||||||
expect(mapped.todaysDate).toEqual(selectors.app.modalState.todaysDate(testState));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,99 +1,26 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
exports[`ModalHeaders render snapshot 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<HistoryHeader
|
<HistoryHeader
|
||||||
id="assignment"
|
id="assignment"
|
||||||
label={
|
label="Assignment"
|
||||||
<FormattedMessage
|
value="test-assignment-name"
|
||||||
defaultMessage="Assignment"
|
|
||||||
description="Edit Modal Assignment header"
|
|
||||||
id="gradebook.GradesView.EditModal.headers.assignment"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value="Qwerty"
|
|
||||||
/>
|
/>
|
||||||
<HistoryHeader
|
<HistoryHeader
|
||||||
id="student"
|
id="student"
|
||||||
label={
|
label="Student"
|
||||||
<FormattedMessage
|
value="test-user-name"
|
||||||
defaultMessage="Student"
|
|
||||||
description="Edit Modal Student header"
|
|
||||||
id="gradebook.GradesView.EditModal.headers.student"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value="Uiop"
|
|
||||||
/>
|
/>
|
||||||
<HistoryHeader
|
<HistoryHeader
|
||||||
id="original-grade"
|
id="original-grade"
|
||||||
label={
|
label="Original Grade"
|
||||||
<FormattedMessage
|
value="test-original-grade"
|
||||||
defaultMessage="Original Grade"
|
|
||||||
description="Edit Modal Original Grade header"
|
|
||||||
id="gradebook.GradesView.EditModal.headers.originalGrade"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value={20}
|
|
||||||
/>
|
/>
|
||||||
<HistoryHeader
|
<HistoryHeader
|
||||||
id="current-grade"
|
id="current-grade"
|
||||||
label={
|
label="Current Grade"
|
||||||
<FormattedMessage
|
value="test-current-grade"
|
||||||
defaultMessage="Current Grade"
|
|
||||||
description="Edit Modal Current Grade header"
|
|
||||||
id="gradebook.GradesView.EditModal.headers.currentGrade"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
|
||||||
<div>
|
|
||||||
<HistoryHeader
|
|
||||||
id="assignment"
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Assignment"
|
|
||||||
description="Edit Modal Assignment header"
|
|
||||||
id="gradebook.GradesView.EditModal.headers.assignment"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value="Qwerty"
|
|
||||||
/>
|
|
||||||
<HistoryHeader
|
|
||||||
id="student"
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Student"
|
|
||||||
description="Edit Modal Student header"
|
|
||||||
id="gradebook.GradesView.EditModal.headers.student"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value="Uiop"
|
|
||||||
/>
|
|
||||||
<HistoryHeader
|
|
||||||
id="original-grade"
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Original Grade"
|
|
||||||
description="Edit Modal Original Grade header"
|
|
||||||
id="gradebook.GradesView.EditModal.headers.originalGrade"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value={20}
|
|
||||||
/>
|
|
||||||
<HistoryHeader
|
|
||||||
id="current-grade"
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Current Grade"
|
|
||||||
description="Edit Modal Current Grade header"
|
|
||||||
id="gradebook.GradesView.EditModal.headers.currentGrade"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
value={2}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`EditModal component render with error snapshot 1`] = `
|
||||||
|
<ModalDialog
|
||||||
|
hasCloseButton={true}
|
||||||
|
isFullscreenOnMobile={true}
|
||||||
|
isOpen="test-is-open"
|
||||||
|
onClose={[MockFunction hooks.onClose]}
|
||||||
|
size="xl"
|
||||||
|
title="Edit Grades"
|
||||||
|
>
|
||||||
|
<ModalDialog.Body>
|
||||||
|
<div>
|
||||||
|
<ModalHeaders />
|
||||||
|
<Alert
|
||||||
|
dismissible={false}
|
||||||
|
show={true}
|
||||||
|
variant="danger"
|
||||||
|
>
|
||||||
|
test-error
|
||||||
|
</Alert>
|
||||||
|
<OverrideTable />
|
||||||
|
<div>
|
||||||
|
Showing most recent actions (max 5). To see more, please contact support
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Note: Once you save, your changes will be visible to students.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalDialog.Body>
|
||||||
|
<ModalDialog.Footer>
|
||||||
|
<ActionRow>
|
||||||
|
<ModalDialog.CloseButton
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</ModalDialog.CloseButton>
|
||||||
|
<Button
|
||||||
|
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
Save Grades
|
||||||
|
</Button>
|
||||||
|
</ActionRow>
|
||||||
|
</ModalDialog.Footer>
|
||||||
|
</ModalDialog>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`EditModal component render without error snapshot 1`] = `
|
||||||
|
<ModalDialog
|
||||||
|
hasCloseButton={true}
|
||||||
|
isFullscreenOnMobile={true}
|
||||||
|
isOpen="test-is-open"
|
||||||
|
onClose={[MockFunction hooks.onClose]}
|
||||||
|
size="xl"
|
||||||
|
title="Edit Grades"
|
||||||
|
>
|
||||||
|
<ModalDialog.Body>
|
||||||
|
<div>
|
||||||
|
<ModalHeaders />
|
||||||
|
<Alert
|
||||||
|
dismissible={false}
|
||||||
|
show={false}
|
||||||
|
variant="danger"
|
||||||
|
/>
|
||||||
|
<OverrideTable />
|
||||||
|
<div>
|
||||||
|
Showing most recent actions (max 5). To see more, please contact support
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Note: Once you save, your changes will be visible to students.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalDialog.Body>
|
||||||
|
<ModalDialog.Footer>
|
||||||
|
<ActionRow>
|
||||||
|
<ModalDialog.CloseButton
|
||||||
|
variant="tertiary"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</ModalDialog.CloseButton>
|
||||||
|
<Button
|
||||||
|
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
Save Grades
|
||||||
|
</Button>
|
||||||
|
</ActionRow>
|
||||||
|
</ModalDialog.Footer>
|
||||||
|
</ModalDialog>
|
||||||
|
`;
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`EditModal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
|
|
||||||
<ModalDialog
|
|
||||||
hasCloseButton={true}
|
|
||||||
isFullscreenOnMobile={true}
|
|
||||||
isOpen={true}
|
|
||||||
onClose={[MockFunction this.closeAssignmentModal]}
|
|
||||||
size="xl"
|
|
||||||
title="Edit Grades"
|
|
||||||
>
|
|
||||||
<ModalDialog.Body>
|
|
||||||
<div>
|
|
||||||
<ModalHeaders />
|
|
||||||
<Alert
|
|
||||||
dismissible={false}
|
|
||||||
show={true}
|
|
||||||
variant="danger"
|
|
||||||
>
|
|
||||||
Weve been trying to contact you regarding...
|
|
||||||
</Alert>
|
|
||||||
<OverrideTable />
|
|
||||||
<div>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
|
|
||||||
description="Edit Modal visibility hint message"
|
|
||||||
id="gradebook.GradesView.EditModal.contactSupport"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Note: Once you save, your changes will be visible to students."
|
|
||||||
description="Edit Modal saved changes effect hint"
|
|
||||||
id="gradebook.GradesView.EditModal.saveVisibility"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalDialog.Body>
|
|
||||||
<ModalDialog.Footer>
|
|
||||||
<ActionRow>
|
|
||||||
<ModalDialog.CloseButton
|
|
||||||
variant="tertiary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Cancel"
|
|
||||||
description="Edit Modal close button text"
|
|
||||||
id="gradebook.GradesView.EditModal.closeText"
|
|
||||||
/>
|
|
||||||
</ModalDialog.CloseButton>
|
|
||||||
<Button
|
|
||||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Save Grades"
|
|
||||||
description="Edit Modal Save button label"
|
|
||||||
id="gradebook.GradesView.EditModal.saveGrade"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</ActionRow>
|
|
||||||
</ModalDialog.Footer>
|
|
||||||
</ModalDialog>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`EditModal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
|
|
||||||
<ModalDialog
|
|
||||||
hasCloseButton={true}
|
|
||||||
isFullscreenOnMobile={true}
|
|
||||||
isOpen={false}
|
|
||||||
onClose={[MockFunction this.closeAssignmentModal]}
|
|
||||||
size="xl"
|
|
||||||
title="Edit Grades"
|
|
||||||
>
|
|
||||||
<ModalDialog.Body>
|
|
||||||
<div>
|
|
||||||
<ModalHeaders />
|
|
||||||
<Alert
|
|
||||||
dismissible={false}
|
|
||||||
show={false}
|
|
||||||
variant="danger"
|
|
||||||
>
|
|
||||||
|
|
||||||
</Alert>
|
|
||||||
<OverrideTable />
|
|
||||||
<div>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Showing most recent actions (max 5). To see more, please contact support"
|
|
||||||
description="Edit Modal visibility hint message"
|
|
||||||
id="gradebook.GradesView.EditModal.contactSupport"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Note: Once you save, your changes will be visible to students."
|
|
||||||
description="Edit Modal saved changes effect hint"
|
|
||||||
id="gradebook.GradesView.EditModal.saveVisibility"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalDialog.Body>
|
|
||||||
<ModalDialog.Footer>
|
|
||||||
<ActionRow>
|
|
||||||
<ModalDialog.CloseButton
|
|
||||||
variant="tertiary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Cancel"
|
|
||||||
description="Edit Modal close button text"
|
|
||||||
id="gradebook.GradesView.EditModal.closeText"
|
|
||||||
/>
|
|
||||||
</ModalDialog.CloseButton>
|
|
||||||
<Button
|
|
||||||
onClick={[MockFunction this.handleAdjustedGradeClick]}
|
|
||||||
variant="primary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Save Grades"
|
|
||||||
description="Edit Modal Save button label"
|
|
||||||
id="gradebook.GradesView.EditModal.saveGrade"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</ActionRow>
|
|
||||||
</ModalDialog.Footer>
|
|
||||||
</ModalDialog>
|
|
||||||
`;
|
|
||||||
29
src/components/GradesView/EditModal/hooks.js
Normal file
29
src/components/GradesView/EditModal/hooks.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
export const useEditModalData = () => {
|
||||||
|
const error = selectors.grades.useGradeData().gradeOverrideHistoryError;
|
||||||
|
const isOpen = selectors.app.useModalData().open;
|
||||||
|
const closeModal = actions.app.useCloseModal();
|
||||||
|
const doneViewingAssignment = actions.grades.useDoneViewingAssignment();
|
||||||
|
const updateGrades = thunkActions.grades.useUpdateGrades();
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
doneViewingAssignment();
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdjustedGradeClick = () => {
|
||||||
|
updateGrades();
|
||||||
|
doneViewingAssignment();
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onClose,
|
||||||
|
error,
|
||||||
|
handleAdjustedGradeClick,
|
||||||
|
isOpen,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useEditModalData;
|
||||||
68
src/components/GradesView/EditModal/hooks.test.js
Normal file
68
src/components/GradesView/EditModal/hooks.test.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { selectors, actions, thunkActions } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import useEditModalData from './hooks';
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
actions: {
|
||||||
|
app: { useCloseModal: jest.fn() },
|
||||||
|
grades: { useDoneViewingAssignment: jest.fn() },
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
app: { useModalData: jest.fn() },
|
||||||
|
grades: { useGradeData: jest.fn() },
|
||||||
|
},
|
||||||
|
thunkActions: {
|
||||||
|
grades: { useUpdateGrades: jest.fn() },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const closeModal = jest.fn();
|
||||||
|
const doneViewingAssignment = jest.fn();
|
||||||
|
const updateGrades = jest.fn();
|
||||||
|
actions.app.useCloseModal.mockReturnValue(closeModal);
|
||||||
|
actions.grades.useDoneViewingAssignment.mockReturnValue(doneViewingAssignment);
|
||||||
|
thunkActions.grades.useUpdateGrades.mockReturnValue(updateGrades);
|
||||||
|
|
||||||
|
const gradeData = { gradeOverridHistoryError: 'test-error' };
|
||||||
|
const modalData = { open: true };
|
||||||
|
selectors.app.useModalData.mockReturnValue(modalData);
|
||||||
|
selectors.grades.useGradeData.mockReturnValue(gradeData);
|
||||||
|
|
||||||
|
let out;
|
||||||
|
describe('useEditModalData', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
out = useEditModalData();
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||||
|
expect(selectors.app.useModalData).toHaveBeenCalled();
|
||||||
|
expect(actions.app.useCloseModal).toHaveBeenCalled();
|
||||||
|
expect(actions.grades.useDoneViewingAssignment).toHaveBeenCalled();
|
||||||
|
expect(thunkActions.grades.useUpdateGrades).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('output', () => {
|
||||||
|
it('forwards error from gradeData.gradeOverrideHistoryError', () => {
|
||||||
|
expect(out.error).toEqual(gradeData.gradeOverrideHistoryError);
|
||||||
|
});
|
||||||
|
it('forwards isOpen from modalData.open', () => {
|
||||||
|
expect(out.isOpen).toEqual(modalData.open);
|
||||||
|
});
|
||||||
|
describe('handleAdjustedGradeClick', () => {
|
||||||
|
it('updates grades, calls doneViewingAssignment and closeModal', () => {
|
||||||
|
out.handleAdjustedGradeClick();
|
||||||
|
expect(updateGrades).toHaveBeenCalled();
|
||||||
|
expect(doneViewingAssignment).toHaveBeenCalled();
|
||||||
|
expect(closeModal).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('onClose calls doneViewingAssignment and closeModal', () => {
|
||||||
|
out.onClose();
|
||||||
|
expect(doneViewingAssignment).toHaveBeenCalled();
|
||||||
|
expect(closeModal).toHaveBeenCalled();
|
||||||
|
expect(updateGrades).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -9,15 +6,12 @@ import {
|
|||||||
ModalDialog,
|
ModalDialog,
|
||||||
ActionRow,
|
ActionRow,
|
||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import thunkActions from 'data/thunkActions';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import OverrideTable from './OverrideTable';
|
import OverrideTable from './OverrideTable';
|
||||||
import ModalHeaders from './ModalHeaders';
|
import ModalHeaders from './ModalHeaders';
|
||||||
|
import useEditModalData from './hooks';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <EditModal />
|
* <EditModal />
|
||||||
@@ -28,87 +22,48 @@ import ModalHeaders from './ModalHeaders';
|
|||||||
* adjusting the grade.
|
* adjusting the grade.
|
||||||
* (also provides a close button that clears the modal state)
|
* (also provides a close button that clears the modal state)
|
||||||
*/
|
*/
|
||||||
export class EditModal extends React.Component {
|
export const EditModal = () => {
|
||||||
constructor(props) {
|
const { formatMessage } = useIntl();
|
||||||
super(props);
|
const {
|
||||||
this.closeAssignmentModal = this.closeAssignmentModal.bind(this);
|
onClose,
|
||||||
this.handleAdjustedGradeClick = this.handleAdjustedGradeClick.bind(this);
|
error,
|
||||||
}
|
handleAdjustedGradeClick,
|
||||||
|
isOpen,
|
||||||
|
} = useEditModalData();
|
||||||
|
|
||||||
closeAssignmentModal() {
|
return (
|
||||||
this.props.doneViewingAssignment();
|
<ModalDialog
|
||||||
this.props.closeModal();
|
title={formatMessage(messages.title)}
|
||||||
}
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
size="xl"
|
||||||
|
hasCloseButton
|
||||||
|
isFullscreenOnMobile
|
||||||
|
>
|
||||||
|
<ModalDialog.Body>
|
||||||
|
<div>
|
||||||
|
<ModalHeaders />
|
||||||
|
<Alert variant="danger" show={!!error} dismissible={false}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
<OverrideTable />
|
||||||
|
<div>{formatMessage(messages.visibility)}</div>
|
||||||
|
<div>{formatMessage(messages.saveVisibility)}</div>
|
||||||
|
</div>
|
||||||
|
</ModalDialog.Body>
|
||||||
|
|
||||||
handleAdjustedGradeClick() {
|
<ModalDialog.Footer>
|
||||||
this.props.updateGrades();
|
<ActionRow>
|
||||||
this.closeAssignmentModal();
|
<ModalDialog.CloseButton variant="tertiary">
|
||||||
}
|
{formatMessage(messages.closeText)}
|
||||||
|
</ModalDialog.CloseButton>
|
||||||
render() {
|
<Button variant="primary" onClick={handleAdjustedGradeClick}>
|
||||||
return (
|
{formatMessage(messages.saveGrade)}
|
||||||
<ModalDialog
|
</Button>
|
||||||
title={this.props.intl.formatMessage(messages.title)}
|
</ActionRow>
|
||||||
isOpen={this.props.open}
|
</ModalDialog.Footer>
|
||||||
onClose={this.closeAssignmentModal}
|
</ModalDialog>
|
||||||
size="xl"
|
);
|
||||||
hasCloseButton
|
|
||||||
isFullscreenOnMobile
|
|
||||||
>
|
|
||||||
<ModalDialog.Body>
|
|
||||||
<div>
|
|
||||||
<ModalHeaders />
|
|
||||||
<Alert
|
|
||||||
variant="danger"
|
|
||||||
show={!!this.props.gradeOverrideHistoryError}
|
|
||||||
dismissible={false}
|
|
||||||
>
|
|
||||||
{this.props.gradeOverrideHistoryError}
|
|
||||||
</Alert>
|
|
||||||
<OverrideTable />
|
|
||||||
<div><FormattedMessage {...messages.visibility} /></div>
|
|
||||||
<div><FormattedMessage {...messages.saveVisibility} /></div>
|
|
||||||
</div>
|
|
||||||
</ModalDialog.Body>
|
|
||||||
<ModalDialog.Footer>
|
|
||||||
<ActionRow>
|
|
||||||
<ModalDialog.CloseButton variant="tertiary">
|
|
||||||
<FormattedMessage {...messages.closeText} />
|
|
||||||
</ModalDialog.CloseButton>
|
|
||||||
<Button variant="primary" onClick={this.handleAdjustedGradeClick}>
|
|
||||||
<FormattedMessage {...messages.saveGrade} />
|
|
||||||
</Button>
|
|
||||||
</ActionRow>
|
|
||||||
</ModalDialog.Footer>
|
|
||||||
</ModalDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditModal.defaultProps = {
|
|
||||||
gradeOverrideHistoryError: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
EditModal.propTypes = {
|
export default EditModal;
|
||||||
// redux
|
|
||||||
gradeOverrideHistoryError: PropTypes.string,
|
|
||||||
open: PropTypes.bool.isRequired,
|
|
||||||
closeModal: PropTypes.func.isRequired,
|
|
||||||
doneViewingAssignment: PropTypes.func.isRequired,
|
|
||||||
updateGrades: PropTypes.func.isRequired,
|
|
||||||
// injected
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
|
||||||
gradeOverrideHistoryError: selectors.grades.gradeOverrideHistoryError(state),
|
|
||||||
open: selectors.app.modalState.open(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mapDispatchToProps = {
|
|
||||||
closeModal: actions.app.closeModal,
|
|
||||||
doneViewingAssignment: actions.grades.doneViewingAssignment,
|
|
||||||
updateGrades: thunkActions.grades.updateGrades,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(EditModal));
|
|
||||||
|
|||||||
132
src/components/GradesView/EditModal/index.test.jsx
Normal file
132
src/components/GradesView/EditModal/index.test.jsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActionRow,
|
||||||
|
ModalDialog,
|
||||||
|
} from '@edx/paragon';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { formatMessage } from 'testUtils';
|
||||||
|
|
||||||
|
import ModalHeaders from './ModalHeaders';
|
||||||
|
import OverrideTable from './OverrideTable';
|
||||||
|
import useEditModalData from './hooks';
|
||||||
|
import EditModal from '.';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
jest.mock('./hooks', () => jest.fn());
|
||||||
|
jest.mock('./ModalHeaders', () => 'ModalHeaders');
|
||||||
|
jest.mock('./OverrideTable', () => 'OverrideTable');
|
||||||
|
|
||||||
|
const hookProps = {
|
||||||
|
onClose: jest.fn().mockName('hooks.onClose'),
|
||||||
|
error: 'test-error',
|
||||||
|
handleAdjustedGradeClick: jest.fn().mockName('hooks.handleAdjustedGradeClick'),
|
||||||
|
isOpen: 'test-is-open',
|
||||||
|
};
|
||||||
|
useEditModalData.mockReturnValue(hookProps);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('EditModal component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
el = shallow(<EditModal />);
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes intl hook', () => {
|
||||||
|
expect(useIntl).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('initializes component hooks', () => {
|
||||||
|
expect(useEditModalData).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('render', () => {
|
||||||
|
test('modal props', () => {
|
||||||
|
const modalProps = el.find(ModalDialog).props();
|
||||||
|
expect(modalProps.title).toEqual(formatMessage(messages.title));
|
||||||
|
expect(modalProps.isOpen).toEqual(hookProps.isOpen);
|
||||||
|
expect(modalProps.onClose).toEqual(hookProps.onClose);
|
||||||
|
});
|
||||||
|
const loadBody = () => {
|
||||||
|
const body = el.find(ModalDialog).children().at(0);
|
||||||
|
const children = body.find('div').children();
|
||||||
|
return { body, children };
|
||||||
|
};
|
||||||
|
const testBody = () => {
|
||||||
|
test('type', () => {
|
||||||
|
const { body } = loadBody();
|
||||||
|
expect(body.type()).toEqual('ModalDialog.Body');
|
||||||
|
});
|
||||||
|
test('headers row', () => {
|
||||||
|
const { children } = loadBody();
|
||||||
|
expect(children.at(0)).toMatchObject(shallow(<ModalHeaders />));
|
||||||
|
});
|
||||||
|
test('table row', () => {
|
||||||
|
const { children } = loadBody();
|
||||||
|
expect(children.at(2)).toMatchObject(shallow(<OverrideTable />));
|
||||||
|
});
|
||||||
|
test('messages', () => {
|
||||||
|
const { children } = loadBody();
|
||||||
|
expect(
|
||||||
|
children.at(3).contains(formatMessage(messages.visibility)),
|
||||||
|
).toEqual(true);
|
||||||
|
expect(
|
||||||
|
children.at(4).contains(formatMessage(messages.saveVisibility)),
|
||||||
|
).toEqual(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const testFooter = () => {
|
||||||
|
let footer;
|
||||||
|
beforeEach(() => {
|
||||||
|
footer = el.find(ModalDialog).children().at(1);
|
||||||
|
});
|
||||||
|
test('type', () => {
|
||||||
|
expect(footer.type()).toEqual('ModalDialog.Footer');
|
||||||
|
});
|
||||||
|
test('contains action row', () => {
|
||||||
|
expect(footer.children().at(0).type()).toEqual('ActionRow');
|
||||||
|
});
|
||||||
|
test('close button', () => {
|
||||||
|
const button = footer.find(ActionRow).children().at(0);
|
||||||
|
expect(button.contains(formatMessage(messages.closeText))).toEqual(true);
|
||||||
|
expect(button.type()).toEqual('ModalDialog.CloseButton');
|
||||||
|
});
|
||||||
|
test('adjusted grade button', () => {
|
||||||
|
const button = footer.find(ActionRow).children().at(1);
|
||||||
|
expect(button.contains(formatMessage(messages.saveGrade))).toEqual(true);
|
||||||
|
expect(button.type()).toEqual('Button');
|
||||||
|
expect(button.props().onClick).toEqual(hookProps.handleAdjustedGradeClick);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
describe('without error', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useEditModalData.mockReturnValueOnce({ ...hookProps, error: undefined });
|
||||||
|
el = shallow(<EditModal />);
|
||||||
|
});
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
testBody();
|
||||||
|
testFooter();
|
||||||
|
test('alert row', () => {
|
||||||
|
const alert = loadBody().children.at(1);
|
||||||
|
expect(alert.type()).toEqual('Alert');
|
||||||
|
expect(alert.props().show).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('with error', () => {
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
testBody();
|
||||||
|
test('alert row', () => {
|
||||||
|
const alert = loadBody().children.at(1);
|
||||||
|
expect(alert.type()).toEqual('Alert');
|
||||||
|
expect(alert.props().show).toEqual(true);
|
||||||
|
expect(alert.contains(hookProps.error)).toEqual(true);
|
||||||
|
});
|
||||||
|
testFooter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import thunkActions from 'data/thunkActions';
|
|
||||||
|
|
||||||
import {
|
|
||||||
EditModal,
|
|
||||||
mapDispatchToProps,
|
|
||||||
mapStateToProps,
|
|
||||||
}
|
|
||||||
from '.';
|
|
||||||
|
|
||||||
jest.mock('./OverrideTable', () => 'OverrideTable');
|
|
||||||
jest.mock('./ModalHeaders', () => 'ModalHeaders');
|
|
||||||
jest.mock('data/actions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: { closeModal: jest.fn() },
|
|
||||||
grades: { doneViewingAssignment: jest.fn() },
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('data/thunkActions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
grades: { updateGrades: jest.fn() },
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: {
|
|
||||||
modalState: {
|
|
||||||
open: jest.fn(state => ({ isModalOpen: state })),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grades: {
|
|
||||||
gradeOverrideHistoryError: jest.fn(state => ({ overrideHistoryError: state })),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
describe('EditModal', () => {
|
|
||||||
let props;
|
|
||||||
beforeEach(() => {
|
|
||||||
props = {
|
|
||||||
gradeOverrideHistoryError: 'Weve been trying to contact you regarding...',
|
|
||||||
open: true,
|
|
||||||
closeModal: jest.fn(),
|
|
||||||
doneViewingAssignment: jest.fn(),
|
|
||||||
updateGrades: jest.fn(),
|
|
||||||
|
|
||||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Component', () => {
|
|
||||||
describe('behavior', () => {
|
|
||||||
let el;
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<EditModal {...props} />);
|
|
||||||
});
|
|
||||||
describe('closeAssignmentModal', () => {
|
|
||||||
it('calls props.doneViewingAssignment and props.closeModal', () => {
|
|
||||||
el.instance().closeAssignmentModal();
|
|
||||||
expect(props.doneViewingAssignment).toHaveBeenCalledWith();
|
|
||||||
expect(props.closeModal).toHaveBeenCalledWith();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('handleAdjustedGradeClick', () => {
|
|
||||||
it('calls props.updateGardes and this.closeAssignmentModal', () => {
|
|
||||||
el.instance().closeAssignmentModal = jest.fn();
|
|
||||||
el.instance().handleAdjustedGradeClick();
|
|
||||||
expect(props.updateGrades).toHaveBeenCalledWith();
|
|
||||||
expect(el.instance().closeAssignmentModal).toHaveBeenCalledWith();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('snapshots', () => {
|
|
||||||
let el;
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<EditModal {...props} />);
|
|
||||||
el.instance().closeAssignmentModal = jest.fn().mockName('this.closeAssignmentModal');
|
|
||||||
el.instance().handleAdjustedGradeClick = jest.fn().mockName(
|
|
||||||
'this.handleAdjustedGradeClick',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
describe('gradeOverrideHistoryError is and empty and open is true', () => {
|
|
||||||
test('modal open and StatusAlert showing', () => {
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('gradeOverrideHistoryError is empty and open is false', () => {
|
|
||||||
test('modal closed and StatusAlert closed', () => {
|
|
||||||
el.setProps({ open: false, gradeOverrideHistoryError: '' });
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
const testState = { martha: 'why did you say that name?!' };
|
|
||||||
let mapped;
|
|
||||||
beforeEach(() => {
|
|
||||||
mapped = mapStateToProps(testState);
|
|
||||||
});
|
|
||||||
test('gradeOverrideHistoryError from grades.gradeOverrideHistoryError', () => {
|
|
||||||
expect(
|
|
||||||
mapped.gradeOverrideHistoryError,
|
|
||||||
).toEqual(selectors.grades.gradeOverrideHistoryError(testState));
|
|
||||||
});
|
|
||||||
test('open from app.modalState.open', () => {
|
|
||||||
expect(mapped.open).toEqual(selectors.app.modalState.open(testState));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapDispatchToProps', () => {
|
|
||||||
test('closeModal from actions.app.closeModal', () => {
|
|
||||||
expect(mapDispatchToProps.closeModal).toEqual(actions.app.closeModal);
|
|
||||||
});
|
|
||||||
test('doneViewingAssignemtn from actions.grades.doneViewingAssignment', () => {
|
|
||||||
expect(
|
|
||||||
mapDispatchToProps.doneViewingAssignment,
|
|
||||||
).toEqual(actions.grades.doneViewingAssignment);
|
|
||||||
});
|
|
||||||
test('updateGrades from thunkActions.grades.updateGrades', () => {
|
|
||||||
expect(mapDispatchToProps.updateGrades).toEqual(thunkActions.grades.updateGrades);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { Button } from '@edx/paragon';
|
import { Button } from '@edx/paragon';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
import { selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FilterBadge
|
* FilterBadge
|
||||||
@@ -16,56 +15,43 @@ import selectors from 'data/selectors';
|
|||||||
* @param {string} filterName - api filter name (for redux connector)
|
* @param {string} filterName - api filter name (for redux connector)
|
||||||
*/
|
*/
|
||||||
export const FilterBadge = ({
|
export const FilterBadge = ({
|
||||||
config: {
|
filterName,
|
||||||
|
handleClose,
|
||||||
|
}) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const {
|
||||||
displayName,
|
displayName,
|
||||||
isDefault,
|
isDefault,
|
||||||
hideValue,
|
hideValue,
|
||||||
value,
|
value,
|
||||||
connectedFilters,
|
connectedFilters,
|
||||||
},
|
} = selectors.root.useFilterBadgeConfig(filterName);
|
||||||
handleClose,
|
if (isDefault) {
|
||||||
}) => !isDefault && (
|
return null;
|
||||||
<div>
|
}
|
||||||
<span className="badge badge-info">
|
return (
|
||||||
<span>
|
<div>
|
||||||
<FormattedMessage {...displayName} />
|
<span className="badge badge-info">
|
||||||
|
<span>{formatMessage(displayName)}</span>
|
||||||
|
<span>
|
||||||
|
{!hideValue ? `: ${value}` : ''}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
className="btn-info"
|
||||||
|
aria-label="close"
|
||||||
|
onClick={handleClose(connectedFilters)}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<br />
|
||||||
{!hideValue ? `: ${value}` : ''}
|
</div>
|
||||||
</span>
|
);
|
||||||
<Button
|
};
|
||||||
className="btn-info"
|
|
||||||
aria-label="close"
|
|
||||||
onClick={handleClose(connectedFilters)}
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
FilterBadge.propTypes = {
|
FilterBadge.propTypes = {
|
||||||
handleClose: PropTypes.func.isRequired,
|
handleClose: PropTypes.func.isRequired,
|
||||||
// eslint-disable-next-line
|
|
||||||
filterName: PropTypes.string.isRequired,
|
filterName: PropTypes.string.isRequired,
|
||||||
// redux
|
|
||||||
config: PropTypes.shape({
|
|
||||||
connectedFilters: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
displayName: PropTypes.shape({
|
|
||||||
defaultMessage: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
isDefault: PropTypes.bool.isRequired,
|
|
||||||
hideValue: PropTypes.bool,
|
|
||||||
value: PropTypes.oneOfType([
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.bool,
|
|
||||||
]),
|
|
||||||
}).isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapStateToProps = (state, ownProps) => ({
|
export default FilterBadge;
|
||||||
config: selectors.root.filterBadgeConfig(state, ownProps.filterName),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(FilterBadge);
|
|
||||||
|
|||||||
@@ -1,107 +1,95 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { formatMessage } from 'testUtils';
|
||||||
import { Button } from '@edx/paragon';
|
import { Button } from '@edx/paragon';
|
||||||
import selectors from 'data/selectors';
|
import { selectors } from 'data/redux/hooks';
|
||||||
import { FilterBadge, mapStateToProps } from './FilterBadge';
|
import FilterBadge from './FilterBadge';
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({
|
jest.mock('@edx/paragon', () => ({
|
||||||
Button: () => 'Button',
|
Button: () => 'Button',
|
||||||
}));
|
}));
|
||||||
jest.mock('data/selectors', () => ({
|
jest.mock('data/redux/hooks', () => ({
|
||||||
__esModule: true,
|
selectors: {
|
||||||
default: {
|
|
||||||
root: {
|
root: {
|
||||||
filterBadgeConfig: jest.fn(state => ({ filterBadgeConfig: state })),
|
useFilterBadgeConfig: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const handleClose = jest.fn(filters => ({ handleClose: filters }));
|
||||||
|
const filterName = 'test-filter-name';
|
||||||
|
|
||||||
|
const hookProps = {
|
||||||
|
displayName: {
|
||||||
|
defaultMessage: 'a common name',
|
||||||
|
},
|
||||||
|
isDefault: false,
|
||||||
|
hideValue: false,
|
||||||
|
value: 'a common value',
|
||||||
|
connectedFilters: ['some', 'filters'],
|
||||||
|
};
|
||||||
|
selectors.root.useFilterBadgeConfig.mockReturnValue(hookProps);
|
||||||
|
|
||||||
|
let el;
|
||||||
describe('FilterBadge', () => {
|
describe('FilterBadge', () => {
|
||||||
describe('component', () => {
|
beforeEach(() => {
|
||||||
const config = {
|
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
|
||||||
displayName: {
|
});
|
||||||
defaultMessage: 'a common name',
|
describe('behavior', () => {
|
||||||
},
|
it('initializes intl hook', () => {
|
||||||
isDefault: false,
|
expect(useIntl).toHaveBeenCalled();
|
||||||
hideValue: false,
|
|
||||||
value: 'a common value',
|
|
||||||
connectedFilters: ['some', 'filters'],
|
|
||||||
};
|
|
||||||
const filterName = 'api.filter.name';
|
|
||||||
let handleClose;
|
|
||||||
let el;
|
|
||||||
let props;
|
|
||||||
beforeEach(() => {
|
|
||||||
handleClose = (filters) => ({ handleClose: filters });
|
|
||||||
props = { filterName, handleClose, config };
|
|
||||||
});
|
});
|
||||||
describe('with default value', () => {
|
it('initializes redux hooks', () => {
|
||||||
beforeEach(() => {
|
expect(selectors.root.useFilterBadgeConfig).toHaveBeenCalledWith(filterName);
|
||||||
el = shallow(
|
|
||||||
<FilterBadge {...props} config={{ ...config, isDefault: true }} />,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('snapshot - empty', () => {
|
|
||||||
expect(el).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
it('does not display', () => {
|
|
||||||
expect(el).toEqual({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('with non-default value (active)', () => {
|
|
||||||
describe('if hideValue is true', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(
|
|
||||||
<FilterBadge {...props} config={{ ...config, hideValue: true }} />,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('snapshot - shows displayName but not value in span', () => {
|
|
||||||
expect(el).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
it('shows displayName but not value in span', () => {
|
|
||||||
expect(el.find('span.badge').childAt(0).getElement()).toEqual(
|
|
||||||
<span>
|
|
||||||
<FormattedMessage {...config.displayName} />
|
|
||||||
</span>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('calls a handleClose event for connected filters on button click', () => {
|
|
||||||
expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('if hideValue is false (default)', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<FilterBadge {...props} />);
|
|
||||||
});
|
|
||||||
test('snapshot', () => {
|
|
||||||
expect(el).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
it('shows displayName and value in span', () => {
|
|
||||||
expect(el.find('span.badge').childAt(0).getElement()).toEqual(
|
|
||||||
<span>
|
|
||||||
<FormattedMessage {...config.displayName} />
|
|
||||||
</span>,
|
|
||||||
);
|
|
||||||
expect(el.find('span.badge').childAt(1).getElement()).toEqual(
|
|
||||||
<span>
|
|
||||||
{`: ${config.value}`}
|
|
||||||
</span>,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('calls a handleClose event for connected filters on button click', () => {
|
|
||||||
expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('mapStateToProps', () => {
|
describe('render', () => {
|
||||||
const testState = { some: 'kind', of: 'alien' };
|
const testDisplayName = () => {
|
||||||
const filterName = 'Lilu Dallas Multipass';
|
test('formatted display name appears on badge', () => {
|
||||||
test('config loads config from root.filterBadgeConfig with ownProps.filterName', () => {
|
expect(el.contains(formatMessage(hookProps.displayName))).toEqual(true);
|
||||||
const { config } = mapStateToProps(testState, { filterName });
|
});
|
||||||
expect(config).toEqual(selectors.root.filterBadgeConfig(testState, filterName));
|
};
|
||||||
|
const testCloseButton = () => {
|
||||||
|
test('close button forwards close method', () => {
|
||||||
|
expect(el.find(Button).props().onClick).toEqual(handleClose(hookProps.connectedFilters));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
test('empty render if isDefault', () => {
|
||||||
|
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
|
||||||
|
...hookProps,
|
||||||
|
isDefault: true,
|
||||||
|
});
|
||||||
|
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
|
||||||
|
expect(el.isEmptyRender()).toEqual(true);
|
||||||
|
});
|
||||||
|
describe('hide Value', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
|
||||||
|
...hookProps,
|
||||||
|
hideValue: true,
|
||||||
|
});
|
||||||
|
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
|
||||||
|
});
|
||||||
|
testDisplayName();
|
||||||
|
testCloseButton();
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
test('value is note present in the badge', () => {
|
||||||
|
expect(el.contains(hookProps.value)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('do not hide value', () => {
|
||||||
|
testDisplayName();
|
||||||
|
testCloseButton();
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
test('value is note present in the badge', () => {
|
||||||
|
expect(el.text().includes(hookProps.value)).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`FilterBadge component with default value snapshot - empty 1`] = `""`;
|
exports[`FilterBadge render do not hide value snapshot 1`] = `
|
||||||
|
|
||||||
exports[`FilterBadge component with non-default value (active) if hideValue is false (default) snapshot 1`] = `
|
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
className="badge badge-info"
|
className="badge badge-info"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<FormattedMessage
|
a common name
|
||||||
defaultMessage="a common name"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
: a common value
|
: a common value
|
||||||
@@ -38,15 +34,13 @@ exports[`FilterBadge component with non-default value (active) if hideValue is f
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`FilterBadge component with non-default value (active) if hideValue is true snapshot - shows displayName but not value in span 1`] = `
|
exports[`FilterBadge render hide Value snapshot 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
className="badge badge-info"
|
className="badge badge-info"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<FormattedMessage
|
a common name
|
||||||
defaultMessage="a common name"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<span />
|
<span />
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { Button, Icon } from '@edx/paragon';
|
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import thunkActions from 'data/thunkActions';
|
|
||||||
|
|
||||||
import messages from './FilterMenuToggle.messages';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
|
|
||||||
* as well as the search box for searching by username/email.
|
|
||||||
*/
|
|
||||||
export const FilterMenuToggle = ({ toggleFilterDrawer }) => (
|
|
||||||
<Button
|
|
||||||
id="edit-filters-btn"
|
|
||||||
className="btn-primary align-self-start"
|
|
||||||
onClick={toggleFilterDrawer}
|
|
||||||
>
|
|
||||||
<Icon className="fa fa-filter" /> <FormattedMessage {...messages.editFilters} />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
FilterMenuToggle.propTypes = {
|
|
||||||
// From Redux
|
|
||||||
toggleFilterDrawer: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = () => ({});
|
|
||||||
|
|
||||||
export const mapDispatchToProps = {
|
|
||||||
toggleFilterDrawer: thunkActions.app.filterMenu.toggle,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(FilterMenuToggle);
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import thunkActions from 'data/thunkActions';
|
|
||||||
|
|
||||||
import { FilterMenuToggle, mapDispatchToProps, mapStateToProps } from './FilterMenuToggle';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({
|
|
||||||
Button: () => 'Button',
|
|
||||||
Icon: () => 'Icon',
|
|
||||||
}));
|
|
||||||
jest.mock('data/thunkActions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: {
|
|
||||||
filterMenu: { toggle: jest.fn() },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('FilterMenuToggle component', () => {
|
|
||||||
describe('snapshots', () => {
|
|
||||||
test('basic snapshot', () => {
|
|
||||||
const toggleFilterDrawer = jest.fn().mockName('this.props.toggleFilterDrawer');
|
|
||||||
expect(shallow((
|
|
||||||
<FilterMenuToggle {...{ toggleFilterDrawer }} />
|
|
||||||
))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
test('does not connect any selectors', () => {
|
|
||||||
expect(mapStateToProps({ test: 'state' })).toEqual({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapDispatchToProps', () => {
|
|
||||||
test('toggleFilterDrawer from thunkActions.app.filterMenu.toggle', () => {
|
|
||||||
expect(mapDispatchToProps.toggleFilterDrawer).toEqual(
|
|
||||||
thunkActions.app.filterMenu.toggle,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`FilterMenuToggle component render snapshot 1`] = `
|
||||||
|
<Button
|
||||||
|
className="btn-primary align-self-start"
|
||||||
|
id="edit-filters-btn"
|
||||||
|
onClick={[MockFunction hooks.toggleFilterMenu]}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
src="FilterAlt"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Edit Filters
|
||||||
|
</Button>
|
||||||
|
`;
|
||||||
31
src/components/GradesView/FilterMenuToggle/index.jsx
Normal file
31
src/components/GradesView/FilterMenuToggle/index.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button, Icon } from '@edx/paragon';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { FilterAlt } from '@edx/paragon/icons';
|
||||||
|
|
||||||
|
import { thunkActions } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
|
||||||
|
* as well as the search box for searching by username/email.
|
||||||
|
*/
|
||||||
|
export const FilterMenuToggle = () => {
|
||||||
|
const toggleFilterMenu = thunkActions.app.filterMenu.useToggleMenu();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
id="edit-filters-btn"
|
||||||
|
className="btn-primary align-self-start"
|
||||||
|
onClick={toggleFilterMenu}
|
||||||
|
>
|
||||||
|
<Icon src={FilterAlt} /> {formatMessage(messages.editFilters)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FilterMenuToggle.propTypes = {};
|
||||||
|
|
||||||
|
export default FilterMenuToggle;
|
||||||
47
src/components/GradesView/FilterMenuToggle/index.test.jsx
Normal file
47
src/components/GradesView/FilterMenuToggle/index.test.jsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { formatMessage } from 'testUtils';
|
||||||
|
import { thunkActions } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import FilterMenuToggle from '.';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
thunkActions: {
|
||||||
|
app: {
|
||||||
|
filterMenu: {
|
||||||
|
useToggleMenu: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const toggleFilterMenu = jest.fn().mockName('hooks.toggleFilterMenu');
|
||||||
|
thunkActions.app.filterMenu.useToggleMenu.mockReturnValue(toggleFilterMenu);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('FilterMenuToggle component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
el = shallow(<FilterMenuToggle />);
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes intl hook', () => {
|
||||||
|
expect(useIntl).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
expect(thunkActions.app.filterMenu.useToggleMenu).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('render', () => {
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
expect(el.type()).toEqual('Button');
|
||||||
|
expect(el.props().onClick).toEqual(toggleFilterMenu);
|
||||||
|
expect(el.text().includes(formatMessage(messages.editFilters)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <FilteredUsersLabel />
|
|
||||||
* Simple label component displaying the filtered and total users shown
|
|
||||||
*/
|
|
||||||
export const FilteredUsersLabel = ({
|
|
||||||
filteredUsersCount,
|
|
||||||
totalUsersCount,
|
|
||||||
}) => {
|
|
||||||
if (!totalUsersCount) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const bold = (val) => (<span className="font-weight-bold">{val}</span>);
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
id="gradebook.GradesTab.usersVisibilityLabel'"
|
|
||||||
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
|
|
||||||
description="Users visibility label"
|
|
||||||
values={{
|
|
||||||
filteredUsers: bold(filteredUsersCount),
|
|
||||||
totalUsers: bold(totalUsersCount),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
FilteredUsersLabel.propTypes = {
|
|
||||||
filteredUsersCount: PropTypes.number.isRequired,
|
|
||||||
totalUsersCount: PropTypes.number.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
|
||||||
totalUsersCount: selectors.grades.totalUsersCount(state),
|
|
||||||
filteredUsersCount: selectors.grades.filteredUsersCount(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(FilteredUsersLabel);
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import { FilteredUsersLabel, mapStateToProps } from './FilteredUsersLabel';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({
|
|
||||||
Icon: () => 'Icon',
|
|
||||||
}));
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
grades: {
|
|
||||||
filteredUsersCount: state => ({ filteredUsersCount: state }),
|
|
||||||
totalUsersCount: state => ({ totalUsersCount: state }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('FilteredUsersLabel', () => {
|
|
||||||
describe('component', () => {
|
|
||||||
const props = {
|
|
||||||
filteredUsersCount: 23,
|
|
||||||
totalUsersCount: 140,
|
|
||||||
};
|
|
||||||
it('does not render if totalUsersCount is falsey', () => {
|
|
||||||
expect(shallow(<FilteredUsersLabel {...props} totalUsersCount={0} />)).toEqual({});
|
|
||||||
});
|
|
||||||
test('snapshot - displays label with number of filtered users out of total', () => {
|
|
||||||
expect(shallow(<FilteredUsersLabel {...props} />)).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
const testState = { a: 'nice', day: 'for', some: 'rain' };
|
|
||||||
let mapped;
|
|
||||||
beforeEach(() => {
|
|
||||||
mapped = mapStateToProps(testState);
|
|
||||||
});
|
|
||||||
test('filteredUsersCount from grades.filteredUsersCount', () => {
|
|
||||||
expect(mapped.filteredUsersCount).toEqual(selectors.grades.filteredUsersCount(testState));
|
|
||||||
});
|
|
||||||
test('totalUsersCount from grades.totalUsersCount', () => {
|
|
||||||
expect(mapped.totalUsersCount).toEqual(selectors.grades.totalUsersCount(testState));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`FilteredUsersLabel component render snapshot 1`] = `
|
||||||
|
<format-message-function
|
||||||
|
message={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "Showing {filteredUsers} of {totalUsers} total learners",
|
||||||
|
"description": "Users visibility label",
|
||||||
|
"id": "gradebook.GradesTab.usersVisibilityLabel",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values={
|
||||||
|
Object {
|
||||||
|
"filteredUsers": <BoldText
|
||||||
|
text={100}
|
||||||
|
/>,
|
||||||
|
"totalUsers": <BoldText
|
||||||
|
text={123}
|
||||||
|
/>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
37
src/components/GradesView/FilteredUsersLabel/index.jsx
Normal file
37
src/components/GradesView/FilteredUsersLabel/index.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { selectors } from 'data/redux/hooks';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
export const BoldText = ({ text }) => (
|
||||||
|
<span className="font-weight-bold">{text}</span>
|
||||||
|
);
|
||||||
|
BoldText.propTypes = {
|
||||||
|
text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <FilteredUsersLabel />
|
||||||
|
* Simple label component displaying the filtered and total users shown
|
||||||
|
*/
|
||||||
|
export const FilteredUsersLabel = () => {
|
||||||
|
const { filteredUsersCount, totalUsersCount } = selectors.grades.useUserCounts();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
if (!totalUsersCount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return formatMessage(
|
||||||
|
messages.visibilityLabel,
|
||||||
|
{
|
||||||
|
filteredUsers: <BoldText text={filteredUsersCount} />,
|
||||||
|
totalUsers: <BoldText text={totalUsersCount} />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
FilteredUsersLabel.propTypes = {};
|
||||||
|
|
||||||
|
export default FilteredUsersLabel;
|
||||||
56
src/components/GradesView/FilteredUsersLabel/index.test.jsx
Normal file
56
src/components/GradesView/FilteredUsersLabel/index.test.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { formatMessage } from 'testUtils';
|
||||||
|
import { selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import FilteredUsersLabel, { BoldText } from '.';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
selectors: {
|
||||||
|
grades: {
|
||||||
|
useUserCounts: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const userCounts = {
|
||||||
|
filteredUsersCount: 100,
|
||||||
|
totalUsersCount: 123,
|
||||||
|
};
|
||||||
|
selectors.grades.useUserCounts.mockReturnValue(userCounts);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('FilteredUsersLabel component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
el = shallow(<FilteredUsersLabel />);
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes intl hook', () => {
|
||||||
|
expect(useIntl).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
expect(selectors.grades.useUserCounts).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('render', () => {
|
||||||
|
test('null render if totalUsersCount is 0', () => {
|
||||||
|
selectors.grades.useUserCounts.mockReturnValueOnce({
|
||||||
|
...userCounts,
|
||||||
|
totalUsersCount: 0,
|
||||||
|
});
|
||||||
|
expect(shallow(<FilteredUsersLabel />).isEmptyRender()).toEqual(true);
|
||||||
|
});
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
expect(el).toMatchObject(shallow(formatMessage(messages.visibilityLabel, {
|
||||||
|
filteredUsers: <BoldText text={userCounts.filteredUsersCount} />,
|
||||||
|
totalUsers: <BoldText text={userCounts.totalUsersCount} />,
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
11
src/components/GradesView/FilteredUsersLabel/messages.js
Normal file
11
src/components/GradesView/FilteredUsersLabel/messages.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
visibilityLabel: {
|
||||||
|
id: 'gradebook.GradesTab.usersVisibilityLabel',
|
||||||
|
defaultMessage: 'Showing {filteredUsers} of {totalUsers} total learners',
|
||||||
|
description: 'Users visibility label',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -1,14 +1,31 @@
|
|||||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { Button } from '@edx/paragon';
|
import { Button } from '@edx/paragon';
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
import { selectors, thunkActions } from 'data/redux/hooks';
|
||||||
import thunkActions from 'data/thunkActions';
|
import transforms from 'data/redux/transforms';
|
||||||
|
import * as module from './GradeButton';
|
||||||
|
|
||||||
const { subsectionGrade } = selectors.grades;
|
export const useGradeButtonData = ({ entry, subsection }) => {
|
||||||
|
const areGradesFrozen = selectors.assignmentTypes.useAreGradesFrozen();
|
||||||
|
const { gradeFormat } = selectors.grades.useGradeData();
|
||||||
|
const setModalState = thunkActions.app.useSetModalStateFromTable();
|
||||||
|
const label = transforms.grades.subsectionGrade({ gradeFormat, subsection });
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
setModalState({
|
||||||
|
userEntry: entry,
|
||||||
|
subsection,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
areGradesFrozen,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GradeButton
|
* GradeButton
|
||||||
@@ -18,38 +35,24 @@ const { subsectionGrade } = selectors.grades;
|
|||||||
* @param {object} entry - user's grade entry
|
* @param {object} entry - user's grade entry
|
||||||
* @param {object} subsection - user's subsection grade from subsection_breakdown
|
* @param {object} subsection - user's subsection grade from subsection_breakdown
|
||||||
*/
|
*/
|
||||||
export class GradeButton extends React.Component {
|
export const GradeButton = ({ entry, subsection }) => {
|
||||||
constructor(props) {
|
const {
|
||||||
super(props);
|
areGradesFrozen,
|
||||||
this.onClick = this.onClick.bind(this);
|
label,
|
||||||
}
|
onClick,
|
||||||
|
} = module.useGradeButtonData({ entry, subsection });
|
||||||
get label() {
|
return areGradesFrozen
|
||||||
return subsectionGrade[this.props.format](this.props.subsection);
|
? label
|
||||||
}
|
: (
|
||||||
|
<Button
|
||||||
onClick() {
|
variant="link"
|
||||||
this.props.setModalState({
|
className="btn-header grade-button"
|
||||||
userEntry: this.props.entry,
|
onClick={onClick}
|
||||||
subsection: this.props.subsection,
|
>
|
||||||
});
|
{label}
|
||||||
}
|
</Button>
|
||||||
|
);
|
||||||
render() {
|
};
|
||||||
return this.props.areGradesFrozen
|
|
||||||
? this.label
|
|
||||||
: (
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
className="btn-header grade-button"
|
|
||||||
onClick={this.onClick}
|
|
||||||
>
|
|
||||||
{this.label}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GradeButton.propTypes = {
|
GradeButton.propTypes = {
|
||||||
subsection: PropTypes.shape({
|
subsection: PropTypes.shape({
|
||||||
attempted: PropTypes.bool,
|
attempted: PropTypes.bool,
|
||||||
@@ -62,19 +65,6 @@ GradeButton.propTypes = {
|
|||||||
user_id: PropTypes.number,
|
user_id: PropTypes.number,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
// redux
|
|
||||||
areGradesFrozen: PropTypes.bool.isRequired,
|
|
||||||
format: PropTypes.string.isRequired,
|
|
||||||
setModalState: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
export default GradeButton;
|
||||||
areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
|
|
||||||
format: selectors.grades.gradeFormat(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mapDispatchToProps = {
|
|
||||||
setModalState: thunkActions.app.setModalStateFromTable,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(GradeButton);
|
|
||||||
|
|||||||
@@ -1,118 +1,121 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
import { Button } from '@edx/paragon';
|
import { selectors, thunkActions } from 'data/redux/hooks';
|
||||||
import selectors from 'data/selectors';
|
import transforms from 'data/redux/transforms';
|
||||||
import thunkActions from 'data/thunkActions';
|
import { keyStore } from 'utils';
|
||||||
|
|
||||||
import {
|
import * as module from './GradeButton';
|
||||||
GradeButton,
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
} from './GradeButton';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({
|
const { useGradeButtonData, default: GradeButton } = module;
|
||||||
Button: () => 'Button',
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('data/selectors', () => ({
|
jest.mock('data/redux/hooks', () => ({
|
||||||
__esModule: true,
|
selectors: {
|
||||||
default: {
|
assignmentTypes: { useAreGradesFrozen: jest.fn() },
|
||||||
assignmentTypes: {
|
|
||||||
areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })),
|
|
||||||
},
|
|
||||||
grades: {
|
grades: {
|
||||||
subsectionGrade: {
|
useGradeData: jest.fn(),
|
||||||
percent: jest.fn(subsection => ({ percent: subsection })),
|
|
||||||
},
|
|
||||||
gradeFormat: jest.fn(state => ({ gradeFormat: state })),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
thunkActions: {
|
||||||
|
app: { useSetModalStateFromTable: jest.fn() },
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
jest.mock('data/redux/transforms', () => ({
|
||||||
jest.mock('data/thunkActions', () => ({
|
grades: {
|
||||||
app: {
|
subsectionGrade: jest.fn(),
|
||||||
setModalStateFromTable: jest.fn(),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
subsection: {
|
||||||
|
attempted: false,
|
||||||
|
percent: 23,
|
||||||
|
score_possible: 32,
|
||||||
|
subsection_name: 'the things we do',
|
||||||
|
module_id: 'in potions',
|
||||||
|
},
|
||||||
|
entry: {
|
||||||
|
user_id: 2,
|
||||||
|
username: 'Jessie',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const gradeFormat = 'percent';
|
||||||
|
const setModalState = jest.fn();
|
||||||
|
const subsectionGrade = 'test-subsection-grade';
|
||||||
|
selectors.assignmentTypes.useAreGradesFrozen.mockReturnValue(false);
|
||||||
|
selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
|
||||||
|
thunkActions.app.useSetModalStateFromTable.mockReturnValue(setModalState);
|
||||||
|
transforms.grades.subsectionGrade.mockReturnValue(subsectionGrade);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
let out;
|
||||||
describe('GradeButton', () => {
|
describe('GradeButton', () => {
|
||||||
let el;
|
|
||||||
let props = {
|
|
||||||
subsection: {
|
|
||||||
attempted: false,
|
|
||||||
percent: 23,
|
|
||||||
score_possible: 32,
|
|
||||||
subsection_name: 'the things we do',
|
|
||||||
module_id: 'in potions',
|
|
||||||
},
|
|
||||||
entry: {
|
|
||||||
user_id: 2,
|
|
||||||
username: 'Jessie',
|
|
||||||
},
|
|
||||||
areGradesFrozen: false,
|
|
||||||
format: 'percent',
|
|
||||||
};
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
props = { ...props, setModalState: jest.fn() };
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
describe('component', () => {
|
describe('useGradeButton hook', () => {
|
||||||
describe('snapshots', () => {
|
beforeEach(() => {
|
||||||
test('grades are frozen', () => {
|
out = useGradeButtonData(props);
|
||||||
el = shallow(<GradeButton {...{ ...props, areGradesFrozen: true }} />);
|
});
|
||||||
const label = 'why you gotta label people?';
|
describe('behavior', () => {
|
||||||
jest.spyOn(el.instance(), 'label', 'get').mockReturnValue(label);
|
it('initializes redux hooks', () => {
|
||||||
el.instance().onClick = jest.fn().mockName('this.onClick');
|
expect(selectors.assignmentTypes.useAreGradesFrozen).toHaveBeenCalled();
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
expect(selectors.grades.useGradeData).toHaveBeenCalled();
|
||||||
expect(el.instance().render()).toEqual(label);
|
expect(transforms.grades.subsectionGrade).toHaveBeenCalledWith({
|
||||||
});
|
gradeFormat,
|
||||||
test('grades are not frozen', () => {
|
subsection: props.subsection,
|
||||||
el = shallow(<GradeButton {...props} />);
|
});
|
||||||
const label = 'why you gotta label people?';
|
expect(thunkActions.app.useSetModalStateFromTable).toHaveBeenCalled();
|
||||||
jest.spyOn(el.instance(), 'label', 'get').mockReturnValue(label);
|
|
||||||
el.instance().onClick = jest.fn().mockName('this.onClick');
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
expect(el.instance().render().props.children).toEqual(label);
|
|
||||||
expect(el.render().is(Button)).toEqual(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('label', () => {
|
describe('output', () => {
|
||||||
it('calls the appropriate formatter with the subsection prop', () => {
|
test('forwards areGradesFrozen from redux hook', () => {
|
||||||
el = shallow(<GradeButton {...props} />);
|
expect(out.areGradesFrozen).toEqual(false);
|
||||||
expect(
|
|
||||||
el.instance().label,
|
|
||||||
).toEqual(selectors.grades.subsectionGrade[props.format](props.subsection));
|
|
||||||
});
|
});
|
||||||
});
|
test('label passed from subsection grade redux hook', () => {
|
||||||
describe('onClick', () => {
|
expect(out.label).toEqual(subsectionGrade);
|
||||||
it('calls props.setModalState with userEntry and subsection', () => {
|
});
|
||||||
el = shallow(<GradeButton {...props} />);
|
test('onClick sets modal state with user entry and subsection', () => {
|
||||||
el.instance().onClick();
|
out.onClick();
|
||||||
expect(props.setModalState).toHaveBeenCalledWith({
|
expect(setModalState).toHaveBeenCalledWith({
|
||||||
userEntry: props.entry,
|
userEntry: props.entry,
|
||||||
subsection: props.subsection,
|
subsection: props.subsection,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('mapStateToProps', () => {
|
describe('component', () => {
|
||||||
let mapped;
|
let hookSpy;
|
||||||
const testState = { teams: { rocket: ['jesse', 'james'] } };
|
const moduleKeys = keyStore(module);
|
||||||
|
const hookProps = {
|
||||||
|
areGradesFrozen: false,
|
||||||
|
label: 'test-label',
|
||||||
|
onClick: jest.fn().mockName('hooks.onClick'),
|
||||||
|
};
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mapped = mapStateToProps(testState);
|
hookSpy = jest.spyOn(module, moduleKeys.useGradeButtonData);
|
||||||
});
|
});
|
||||||
test('areGradesFrozen form assignmentTypes.areGradesFrozen', () => {
|
describe('frozen grades', () => {
|
||||||
expect(
|
beforeEach(() => {
|
||||||
mapped.areGradesFrozen,
|
hookSpy.mockReturnValue({ ...hookProps, areGradesFrozen: true });
|
||||||
).toEqual(selectors.assignmentTypes.areGradesFrozen(testState));
|
el = shallow(<GradeButton {...props} />);
|
||||||
|
});
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
expect(el.text()).toEqual(hookProps.label);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
test('format form grades.format', () => {
|
describe('not frozen grades', () => {
|
||||||
expect(mapped.format).toEqual(selectors.grades.gradeFormat(testState));
|
beforeEach(() => {
|
||||||
});
|
hookSpy.mockReturnValue(hookProps);
|
||||||
});
|
el = shallow(<GradeButton {...props} />);
|
||||||
describe('mapDispatchToProps', () => {
|
});
|
||||||
test('setModalState from thunkActions.app.setModalStateFromTable', () => {
|
test('snapshot', () => {
|
||||||
expect(mapDispatchToProps.setModalState).toEqual(thunkActions.app.setModalStateFromTable);
|
expect(el).toMatchSnapshot();
|
||||||
|
expect(el.type()).toEqual('Button');
|
||||||
|
expect(el.props().onClick).toEqual(hookProps.onClick);
|
||||||
|
expect(el.contains(hookProps.label)).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { StrictDict } from 'utils';
|
import { useIntl, getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
OverlayTrigger,
|
OverlayTrigger,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
import { FormattedMessage, getLocale, isRtl } from '@edx/frontend-platform/i18n';
|
|
||||||
|
import { StrictDict } from 'utils';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
@@ -18,32 +18,33 @@ export const totalGradePercentageMessage = 'Total Grade values are always displa
|
|||||||
* Total Grade column header.
|
* Total Grade column header.
|
||||||
* displays an overlay tooltip with screen-reader text to indicate total grade percentage
|
* displays an overlay tooltip with screen-reader text to indicate total grade percentage
|
||||||
*/
|
*/
|
||||||
const TotalGradeLabelReplacement = () => (
|
const TotalGradeLabelReplacement = () => {
|
||||||
<div>
|
const { formatMessage } = useIntl();
|
||||||
<OverlayTrigger
|
return (
|
||||||
trigger={['hover', 'focus']}
|
<div>
|
||||||
key="left-basic"
|
<OverlayTrigger
|
||||||
placement={isRtl(getLocale()) ? 'right' : 'left'}
|
trigger={['hover', 'focus']}
|
||||||
overlay={(
|
key="left-basic"
|
||||||
<Tooltip id="course-grade-tooltip">
|
placement={isRtl(getLocale()) ? 'right' : 'left'}
|
||||||
<FormattedMessage {...messages.totalGradePercentage} />
|
overlay={(
|
||||||
</Tooltip>
|
<Tooltip id="course-grade-tooltip">
|
||||||
)}
|
{formatMessage(messages.totalGradePercentage)}
|
||||||
>
|
</Tooltip>
|
||||||
<div>
|
)}
|
||||||
<FormattedMessage {...messages.totalGradeHeading} />
|
>
|
||||||
<div id="courseGradeTooltipIcon">
|
<div>
|
||||||
<Icon
|
{formatMessage(messages.totalGradeHeading)}
|
||||||
className="fa fa-info-circle"
|
<div id="courseGradeTooltipIcon">
|
||||||
screenReaderText={(
|
<Icon
|
||||||
<FormattedMessage {...messages.totalGradePercentage} />
|
className="fa fa-info-circle"
|
||||||
)}
|
screenReaderText={formatMessage(messages.totalGradePercentage)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</OverlayTrigger>
|
||||||
</OverlayTrigger>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asterisk to display next to heading labels that are only used for masters students
|
* Asterisk to display next to heading labels that are only used for masters students
|
||||||
@@ -56,28 +57,34 @@ const mastersOnlyFieldAsterisk = (
|
|||||||
* <UsernameLabelReplacement />
|
* <UsernameLabelReplacement />
|
||||||
* Username column header. Lists that Student Key is possibly available
|
* Username column header. Lists that Student Key is possibly available
|
||||||
*/
|
*/
|
||||||
const UsernameLabelReplacement = () => (
|
const UsernameLabelReplacement = () => {
|
||||||
<div>
|
const { formatMessage } = useIntl();
|
||||||
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage {...messages.usernameHeading} />
|
<div>
|
||||||
|
{formatMessage(messages.usernameHeading)}
|
||||||
|
</div>
|
||||||
|
<div className="font-weight-normal student-key">
|
||||||
|
{formatMessage(messages.studentKeyLabel)}
|
||||||
|
{ mastersOnlyFieldAsterisk }
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-weight-normal student-key">
|
);
|
||||||
<FormattedMessage {...messages.studentKeyLabel} />
|
};
|
||||||
{ mastersOnlyFieldAsterisk }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <MastersOnlyLabelReplacement {message}>
|
* <MastersOnlyLabelReplacement {message}>
|
||||||
* Column header for fields that are only available for masters students
|
* Column header for fields that are only available for masters students
|
||||||
*/
|
*/
|
||||||
const MastersOnlyLabelReplacement = (message) => (
|
const MastersOnlyLabelReplacement = (message) => {
|
||||||
<div>
|
const { formatMessage } = useIntl();
|
||||||
<FormattedMessage {...message} />
|
return (
|
||||||
{ mastersOnlyFieldAsterisk }
|
<div>
|
||||||
</div>
|
{formatMessage(message)}
|
||||||
);
|
{ mastersOnlyFieldAsterisk }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default StrictDict({
|
export default StrictDict({
|
||||||
TotalGradeLabelReplacement,
|
TotalGradeLabelReplacement,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`GradeButton component snapshots grades are frozen 1`] = `"why you gotta label people?"`;
|
exports[`GradeButton component frozen grades snapshot 1`] = `"test-label"`;
|
||||||
|
|
||||||
exports[`GradeButton component snapshots grades are not frozen 1`] = `
|
exports[`GradeButton component not frozen grades snapshot 1`] = `
|
||||||
<Button
|
<Button
|
||||||
className="btn-header grade-button"
|
className="btn-header grade-button"
|
||||||
onClick={[MockFunction this.onClick]}
|
onClick={[MockFunction hooks.onClick]}
|
||||||
variant="link"
|
variant="link"
|
||||||
>
|
>
|
||||||
why you gotta label people?
|
test-label
|
||||||
</Button>
|
</Button>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -2,11 +2,7 @@
|
|||||||
|
|
||||||
exports[`LabelReplacements MastersOnlyLabelReplacement snapshot 1`] = `
|
exports[`LabelReplacements MastersOnlyLabelReplacement snapshot 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage
|
defaultMessAge
|
||||||
defaultMessage="defaultMessAge"
|
|
||||||
description="desCripTion"
|
|
||||||
id="id"
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
className="font-weight-normal"
|
className="font-weight-normal"
|
||||||
>
|
>
|
||||||
@@ -19,11 +15,7 @@ exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
id="course-grade-tooltip"
|
id="course-grade-tooltip"
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
Total Grade values are always displayed as a percentage
|
||||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
|
||||||
description="Gradebook table message that total grades are displayed in percent format"
|
|
||||||
id="gradebook.GradesView.table.totalGradePercentage"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -35,11 +27,7 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
id="course-grade-tooltip"
|
id="course-grade-tooltip"
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
Total Grade values are always displayed as a percentage
|
||||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
|
||||||
description="Gradebook table message that total grades are displayed in percent format"
|
|
||||||
id="gradebook.GradesView.table.totalGradePercentage"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
placement="left"
|
placement="left"
|
||||||
@@ -51,23 +39,13 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage
|
Total Grade (%)
|
||||||
defaultMessage="Total Grade (%)"
|
|
||||||
description="Gradebook table total grade column header"
|
|
||||||
id="gradebook.GradesView.table.headings.totalGrade"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
id="courseGradeTooltipIcon"
|
id="courseGradeTooltipIcon"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className="fa fa-info-circle"
|
className="fa fa-info-circle"
|
||||||
screenReaderText={
|
screenReaderText="Total Grade values are always displayed as a percentage"
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
|
||||||
description="Gradebook table message that total grades are displayed in percent format"
|
|
||||||
id="gradebook.GradesView.table.totalGradePercentage"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,20 +56,12 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
|
|||||||
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
|
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage
|
Username
|
||||||
defaultMessage="Username"
|
|
||||||
description="Gradebook table username column header"
|
|
||||||
id="gradebook.GradesView.table.headings.username"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="font-weight-normal student-key"
|
className="font-weight-normal student-key"
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
Student Key
|
||||||
defaultMessage="Student Key"
|
|
||||||
description="Gradebook table Student Key label"
|
|
||||||
id="gradebook.GradesView.table.labels.studentKey"
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
className="font-weight-normal"
|
className="font-weight-normal"
|
||||||
>
|
>
|
||||||
@@ -109,11 +79,7 @@ exports[`snapshot left to right overlay placement 1`] = `
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
id="course-grade-tooltip"
|
id="course-grade-tooltip"
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
Total Grade values are always displayed as a percentage
|
||||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
|
||||||
description="Gradebook table message that total grades are displayed in percent format"
|
|
||||||
id="gradebook.GradesView.table.totalGradePercentage"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
placement="right"
|
placement="right"
|
||||||
@@ -125,23 +91,13 @@ exports[`snapshot left to right overlay placement 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage
|
Total Grade (%)
|
||||||
defaultMessage="Total Grade (%)"
|
|
||||||
description="Gradebook table total grade column header"
|
|
||||||
id="gradebook.GradesView.table.headings.totalGrade"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
id="courseGradeTooltipIcon"
|
id="courseGradeTooltipIcon"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className="fa fa-info-circle"
|
className="fa fa-info-circle"
|
||||||
screenReaderText={
|
screenReaderText="Total Grade values are always displayed as a percentage"
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
|
||||||
description="Gradebook table message that total grades are displayed in percent format"
|
|
||||||
id="gradebook.GradesView.table.totalGradePercentage"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,11 +113,7 @@ exports[`snapshot right to left overlay placement 1`] = `
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
id="course-grade-tooltip"
|
id="course-grade-tooltip"
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
Total Grade values are always displayed as a percentage
|
||||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
|
||||||
description="Gradebook table message that total grades are displayed in percent format"
|
|
||||||
id="gradebook.GradesView.table.totalGradePercentage"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
placement="left"
|
placement="left"
|
||||||
@@ -173,23 +125,13 @@ exports[`snapshot right to left overlay placement 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage
|
Total Grade (%)
|
||||||
defaultMessage="Total Grade (%)"
|
|
||||||
description="Gradebook table total grade column header"
|
|
||||||
id="gradebook.GradesView.table.headings.totalGrade"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
id="courseGradeTooltipIcon"
|
id="courseGradeTooltipIcon"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className="fa fa-info-circle"
|
className="fa fa-info-circle"
|
||||||
screenReaderText={
|
screenReaderText="Total Grade values are always displayed as a percentage"
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Total Grade values are always displayed as a percentage"
|
|
||||||
description="Gradebook table message that total grades are displayed in percent format"
|
|
||||||
id="gradebook.GradesView.table.totalGradePercentage"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`GradebookTable snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
className="gradebook-container"
|
||||||
|
>
|
||||||
|
<DataTable
|
||||||
|
RowStatusComponent={[MockFunction hooks.nullMethod]}
|
||||||
|
columns={
|
||||||
|
Array [
|
||||||
|
"some",
|
||||||
|
"columns",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
data={
|
||||||
|
Array [
|
||||||
|
"some",
|
||||||
|
"data",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
hasFixedColumnWidths={true}
|
||||||
|
itemCount={3}
|
||||||
|
rowHeaderColumnKey="username"
|
||||||
|
>
|
||||||
|
<DataTable.TableControlBar />
|
||||||
|
<DataTable.Table />
|
||||||
|
<DataTable.EmptyTable
|
||||||
|
content="empty-table-content"
|
||||||
|
/>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`GradebookTable component snapshot - fields1 and 2 between email and totalGrade, mocked rows 1`] = `
|
|
||||||
<div
|
|
||||||
className="gradebook-container"
|
|
||||||
>
|
|
||||||
<DataTable
|
|
||||||
RowStatusComponent={[MockFunction this.nullMethod]}
|
|
||||||
columns={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"Header": <UsernameLabelReplacement />,
|
|
||||||
"accessor": "Username",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"Header": <MastersOnlyLabelReplacement
|
|
||||||
defaultMessage="Full Name"
|
|
||||||
description="Gradebook table full name column header"
|
|
||||||
id="gradebook.GradesView.table.headings.fullName"
|
|
||||||
/>,
|
|
||||||
"accessor": "Full Name",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"Header": <MastersOnlyLabelReplacement
|
|
||||||
defaultMessage="Email"
|
|
||||||
description="Gradebook table email column header"
|
|
||||||
id="gradebook.GradesView.table.headings.email"
|
|
||||||
/>,
|
|
||||||
"accessor": "Email",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"Header": "field1",
|
|
||||||
"accessor": "field1",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"Header": "field2",
|
|
||||||
"accessor": "field2",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"Header": <TotalGradeLabelReplacement />,
|
|
||||||
"accessor": "Total Grade (%)",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
data={
|
|
||||||
Array [
|
|
||||||
"mappedRow: 1",
|
|
||||||
"mappedRow: 2",
|
|
||||||
"mappedRow: 3",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
hasFixedColumnWidths={true}
|
|
||||||
itemCount={3}
|
|
||||||
rowHeaderColumnKey="username"
|
|
||||||
>
|
|
||||||
<DataTable.TableControlBar />
|
|
||||||
<DataTable.Table />
|
|
||||||
<DataTable.EmptyTable
|
|
||||||
content="No results found"
|
|
||||||
/>
|
|
||||||
</DataTable>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
61
src/components/GradesView/GradebookTable/hooks.jsx
Normal file
61
src/components/GradesView/GradebookTable/hooks.jsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { selectors } from 'data/redux/hooks';
|
||||||
|
import transforms from 'data/redux/transforms';
|
||||||
|
import { Headings } from 'data/constants/grades';
|
||||||
|
import { getLocalizedPercentSign } from 'i18n/utils';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
import Fields from './Fields';
|
||||||
|
import LabelReplacements from './LabelReplacements';
|
||||||
|
import GradeButton from './GradeButton';
|
||||||
|
|
||||||
|
const { roundGrade } = transforms.grades;
|
||||||
|
|
||||||
|
export const useGradebookTableData = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const grades = selectors.grades.useAllGrades();
|
||||||
|
const headings = selectors.root.useGetHeadings();
|
||||||
|
|
||||||
|
const mapHeaders = (heading) => {
|
||||||
|
let label;
|
||||||
|
if (heading === Headings.totalGrade) {
|
||||||
|
label = <LabelReplacements.TotalGradeLabelReplacement />;
|
||||||
|
} else if (heading === Headings.username) {
|
||||||
|
label = <LabelReplacements.UsernameLabelReplacement />;
|
||||||
|
} else if (heading === Headings.email) {
|
||||||
|
label = <LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />;
|
||||||
|
} else if (heading === Headings.fullName) {
|
||||||
|
label = <LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />;
|
||||||
|
} else {
|
||||||
|
label = heading;
|
||||||
|
}
|
||||||
|
return { Header: label, accessor: heading };
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapRows = entry => ({
|
||||||
|
[Headings.username]: (
|
||||||
|
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
|
||||||
|
),
|
||||||
|
[Headings.email]: (<Fields.Email email={entry.email} />),
|
||||||
|
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
|
||||||
|
...entry.section_breakdown.reduce((acc, subsection) => ({
|
||||||
|
...acc,
|
||||||
|
[subsection.label]: <GradeButton {...{ entry, subsection }} />,
|
||||||
|
}), {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const nullMethod = () => null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns: headings.map(mapHeaders),
|
||||||
|
data: grades.map(mapRows),
|
||||||
|
grades,
|
||||||
|
nullMethod,
|
||||||
|
emptyContent: formatMessage(messages.noResultsFound),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGradebookTableData;
|
||||||
192
src/components/GradesView/GradebookTable/hooks.test.jsx
Normal file
192
src/components/GradesView/GradebookTable/hooks.test.jsx
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { formatMessage } from 'testUtils';
|
||||||
|
|
||||||
|
import { getLocalizedPercentSign } from 'i18n/utils';
|
||||||
|
import { selectors } from 'data/redux/hooks';
|
||||||
|
import transforms from 'data/redux/transforms';
|
||||||
|
import { Headings } from 'data/constants/grades';
|
||||||
|
import LabelReplacements from './LabelReplacements';
|
||||||
|
import Fields from './Fields';
|
||||||
|
import GradeButton from './GradeButton';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
import useGradebookTableData from './hooks';
|
||||||
|
|
||||||
|
jest.mock('i18n/utils', () => ({
|
||||||
|
getLocalizedPercentSign: () => '%',
|
||||||
|
}));
|
||||||
|
jest.mock('./GradeButton', () => 'GradeButton');
|
||||||
|
jest.mock('./Fields', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||||
|
Username: 'Fields.Username',
|
||||||
|
Email: 'Fields.Email',
|
||||||
|
}));
|
||||||
|
jest.mock('./LabelReplacements', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||||
|
TotalGradeLabelReplacement: 'LabelReplacements.TotalGradeLabelReplacement',
|
||||||
|
UsernameLabelReplacement: 'LabelReplacements.UsernameLabelReplacement',
|
||||||
|
MastersOnlyLabelReplacement: 'LabelReplacements.MastersOnlyLabelReplacement',
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
selectors: {
|
||||||
|
grades: { useAllGrades: jest.fn() },
|
||||||
|
root: { useGetHeadings: jest.fn() },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
jest.mock('data/redux/transforms', () => ({
|
||||||
|
grades: { roundGrade: jest.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const roundGrade = grade => grade * 20;
|
||||||
|
transforms.grades.roundGrade.mockImplementation(roundGrade);
|
||||||
|
|
||||||
|
const subsectionLabels = [
|
||||||
|
'subsectionLabel1',
|
||||||
|
'subsectionLabel2',
|
||||||
|
'subsectionLabel3',
|
||||||
|
];
|
||||||
|
|
||||||
|
const allGrades = [
|
||||||
|
{
|
||||||
|
username: 'test-username-1',
|
||||||
|
external_user_key: 'EKey1',
|
||||||
|
email: 'email-1',
|
||||||
|
fullName: 'test-fullNAME',
|
||||||
|
percent: 0.9,
|
||||||
|
section_breakdown: [
|
||||||
|
{ label: subsectionLabels[0] },
|
||||||
|
{ label: subsectionLabels[1] },
|
||||||
|
{ label: subsectionLabels[2] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: 'test-username-2',
|
||||||
|
external_user_key: 'EKey2',
|
||||||
|
email: 'email-2',
|
||||||
|
percent: 0.8,
|
||||||
|
section_breakdown: [
|
||||||
|
{ label: subsectionLabels[0] },
|
||||||
|
{ label: subsectionLabels[1] },
|
||||||
|
{ label: subsectionLabels[2] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: 'test-username-3',
|
||||||
|
external_user_key: 'EKey3',
|
||||||
|
email: 'email-3',
|
||||||
|
percent: 0.6,
|
||||||
|
section_breakdown: [
|
||||||
|
{ label: subsectionLabels[0] },
|
||||||
|
{ label: subsectionLabels[1] },
|
||||||
|
{ label: subsectionLabels[2] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const testHeading = 'test-heading-value';
|
||||||
|
const headings = [
|
||||||
|
Headings.totalGrade,
|
||||||
|
Headings.username,
|
||||||
|
Headings.email,
|
||||||
|
Headings.fullName,
|
||||||
|
testHeading,
|
||||||
|
];
|
||||||
|
selectors.grades.useAllGrades.mockReturnValue(allGrades);
|
||||||
|
selectors.root.useGetHeadings.mockReturnValue(headings);
|
||||||
|
|
||||||
|
let out;
|
||||||
|
describe('useGradebookTableData', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
out = useGradebookTableData();
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes intl hook', () => {
|
||||||
|
expect(useIntl).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
expect(selectors.grades.useAllGrades).toHaveBeenCalled();
|
||||||
|
expect(selectors.root.useGetHeadings).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('output', () => {
|
||||||
|
describe('columns', () => {
|
||||||
|
test('total grade heading produces TotalGradeLabelReplacement label', () => {
|
||||||
|
const { Header, accessor } = out.columns[0];
|
||||||
|
expect(accessor).toEqual(headings[0]);
|
||||||
|
expect(shallow(Header)).toMatchObject(
|
||||||
|
shallow(<LabelReplacements.TotalGradeLabelReplacement />),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('username heading produces UsernameLabelReplacement', () => {
|
||||||
|
const { Header, accessor } = out.columns[1];
|
||||||
|
expect(accessor).toEqual(headings[1]);
|
||||||
|
expect(shallow(Header)).toMatchObject(
|
||||||
|
shallow(<LabelReplacements.UsernameLabelReplacement />),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('email heading replaces with email heading message', () => {
|
||||||
|
const { Header, accessor } = out.columns[2];
|
||||||
|
expect(accessor).toEqual(headings[2]);
|
||||||
|
expect(shallow(Header)).toMatchObject(
|
||||||
|
shallow(<LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('fullName heading replaces with fullName heading message', () => {
|
||||||
|
const { Header, accessor } = out.columns[3];
|
||||||
|
expect(accessor).toEqual(headings[3]);
|
||||||
|
expect(shallow(Header)).toMatchObject(
|
||||||
|
shallow(<LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('other headings are passed through', () => {
|
||||||
|
const { Header, accessor } = out.columns[4];
|
||||||
|
expect(accessor).toEqual(headings[4]);
|
||||||
|
expect(Header).toEqual(headings[4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('data', () => {
|
||||||
|
test('username field', () => {
|
||||||
|
allGrades.forEach((entry, index) => {
|
||||||
|
expect(out.data[index][Headings.username]).toMatchObject(
|
||||||
|
<Fields.Username username={entry.username} userKey={entry.external_user_key} />,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('email field', () => {
|
||||||
|
allGrades.forEach((entry, index) => {
|
||||||
|
expect(out.data[index][Headings.email]).toMatchObject(
|
||||||
|
<Fields.Email email={entry.email} />,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('totalGrade field', () => {
|
||||||
|
allGrades.forEach((entry, index) => {
|
||||||
|
expect(out.data[index][Headings.totalGrade]).toEqual(
|
||||||
|
`${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('section breakdown', () => {
|
||||||
|
allGrades.forEach((entry, gradeIndex) => {
|
||||||
|
subsectionLabels.forEach((label, labelIndex) => {
|
||||||
|
expect(out.data[gradeIndex][label]).toMatchObject(
|
||||||
|
<GradeButton entry={entry} subsection={entry.section_breakdown[labelIndex]} />,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('forwards grades from redux', () => {
|
||||||
|
expect(out.grades).toEqual(allGrades);
|
||||||
|
});
|
||||||
|
test('nullMethod returns null', () => {
|
||||||
|
expect(out.nullMethod()).toEqual(null);
|
||||||
|
});
|
||||||
|
test('emptyContent', () => {
|
||||||
|
expect(out.emptyContent).toEqual(formatMessage(messages.noResultsFound));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,21 +1,8 @@
|
|||||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { DataTable } from '@edx/paragon';
|
import { DataTable } from '@edx/paragon';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
import useGradebookTableData from './hooks';
|
||||||
import { Headings } from 'data/constants/grades';
|
|
||||||
import { getLocalizedPercentSign } from 'i18n/utils';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import Fields from './Fields';
|
|
||||||
import LabelReplacements from './LabelReplacements';
|
|
||||||
import GradeButton from './GradeButton';
|
|
||||||
|
|
||||||
const { roundGrade } = selectors.grades;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <GraebookTable />
|
* <GraebookTable />
|
||||||
@@ -23,96 +10,33 @@ const { roundGrade } = selectors.grades;
|
|||||||
* a row for each user, with a column for their username, email, and total grade,
|
* a row for each user, with a column for their username, email, and total grade,
|
||||||
* along with one for each subsection in their grade entry.
|
* along with one for each subsection in their grade entry.
|
||||||
*/
|
*/
|
||||||
export class GradebookTable extends React.Component {
|
export const GradebookTable = () => {
|
||||||
constructor(props) {
|
const {
|
||||||
super(props);
|
columns,
|
||||||
this.mapHeaders = this.mapHeaders.bind(this);
|
data,
|
||||||
this.mapRows = this.mapRows.bind(this);
|
grades,
|
||||||
this.nullMethod = this.nullMethod.bind(this);
|
nullMethod,
|
||||||
}
|
emptyContent,
|
||||||
|
} = useGradebookTableData();
|
||||||
|
|
||||||
mapHeaders(heading) {
|
return (
|
||||||
let label;
|
<div className="gradebook-container">
|
||||||
if (heading === Headings.totalGrade) {
|
<DataTable
|
||||||
label = <LabelReplacements.TotalGradeLabelReplacement />;
|
columns={columns}
|
||||||
} else if (heading === Headings.username) {
|
data={data}
|
||||||
label = <LabelReplacements.UsernameLabelReplacement />;
|
rowHeaderColumnKey="username"
|
||||||
} else if (heading === Headings.email) {
|
hasFixedColumnWidths
|
||||||
label = <LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />;
|
itemCount={grades.length}
|
||||||
} else if (heading === Headings.fullName) {
|
RowStatusComponent={nullMethod}
|
||||||
label = <LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />;
|
>
|
||||||
} else {
|
<DataTable.TableControlBar />
|
||||||
label = heading;
|
<DataTable.Table />
|
||||||
}
|
<DataTable.EmptyTable content={emptyContent} />
|
||||||
return { Header: label, accessor: heading };
|
</DataTable>
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
mapRows = entry => ({
|
|
||||||
[Headings.username]: (
|
|
||||||
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
|
|
||||||
),
|
|
||||||
[Headings.fullName]: (<Fields.Text value={entry.full_name} />),
|
|
||||||
[Headings.email]: (<Fields.Text value={entry.email} />),
|
|
||||||
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
|
|
||||||
...entry.section_breakdown.reduce((acc, subsection) => ({
|
|
||||||
...acc,
|
|
||||||
[subsection.label]: <GradeButton {...{ entry, subsection }} />,
|
|
||||||
}), {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
nullMethod() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="gradebook-container">
|
|
||||||
<DataTable
|
|
||||||
columns={this.props.headings.map(this.mapHeaders)}
|
|
||||||
data={this.props.grades.map(this.mapRows)}
|
|
||||||
rowHeaderColumnKey="username"
|
|
||||||
hasFixedColumnWidths
|
|
||||||
itemCount={this.props.grades.length}
|
|
||||||
RowStatusComponent={this.nullMethod}
|
|
||||||
>
|
|
||||||
<DataTable.TableControlBar />
|
|
||||||
<DataTable.Table />
|
|
||||||
<DataTable.EmptyTable content={this.props.intl.formatMessage(messages.noResultsFound)} />
|
|
||||||
</DataTable>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GradebookTable.defaultProps = {
|
|
||||||
grades: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
GradebookTable.propTypes = {
|
GradebookTable.propTypes = {};
|
||||||
// redux
|
|
||||||
grades: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
percent: PropTypes.number,
|
|
||||||
section_breakdown: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
attempted: PropTypes.bool,
|
|
||||||
category: PropTypes.string,
|
|
||||||
label: PropTypes.string,
|
|
||||||
module_id: PropTypes.string,
|
|
||||||
percent: PropTypes.number,
|
|
||||||
scoreEarned: PropTypes.number,
|
|
||||||
scorePossible: PropTypes.number,
|
|
||||||
subsection_name: PropTypes.string,
|
|
||||||
})),
|
|
||||||
user_id: PropTypes.number,
|
|
||||||
user_name: PropTypes.string,
|
|
||||||
})),
|
|
||||||
headings: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
// injected
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
export default GradebookTable;
|
||||||
grades: selectors.grades.allGrades(state),
|
|
||||||
headings: selectors.root.getHeadings(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps)(GradebookTable));
|
|
||||||
|
|||||||
39
src/components/GradesView/GradebookTable/index.test.jsx
Normal file
39
src/components/GradesView/GradebookTable/index.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { DataTable } from '@edx/paragon';
|
||||||
|
|
||||||
|
import useGradebookTableData from './hooks';
|
||||||
|
import GradebookTable from '.';
|
||||||
|
|
||||||
|
jest.mock('./hooks', () => jest.fn());
|
||||||
|
|
||||||
|
const hookProps = {
|
||||||
|
columns: ['some', 'columns'],
|
||||||
|
data: ['some', 'data'],
|
||||||
|
grades: ['a', 'few', 'grades'],
|
||||||
|
nullMethod: jest.fn().mockName('hooks.nullMethod'),
|
||||||
|
emptyContent: 'empty-table-content',
|
||||||
|
};
|
||||||
|
useGradebookTableData.mockReturnValue(hookProps);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('GradebookTable', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
el = shallow(<GradebookTable />);
|
||||||
|
});
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
test('Datatable props', () => {
|
||||||
|
const datatable = el.find(DataTable);
|
||||||
|
const props = datatable.props();
|
||||||
|
expect(props.columns).toEqual(hookProps.columns);
|
||||||
|
expect(props.data).toEqual(hookProps.data);
|
||||||
|
expect(props.itemCount).toEqual(hookProps.grades.length);
|
||||||
|
expect(props.RowStatusComponent).toEqual(hookProps.nullMethod);
|
||||||
|
expect(datatable.children().at(2).type()).toEqual('DataTable.EmptyTable');
|
||||||
|
expect(datatable.children().at(2).props().content).toEqual(hookProps.emptyContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import { DataTable } from '@edx/paragon';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import { Headings } from 'data/constants/grades';
|
|
||||||
import LabelReplacements from './LabelReplacements';
|
|
||||||
import Fields from './Fields';
|
|
||||||
import messages from './messages';
|
|
||||||
import { GradebookTable, mapStateToProps } from '.';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
|
|
||||||
DataTable: {
|
|
||||||
Table: 'DataTable.Table',
|
|
||||||
TableControlBar: 'DataTable.TableControlBar',
|
|
||||||
EmptyTable: 'DataTable.EmptyTable',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('./Fields', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
Username: () => 'Fields.Username',
|
|
||||||
Text: () => 'Fields.Text',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('./LabelReplacements', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
TotalGradeLabelReplacement: () => 'TotalGradeLabelReplacement',
|
|
||||||
UsernameLabelReplacement: () => 'UsernameLabelReplacement',
|
|
||||||
MastersOnlyLabelReplacement: () => 'MastersOnlyLabelReplacement',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('./GradeButton', () => 'GradeButton');
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
grades: {
|
|
||||||
roundGrade: jest.fn(grade => `roundedGrade: ${grade}`),
|
|
||||||
allGrades: jest.fn(state => ({ allGrades: state })),
|
|
||||||
},
|
|
||||||
root: {
|
|
||||||
getHeadings: jest.fn(state => ({ getHeadings: state })),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
describe('GradebookTable', () => {
|
|
||||||
describe('component', () => {
|
|
||||||
let el;
|
|
||||||
const fields = { field1: 'field1', field2: 'field2' };
|
|
||||||
const props = {
|
|
||||||
grades: [
|
|
||||||
{
|
|
||||||
percent: 1,
|
|
||||||
section_breakdown: [
|
|
||||||
{ label: fields.field1, percent: 1.2 },
|
|
||||||
{ label: fields.field2, percent: 2.3 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
percent: 2,
|
|
||||||
section_breakdown: [
|
|
||||||
{ label: fields.field1, percent: 1.2 },
|
|
||||||
{ label: fields.field2, percent: 2.3 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
percent: 3,
|
|
||||||
section_breakdown: [
|
|
||||||
{ label: fields.field1, percent: 1.2 },
|
|
||||||
{ label: fields.field2, percent: 2.3 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
headings: [
|
|
||||||
Headings.username,
|
|
||||||
Headings.fullName,
|
|
||||||
Headings.email,
|
|
||||||
fields.field1,
|
|
||||||
fields.field2,
|
|
||||||
Headings.totalGrade,
|
|
||||||
],
|
|
||||||
|
|
||||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
|
||||||
};
|
|
||||||
test('snapshot - fields1 and 2 between email and totalGrade, mocked rows', () => {
|
|
||||||
el = shallow(<GradebookTable {...props} />);
|
|
||||||
el.instance().nullMethod = jest.fn().mockName('this.nullMethod');
|
|
||||||
el.instance().mapRows = (entry) => `mappedRow: ${entry.percent}`;
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
test('null method returns null for stub component', () => {
|
|
||||||
el = shallow(<GradebookTable {...props} />);
|
|
||||||
expect(el.instance().nullMethod()).toEqual(null);
|
|
||||||
});
|
|
||||||
describe('table columns (mapHeaders)', () => {
|
|
||||||
let headings;
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<GradebookTable {...props} />);
|
|
||||||
headings = el.find(DataTable).props().columns;
|
|
||||||
});
|
|
||||||
test('username sets key and replaces Header with component', () => {
|
|
||||||
const heading = headings[0];
|
|
||||||
expect(heading.accessor).toEqual(Headings.username);
|
|
||||||
expect(heading.Header.type).toEqual(LabelReplacements.UsernameLabelReplacement);
|
|
||||||
});
|
|
||||||
test('full name sets key and Header from header', () => {
|
|
||||||
const heading = headings[1];
|
|
||||||
expect(heading.accessor).toEqual(Headings.fullName);
|
|
||||||
expect(heading.Header).toEqual(<LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />);
|
|
||||||
});
|
|
||||||
test('email sets key and Header from header', () => {
|
|
||||||
const heading = headings[2];
|
|
||||||
expect(heading.accessor).toEqual(Headings.email);
|
|
||||||
expect(heading.Header).toEqual(<LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />);
|
|
||||||
});
|
|
||||||
test('subsections set key and Header from header', () => {
|
|
||||||
expect(headings[3]).toEqual({ accessor: fields.field1, Header: fields.field1 });
|
|
||||||
expect(headings[4]).toEqual({ accessor: fields.field2, Header: fields.field2 });
|
|
||||||
});
|
|
||||||
test('totalGrade sets key and replaces Header with component', () => {
|
|
||||||
const heading = headings[5];
|
|
||||||
expect(heading.accessor).toEqual(Headings.totalGrade);
|
|
||||||
expect(heading.Header.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('table data (mapRows)', () => {
|
|
||||||
let rows;
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<GradebookTable {...props} />);
|
|
||||||
rows = el.find(DataTable).props().data;
|
|
||||||
});
|
|
||||||
describe.each([0, 1, 2])('gradeEntry($percent)', (gradeIndex) => {
|
|
||||||
let row;
|
|
||||||
const entry = props.grades[gradeIndex];
|
|
||||||
beforeEach(() => {
|
|
||||||
row = rows[gradeIndex];
|
|
||||||
});
|
|
||||||
test('username set to Username Field', () => {
|
|
||||||
const field = row[Headings.username];
|
|
||||||
expect(field.type).toEqual(Fields.Username);
|
|
||||||
expect(field.props).toEqual({
|
|
||||||
username: entry.username,
|
|
||||||
userKey: entry.external_user_key,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('fullName set to Text Field', () => {
|
|
||||||
const field = row[Headings.fullName];
|
|
||||||
expect(field.type).toEqual(Fields.Text);
|
|
||||||
expect(field.props).toEqual({ value: entry.full_name });
|
|
||||||
});
|
|
||||||
test('email set to Text Field', () => {
|
|
||||||
const field = row[Headings.email];
|
|
||||||
expect(field.type).toEqual(Fields.Text);
|
|
||||||
expect(field.props).toEqual({ value: entry.email });
|
|
||||||
});
|
|
||||||
test('totalGrade set to rounded percent grade * 100', () => {
|
|
||||||
expect(
|
|
||||||
row[Headings.totalGrade],
|
|
||||||
).toEqual(`${selectors.grades.roundGrade(entry.percent * 100)}%`);
|
|
||||||
});
|
|
||||||
test('subsections loaded as GradeButtons', () => {
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
let mapped;
|
|
||||||
const testState = {
|
|
||||||
where: 'did',
|
|
||||||
all: 'of',
|
|
||||||
these: 'bananas',
|
|
||||||
come: 'from?',
|
|
||||||
};
|
|
||||||
beforeEach(() => {
|
|
||||||
mapped = mapStateToProps(testState);
|
|
||||||
});
|
|
||||||
test('grades from grades.allGrades', () => {
|
|
||||||
expect(mapped.grades).toEqual(selectors.grades.allGrades(testState));
|
|
||||||
});
|
|
||||||
test('headings from root.getHeadings', () => {
|
|
||||||
expect(mapped.headings).toEqual(selectors.root.getHeadings(testState));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -29,7 +29,7 @@ testFormData.append('csv', testFile);
|
|||||||
const ref = {
|
const ref = {
|
||||||
current: { click: jest.fn(), files: [testFile], value: 'test-value' },
|
current: { click: jest.fn(), files: [testFile], value: 'test-value' },
|
||||||
};
|
};
|
||||||
describe('useAssignmentFilterData hook', () => {
|
describe('useImportButtonData hook', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
React.useRef.mockReturnValue(ref);
|
React.useRef.mockReturnValue(ref);
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { Toast } from '@edx/paragon';
|
|
||||||
import {
|
|
||||||
injectIntl,
|
|
||||||
intlShape,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import { views } from 'data/constants/app';
|
|
||||||
import messages from './ImportSuccessToast.messages';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <ImportSuccessToast />
|
|
||||||
* Toast component triggered by successful grade upload.
|
|
||||||
* Provides a link to view the Bulk Management History tab.
|
|
||||||
*/
|
|
||||||
export class ImportSuccessToast extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.onClose = this.onClose.bind(this);
|
|
||||||
this.handleShowHistoryView = this.handleShowHistoryView.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose() {
|
|
||||||
this.props.setShow(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleShowHistoryView() {
|
|
||||||
this.props.setAppView(views.bulkManagementHistory);
|
|
||||||
this.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Toast
|
|
||||||
action={{
|
|
||||||
label: this.props.intl.formatMessage(messages.showHistoryViewBtn),
|
|
||||||
onClick: this.handleShowHistoryView,
|
|
||||||
}}
|
|
||||||
onClose={this.onClose}
|
|
||||||
show={this.props.show}
|
|
||||||
>
|
|
||||||
{this.props.intl.formatMessage(messages.description)}
|
|
||||||
</Toast>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportSuccessToast.propTypes = {
|
|
||||||
// injected
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
// redux
|
|
||||||
show: PropTypes.bool.isRequired,
|
|
||||||
setAppView: PropTypes.func.isRequired,
|
|
||||||
setShow: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
|
||||||
show: selectors.app.showImportSuccessToast(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mapDispatchToProps = {
|
|
||||||
setAppView: actions.app.setView,
|
|
||||||
setShow: actions.app.setShowImportSuccessToast,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ImportSuccessToast));
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import { views } from 'data/constants/app';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ImportSuccessToast,
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
} from './ImportSuccessToast';
|
|
||||||
import messages from './ImportSuccessToast.messages';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({
|
|
||||||
Toast: () => 'Toast',
|
|
||||||
}));
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: {
|
|
||||||
showImportSuccessToast: (state) => ({ showImportSuccessToast: state }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('data/actions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
app: {
|
|
||||||
setView: jest.fn(),
|
|
||||||
setShow: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('ImportSuccessToast component', () => {
|
|
||||||
describe('snapshots', () => {
|
|
||||||
let el;
|
|
||||||
let props = {
|
|
||||||
show: true,
|
|
||||||
};
|
|
||||||
beforeEach(() => {
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
|
||||||
setAppView: jest.fn(),
|
|
||||||
setShow: jest.fn(),
|
|
||||||
};
|
|
||||||
el = shallow(<ImportSuccessToast {...props} />);
|
|
||||||
});
|
|
||||||
test('snapshot', () => {
|
|
||||||
el.instance().handleShowHistoryView = jest.fn().mockName('handleShowHistoryView');
|
|
||||||
el.instance().onClose = jest.fn().mockName('onClose');
|
|
||||||
expect(el).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
describe('Toast props', () => {
|
|
||||||
let toastProps;
|
|
||||||
beforeEach(() => {
|
|
||||||
toastProps = el.props();
|
|
||||||
});
|
|
||||||
test('action has translated label and onClick from this.handleShowHistoryView', () => {
|
|
||||||
expect(toastProps.action).toEqual({
|
|
||||||
label: props.intl.formatMessage(messages.showHistoryViewBtn),
|
|
||||||
onClick: el.instance().handleShowHistoryView,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('onClose from this.onClose method', () => {
|
|
||||||
expect(toastProps.onClose).toEqual(el.instance().onClose);
|
|
||||||
});
|
|
||||||
test('show from show prop', () => {
|
|
||||||
expect(toastProps.show).toEqual(props.show);
|
|
||||||
el.setProps({ show: false });
|
|
||||||
expect(el.props().show).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('onClose', () => {
|
|
||||||
it('calls props.setShow(false)', () => {
|
|
||||||
el.instance().onClose();
|
|
||||||
expect(props.setShow).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('handleShowHistoryView', () => {
|
|
||||||
it('calls setAppView with views.bulkManagementHistory and this.onClose', () => {
|
|
||||||
el.instance().onClose = jest.fn();
|
|
||||||
el.instance().handleShowHistoryView();
|
|
||||||
expect(props.setAppView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
|
||||||
expect(el.instance().onClose).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('behavior', () => {
|
|
||||||
});
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
const testState = { somewhere: 'over', the: 'rainbow' };
|
|
||||||
const mapped = mapStateToProps(testState);
|
|
||||||
test('show from app showImportSuccessToast selector', () => {
|
|
||||||
expect(mapped.show).toEqual(
|
|
||||||
selectors.app.showImportSuccessToast(testState),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapDispatchToProps', () => {
|
|
||||||
test('setAppView from actions.app.setView', () => {
|
|
||||||
expect(mapDispatchToProps.setAppView).toEqual(actions.app.setView);
|
|
||||||
});
|
|
||||||
test('setShow from actions.setShowImportSuccessToast', () => {
|
|
||||||
expect(mapDispatchToProps.setShow).toEqual(actions.app.setShowImportSuccessToast);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ImportSuccessToast component render snapshot 1`] = `
|
||||||
|
<Toast
|
||||||
|
action="test-action"
|
||||||
|
onClose={[MockFunction hooks.onClose]}
|
||||||
|
show="test-show"
|
||||||
|
>
|
||||||
|
test-description
|
||||||
|
</Toast>
|
||||||
|
`;
|
||||||
39
src/components/GradesView/ImportSuccessToast/hooks.js
Normal file
39
src/components/GradesView/ImportSuccessToast/hooks.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
import { views } from 'data/constants/app';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <ImportSuccessToast />
|
||||||
|
* Toast component triggered by successful grade upload.
|
||||||
|
* Provides a link to view the Bulk Management History tab.
|
||||||
|
*/
|
||||||
|
export const useImportSuccessToastData = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const show = selectors.app.useShowImportSuccessToast();
|
||||||
|
const setAppView = actions.app.useSetView();
|
||||||
|
const setShow = actions.app.useSetShowImportSuccessToast();
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
setShow(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowHistoryView = () => {
|
||||||
|
setAppView(views.bulkManagementHistory);
|
||||||
|
setShow(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: {
|
||||||
|
label: formatMessage(messages.showHistoryViewBtn),
|
||||||
|
onClick: handleShowHistoryView,
|
||||||
|
},
|
||||||
|
onClose,
|
||||||
|
show,
|
||||||
|
description: formatMessage(messages.description),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useImportSuccessToastData;
|
||||||
58
src/components/GradesView/ImportSuccessToast/hooks.test.js
Normal file
58
src/components/GradesView/ImportSuccessToast/hooks.test.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { formatMessage } from 'testUtils';
|
||||||
|
import { views } from 'data/constants/app';
|
||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import useImportSuccessToastData from './hooks';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
actions: {
|
||||||
|
app: {
|
||||||
|
useSetView: jest.fn(),
|
||||||
|
useSetShowImportSuccessToast: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
app: { useShowImportSuccessToast: jest.fn() },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setView = jest.fn().mockName('hooks.setView');
|
||||||
|
const setShowToast = jest.fn().mockName('hooks.setShowImportSuccessToast');
|
||||||
|
actions.app.useSetView.mockReturnValue(setView);
|
||||||
|
actions.app.useSetShowImportSuccessToast.mockReturnValue(setShowToast);
|
||||||
|
const showImportSuccessToast = 'test-show-import-success-toast';
|
||||||
|
selectors.app.useShowImportSuccessToast.mockReturnValue(showImportSuccessToast);
|
||||||
|
|
||||||
|
let out;
|
||||||
|
describe('ImportSuccessToast component', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
out = useImportSuccessToastData();
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes intl hook', () => {
|
||||||
|
expect(useIntl).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
it('initializes redux hooks', () => {
|
||||||
|
expect(selectors.app.useShowImportSuccessToast).toHaveBeenCalled();
|
||||||
|
expect(actions.app.useSetView).toHaveBeenCalled();
|
||||||
|
expect(actions.app.useSetShowImportSuccessToast).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('output', () => {
|
||||||
|
test('action label', () => {
|
||||||
|
expect(out.action.label).toEqual(formatMessage(messages.showHistoryViewBtn));
|
||||||
|
});
|
||||||
|
test('action click event', () => {
|
||||||
|
out.action.onClick();
|
||||||
|
expect(setView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||||
|
expect(setShowToast).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
test('onClose', () => {
|
||||||
|
out.onClose();
|
||||||
|
expect(setShowToast).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
28
src/components/GradesView/ImportSuccessToast/index.jsx
Normal file
28
src/components/GradesView/ImportSuccessToast/index.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Toast } from '@edx/paragon';
|
||||||
|
|
||||||
|
import useImportSuccessToastData from './hooks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <ImportSuccessToast />
|
||||||
|
* Toast component triggered by successful grade upload.
|
||||||
|
* Provides a link to view the Bulk Management History tab.
|
||||||
|
*/
|
||||||
|
export const ImportSuccessToast = () => {
|
||||||
|
const {
|
||||||
|
action,
|
||||||
|
onClose,
|
||||||
|
show,
|
||||||
|
description,
|
||||||
|
} = useImportSuccessToastData();
|
||||||
|
return (
|
||||||
|
<Toast {...{ action, onClose, show }}>
|
||||||
|
{description}
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ImportSuccessToast.propTypes = {};
|
||||||
|
|
||||||
|
export default ImportSuccessToast;
|
||||||
39
src/components/GradesView/ImportSuccessToast/index.test.jsx
Normal file
39
src/components/GradesView/ImportSuccessToast/index.test.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import useImportSuccessToastData from './hooks';
|
||||||
|
import ImportSuccessToast from '.';
|
||||||
|
|
||||||
|
jest.mock('./hooks', () => jest.fn());
|
||||||
|
|
||||||
|
const hookProps = {
|
||||||
|
action: 'test-action',
|
||||||
|
onClose: jest.fn().mockName('hooks.onClose'),
|
||||||
|
show: 'test-show',
|
||||||
|
description: 'test-description',
|
||||||
|
};
|
||||||
|
useImportSuccessToastData.mockReturnValue(hookProps);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('ImportSuccessToast component', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
el = shallow(<ImportSuccessToast />);
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes component hook', () => {
|
||||||
|
expect(useImportSuccessToastData).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('render', () => {
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
test('Toast', () => {
|
||||||
|
expect(el.type()).toEqual('Toast');
|
||||||
|
expect(el.props().action).toEqual(hookProps.action);
|
||||||
|
expect(el.props().onClose).toEqual(hookProps.onClose);
|
||||||
|
expect(el.props().show).toEqual(hookProps.show);
|
||||||
|
expect(el.text()).toEqual(hookProps.description);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import actions from 'data/actions';
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
|
|
||||||
import NetworkButton from 'components/NetworkButton';
|
|
||||||
import messages from './InterventionsReport.messages';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <InterventionsReport />
|
|
||||||
* Provides download buttons for Bulk Management and Intervention reports, only if
|
|
||||||
* showBulkManagement is set in redus.
|
|
||||||
*/
|
|
||||||
export class InterventionsReport extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick() {
|
|
||||||
this.props.downloadInterventionReport();
|
|
||||||
window.location.assign(this.props.interventionExportUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return this.props.showBulkManagement && (
|
|
||||||
<div>
|
|
||||||
<h4 className="mt-0">
|
|
||||||
<FormattedMessage {...messages.title} />
|
|
||||||
</h4>
|
|
||||||
<div
|
|
||||||
className="d-flex justify-content-between align-items-center"
|
|
||||||
>
|
|
||||||
<div className="intervention-report-description">
|
|
||||||
<FormattedMessage {...messages.description} />
|
|
||||||
</div>
|
|
||||||
<NetworkButton
|
|
||||||
label={messages.downloadBtn}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InterventionsReport.defaultProps = {
|
|
||||||
showBulkManagement: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
InterventionsReport.propTypes = {
|
|
||||||
// redux
|
|
||||||
downloadInterventionReport: PropTypes.func.isRequired,
|
|
||||||
interventionExportUrl: PropTypes.string.isRequired,
|
|
||||||
showBulkManagement: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
|
||||||
interventionExportUrl: selectors.root.interventionExportUrl(state),
|
|
||||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mapDispatchToProps = {
|
|
||||||
downloadInterventionReport: actions.grades.downloadReport.intervention,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(InterventionsReport);
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import actions from 'data/actions';
|
|
||||||
|
|
||||||
import {
|
|
||||||
InterventionsReport,
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
} from './InterventionsReport';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({
|
|
||||||
Toast: () => 'Toast',
|
|
||||||
}));
|
|
||||||
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
root: {
|
|
||||||
interventionExportUrl: (state) => ({ interventionExportUrl: state }),
|
|
||||||
showBulkManagement: (state) => ({ showBulkManagement: state }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('data/actions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
grades: {
|
|
||||||
downloadReport: { intervention: jest.fn() },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('InterventionsReport component', () => {
|
|
||||||
let el;
|
|
||||||
let props = {
|
|
||||||
interventionExportUrl: 'url.for.exporting.interventions',
|
|
||||||
showBulkManagement: true,
|
|
||||||
};
|
|
||||||
let location;
|
|
||||||
beforeAll(() => {
|
|
||||||
location = window.location;
|
|
||||||
});
|
|
||||||
beforeEach(() => {
|
|
||||||
delete window.location;
|
|
||||||
window.location = Object.defineProperties(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
...Object.getOwnPropertyDescriptors(location),
|
|
||||||
assign: { configurable: true, value: jest.fn() },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
downloadInterventionReport: jest.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
afterAll(() => {
|
|
||||||
window.location = location;
|
|
||||||
});
|
|
||||||
describe('snapshots', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<InterventionsReport {...props} />);
|
|
||||||
});
|
|
||||||
test('snapshot', () => {
|
|
||||||
el.instance().handleClick = jest.fn().mockName('handleClick');
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
test('returns empty if props.showBulkManagement is false', () => {
|
|
||||||
el.setProps({ showBulkManagement: false });
|
|
||||||
expect(el.instance().render()).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('behavior', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<InterventionsReport {...props} />);
|
|
||||||
});
|
|
||||||
describe('handleClick', () => {
|
|
||||||
it('calls props.downloadInterventionReport and navigates to props.interventionExportUrl', () => {
|
|
||||||
el.instance().handleClick();
|
|
||||||
expect(props.downloadInterventionReport).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
const testState = { somewhere: 'over', the: 'rainbow' };
|
|
||||||
const mapped = mapStateToProps(testState);
|
|
||||||
test('interventionExportUrl from root interventionExportUrl selector', () => {
|
|
||||||
expect(mapped.interventionExportUrl).toEqual(
|
|
||||||
selectors.root.interventionExportUrl(testState),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('showBulkManagement from root showBulkManagement selector', () => {
|
|
||||||
expect(mapped.showBulkManagement).toEqual(
|
|
||||||
selectors.root.showBulkManagement(testState),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapDispatchToProps', () => {
|
|
||||||
test('downloadInterventionReport from actions.grades.downloadReport.intervention', () => {
|
|
||||||
expect(mapDispatchToProps.downloadInterventionReport).toEqual(
|
|
||||||
actions.grades.downloadReport.intervention,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`InterventionsReport component output snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
<h4
|
||||||
|
className="mt-0"
|
||||||
|
>
|
||||||
|
Interventions Report
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
className="d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="intervention-report-description"
|
||||||
|
>
|
||||||
|
Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.
|
||||||
|
</div>
|
||||||
|
<NetworkButton
|
||||||
|
label={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "Download Interventions",
|
||||||
|
"description": "The labeled button to download the Intervention report from the Grades View",
|
||||||
|
"id": "gradebook.GradesView.InterventionsReport.downloadBtn",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClick={[MockFunction]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
19
src/components/GradesView/InterventionsReport/hooks.js
Normal file
19
src/components/GradesView/InterventionsReport/hooks.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
const useInterventionsReportData = () => {
|
||||||
|
const interventionExportUrl = selectors.root.useInterventionExportUrl();
|
||||||
|
const showBulkManagement = selectors.root.useShowBulkManagement();
|
||||||
|
const downloadInterventionReport = actions.grades.useDownloadInterventionReport();
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
downloadInterventionReport();
|
||||||
|
window.location.assign(interventionExportUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
show: showBulkManagement,
|
||||||
|
handleClick,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useInterventionsReportData;
|
||||||
56
src/components/GradesView/InterventionsReport/hooks.test.js
Normal file
56
src/components/GradesView/InterventionsReport/hooks.test.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { actions, selectors } from 'data/redux/hooks';
|
||||||
|
|
||||||
|
import useInterventionsReportData from './hooks';
|
||||||
|
|
||||||
|
jest.mock('data/redux/hooks', () => ({
|
||||||
|
actions: {
|
||||||
|
grades: {
|
||||||
|
useDownloadInterventionReport: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
root: {
|
||||||
|
useInterventionExportUrl: jest.fn(),
|
||||||
|
useShowBulkManagement: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const downloadReport = jest.fn();
|
||||||
|
actions.grades.useDownloadInterventionReport.mockReturnValue(downloadReport);
|
||||||
|
selectors.root.useShowBulkManagement.mockReturnValue(true);
|
||||||
|
const exportUrl = 'test-intervention-export-url';
|
||||||
|
selectors.root.useInterventionExportUrl.mockReturnValue(exportUrl);
|
||||||
|
|
||||||
|
let hook;
|
||||||
|
let oldLocation;
|
||||||
|
describe('useInterventionsReportData hooks', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
oldLocation = window.location;
|
||||||
|
delete window.location;
|
||||||
|
window.location = { assign: jest.fn() };
|
||||||
|
hook = useInterventionsReportData();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
window.location = oldLocation;
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes hooks', () => {
|
||||||
|
expect(selectors.root.useInterventionExportUrl).toHaveBeenCalled();
|
||||||
|
expect(selectors.root.useShowBulkManagement).toHaveBeenCalled();
|
||||||
|
expect(actions.grades.useDownloadInterventionReport).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('output', () => {
|
||||||
|
test('show from showBulkManagement selector', () => {
|
||||||
|
expect(hook.show).toEqual(true);
|
||||||
|
});
|
||||||
|
describe('handleClick', () => {
|
||||||
|
it('downloads interventions report and navigates to export url', () => {
|
||||||
|
hook.handleClick();
|
||||||
|
expect(downloadReport).toHaveBeenCalled();
|
||||||
|
expect(window.location.assign).toHaveBeenCalledWith(exportUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
43
src/components/GradesView/InterventionsReport/index.jsx
Normal file
43
src/components/GradesView/InterventionsReport/index.jsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import NetworkButton from 'components/NetworkButton';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
import useInterventionsReportData from './hooks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <InterventionsReport />
|
||||||
|
* Provides download buttons for Bulk Management and Intervention reports, only if
|
||||||
|
* showBulkManagement is set in redus.
|
||||||
|
*/
|
||||||
|
export const InterventionsReport = () => {
|
||||||
|
const { show, handleClick } = useInterventionsReportData();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4 className="mt-0">
|
||||||
|
{formatMessage(messages.title)}
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
className="d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<div className="intervention-report-description">
|
||||||
|
{formatMessage(messages.description)}
|
||||||
|
</div>
|
||||||
|
<NetworkButton
|
||||||
|
label={messages.downloadBtn}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InterventionsReport;
|
||||||
42
src/components/GradesView/InterventionsReport/index.test.jsx
Normal file
42
src/components/GradesView/InterventionsReport/index.test.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import NetworkButton from 'components/NetworkButton';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
import useInterventionsReportData from './hooks';
|
||||||
|
import InterventionsReport from '.';
|
||||||
|
|
||||||
|
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
||||||
|
jest.mock('./hooks', () => jest.fn());
|
||||||
|
|
||||||
|
const hookProps = { show: true, handleClick: jest.fn() };
|
||||||
|
useInterventionsReportData.mockReturnValue(hookProps);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
describe('InterventionsReport component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
el = shallow(<InterventionsReport />);
|
||||||
|
});
|
||||||
|
describe('behavior', () => {
|
||||||
|
it('initializes hooks', () => {
|
||||||
|
expect(useInterventionsReportData).toHaveBeenCalledWith();
|
||||||
|
expect(useIntl).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('output', () => {
|
||||||
|
it('does now render if show is false', () => {
|
||||||
|
useInterventionsReportData.mockReturnValueOnce({ ...hookProps, show: false });
|
||||||
|
el = shallow(<InterventionsReport />);
|
||||||
|
expect(el.isEmptyRender()).toEqual(true);
|
||||||
|
});
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el).toMatchSnapshot();
|
||||||
|
const btnProps = el.find(NetworkButton).props();
|
||||||
|
expect(btnProps.label).toEqual(messages.downloadBtn);
|
||||||
|
expect(btnProps.onClick).toEqual(hookProps.handleClick);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
import selectors from 'data/selectors';
|
|
||||||
import thunkActions from 'data/thunkActions';
|
|
||||||
|
|
||||||
import { PageButtons, mapStateToProps, mapDispatchToProps } from '.';
|
|
||||||
|
|
||||||
jest.mock('@edx/paragon', () => ({
|
|
||||||
Button: () => 'Button',
|
|
||||||
}));
|
|
||||||
jest.mock('data/selectors', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
grades: {
|
|
||||||
nextPage: jest.fn(state => ({ nextPage: state })),
|
|
||||||
prevPage: jest.fn(state => ({ prevPage: state })),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('data/thunkActions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: {
|
|
||||||
grades: {
|
|
||||||
fetchPrevNextGrades: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
let props;
|
|
||||||
let el;
|
|
||||||
describe('PageButtons component', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
props = {
|
|
||||||
getPrevNextGrades: jest.fn(),
|
|
||||||
nextPage: 'NEXT PAGE',
|
|
||||||
prevPage: 'prev PAGE',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
describe('snapshots', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<PageButtons {...props} />);
|
|
||||||
el.instance.fetchNextGrades = jest.fn().mockName('fetchNextGrades');
|
|
||||||
el.instance.fetchPrevGrades = jest.fn().mockName('fetchPrevGrades');
|
|
||||||
});
|
|
||||||
test('buttons enabled with both endpoints provided', () => {
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
test('nextPage disabled if not provided', () => {
|
|
||||||
el.setProps({ nextPage: undefined });
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
test('prevPage disabled if not provided', () => {
|
|
||||||
el.setProps({ prevPage: undefined });
|
|
||||||
expect(el.instance().render()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('behavior', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
el = shallow(<PageButtons {...props} />);
|
|
||||||
});
|
|
||||||
describe('getPrevGrades', () => {
|
|
||||||
it('calls props.getPrevNextGrades with props.prevPage', () => {
|
|
||||||
el.instance().getPrevGrades();
|
|
||||||
expect(props.getPrevNextGrades).toHaveBeenCalledWith(props.prevPage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('getNextGrades', () => {
|
|
||||||
it('calls props.getPrevNextGrades with props.nextPage', () => {
|
|
||||||
el.instance().getNextGrades();
|
|
||||||
expect(props.getPrevNextGrades).toHaveBeenCalledWith(props.nextPage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapStateToProps', () => {
|
|
||||||
const testState = { l: 'eeeerroooooy', j: 'jjjjeeeeeeenkins' };
|
|
||||||
let mapped;
|
|
||||||
beforeEach(() => {
|
|
||||||
mapped = mapStateToProps(testState);
|
|
||||||
});
|
|
||||||
test('nextPage from grades.nextPage', () => {
|
|
||||||
expect(mapped.nextPage).toEqual(selectors.grades.nextPage(testState));
|
|
||||||
});
|
|
||||||
test('prevPage from grades.prevPage', () => {
|
|
||||||
expect(mapped.prevPage).toEqual(selectors.grades.prevPage(testState));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('mapDispatchToProps', () => {
|
|
||||||
test('getPrevNextGrades from thunkActions.grades.fetchPrevNextGrades', () => {
|
|
||||||
expect(
|
|
||||||
mapDispatchToProps.getPrevNextGrades,
|
|
||||||
).toEqual(thunkActions.grades.fetchPrevNextGrades);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`PageButtons component snapshots buttons enabled with both endpoints provided 1`] = `
|
|
||||||
<div
|
|
||||||
className="d-flex justify-content-center"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"paddingBottom": "20px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
disabled={false}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"margin": "20px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variant="outline-primary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Previous Page"
|
|
||||||
description="Grades tab Previous Page button text"
|
|
||||||
id="gradebook.GradesView.PageButtons.prevPage"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={false}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"margin": "20px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variant="outline-primary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Next Page"
|
|
||||||
description="Grades tab Next Page button text"
|
|
||||||
id="gradebook.GradesView.PageButtons.nextPage"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`PageButtons component snapshots nextPage disabled if not provided 1`] = `
|
|
||||||
<div
|
|
||||||
className="d-flex justify-content-center"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"paddingBottom": "20px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
disabled={false}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"margin": "20px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variant="outline-primary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Previous Page"
|
|
||||||
description="Grades tab Previous Page button text"
|
|
||||||
id="gradebook.GradesView.PageButtons.prevPage"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={true}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"margin": "20px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variant="outline-primary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Next Page"
|
|
||||||
description="Grades tab Next Page button text"
|
|
||||||
id="gradebook.GradesView.PageButtons.nextPage"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`PageButtons component snapshots prevPage disabled if not provided 1`] = `
|
|
||||||
<div
|
|
||||||
className="d-flex justify-content-center"
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"paddingBottom": "20px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
disabled={true}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"margin": "20px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variant="outline-primary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Previous Page"
|
|
||||||
description="Grades tab Previous Page button text"
|
|
||||||
id="gradebook.GradesView.PageButtons.prevPage"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={false}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"margin": "20px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variant="outline-primary"
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Next Page"
|
|
||||||
description="Grades tab Next Page button text"
|
|
||||||
id="gradebook.GradesView.PageButtons.nextPage"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user