From c1c3fcd1612e5c6d962e70738fe03f755ed113ac Mon Sep 17 00:00:00 2001 From: ilee2u Date: Tue, 12 Dec 2023 14:36:13 -0500 Subject: [PATCH 01/23] feat: add loading circles to modals - reset, reject, and verify buttons all have loading circles now - also, renamed the components to modals since that's what they are --- .../ExamsPage/components/AttemptList.jsx | 18 ++--- ...ptButton.jsx => ResetExamAttemptModal.jsx} | 36 ++++++--- ...est.jsx => ResetExamAttemptModal.test.jsx} | 16 ++-- ...tButton.jsx => ReviewExamAttemptModal.jsx} | 77 ++++++++++++------- ...st.jsx => ReviewExamAttemptModal.test.jsx} | 28 +++---- src/pages/ExamsPage/messages.js | 60 +++++++-------- 6 files changed, 137 insertions(+), 98 deletions(-) rename src/pages/ExamsPage/components/{ResetExamAttemptButton.jsx => ResetExamAttemptModal.jsx} (61%) rename src/pages/ExamsPage/components/{ResetExamAttemptButton.test.jsx => ResetExamAttemptModal.test.jsx} (75%) rename src/pages/ExamsPage/components/{ReviewExamAttemptButton.jsx => ReviewExamAttemptModal.jsx} (66%) rename src/pages/ExamsPage/components/{ReviewExamAttemptButton.test.jsx => ReviewExamAttemptModal.test.jsx} (83%) diff --git a/src/pages/ExamsPage/components/AttemptList.jsx b/src/pages/ExamsPage/components/AttemptList.jsx index 0a82790..395ff2e 100644 --- a/src/pages/ExamsPage/components/AttemptList.jsx +++ b/src/pages/ExamsPage/components/AttemptList.jsx @@ -2,8 +2,8 @@ import PropTypes from 'prop-types'; import { DataTable, TextFilter, CheckboxFilter } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import * as constants from 'data/constants'; -import ResetExamAttemptButton from './ResetExamAttemptButton'; -import ReviewExamAttemptButton from './ReviewExamAttemptButton'; +import ResetExamAttemptModal from './ResetExamAttemptModal'; +import ReviewExamAttemptModal from './ReviewExamAttemptModal'; import messages from '../messages'; // TODO: these should be updated to use intl messages @@ -81,17 +81,17 @@ const StatusFilterChoices = [ }, ]; -// The button components must be compartmentalized here otherwise npm lint throws an unstable-nested-component error. -const ResetButton = (row) => ( - ( + ); -const ReviewButton = (row) => ( - ( + { { id: 'action', Header: formatMessage(messages.examAttemptsTableHeaderAction), - Cell: ({ row }) => ResetButton(row), + Cell: ({ row }) => ResetModal(row), }, { id: 'review', Header: formatMessage(messages.examAttemptsTableHeaderReview), - Cell: ({ row }) => ReviewButton(row), + Cell: ({ row }) => ReviewModal(row), }, ]} data={attempts} diff --git a/src/pages/ExamsPage/components/ResetExamAttemptButton.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx similarity index 61% rename from src/pages/ExamsPage/components/ResetExamAttemptButton.jsx rename to src/pages/ExamsPage/components/ResetExamAttemptModal.jsx index f19475d..d44c633 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptButton.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx @@ -1,22 +1,35 @@ import PropTypes from 'prop-types'; +import { useState } from 'react'; import { - Button, useToggle, ModalDialog, ActionRow, + Button, useToggle, ModalDialog, ActionRow, StatefulButton, } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useDeleteExamAttempt } from '../hooks'; import messages from '../messages'; -const ResetExamAttemptButton = ({ username, examName, attemptId }) => { +const ResetButtonProps = { + labels: { + default: 'Reset', + pending: 'Resetting...', + complete: 'Reset', + error: 'Error' + }, + variant: 'primary', +}; + +const ResetExamAttemptModal = ({ username, examName, attemptId }) => { const [isOpen, open, close] = useToggle(false); const resetExamAttempt = useDeleteExamAttempt(); const { formatMessage } = useIntl(); + const [resetButtonStatus, setResetButtonStatus] = useState(""); + return ( <>
{ > - {formatMessage(messages.ResetExamAttemptButtonModalTitle)} + {formatMessage(messages.ResetExamAttemptModalModalTitle)} -

{formatMessage(messages.ResetExamAttemptButtonModalBody)}

+

{formatMessage(messages.ResetExamAttemptModalModalBody)}

  • {formatMessage(messages.Username)}{username}
  • {formatMessage(messages.ExamName)}{examName}
  • @@ -45,16 +58,17 @@ const ResetExamAttemptButton = ({ username, examName, attemptId }) => { - {formatMessage(messages.ResetExamAttemptButtonCancel)} + {formatMessage(messages.ResetExamAttemptModalCancel)} - + {formatMessage(messages.ResetExamAttemptModalConfirm)} + @@ -62,10 +76,10 @@ const ResetExamAttemptButton = ({ username, examName, attemptId }) => { ); }; -ResetExamAttemptButton.propTypes = { +ResetExamAttemptModal.propTypes = { username: PropTypes.string.isRequired, examName: PropTypes.string.isRequired, attemptId: PropTypes.number.isRequired, }; -export default ResetExamAttemptButton; +export default ResetExamAttemptModal; diff --git a/src/pages/ExamsPage/components/ResetExamAttemptButton.test.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx similarity index 75% rename from src/pages/ExamsPage/components/ResetExamAttemptButton.test.jsx rename to src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx index c78122d..1b2644b 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptButton.test.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; -import ResetExamAttemptButton from './ResetExamAttemptButton'; +import ResetExamAttemptModal from './ResetExamAttemptModal'; import * as hooks from '../hooks'; @@ -13,23 +13,23 @@ const mockMakeNetworkRequest = jest.fn(); // nomally mocked for unit tests but required for rendering/snapshots jest.unmock('react'); -const resetButton = ; +const resetModal = ; -describe('ResetExamAttemptButton', () => { +describe('ResetExamAttemptModal', () => { beforeEach(() => { jest.restoreAllMocks(); hooks.useDeleteExamAttempt.mockReturnValue(mockMakeNetworkRequest); }); - it('Test that the ResetExamAttemptButton matches snapshot', () => { - expect(render(resetButton)).toMatchSnapshot(); + it('Test that the ResetExamAttemptModal matches snapshot', () => { + expect(render(resetModal)).toMatchSnapshot(); }); it('Modal appears upon clicking button', () => { - render(resetButton); + render(resetModal); screen.getByText('Reset').click(); expect(screen.getByText('Please confirm your choice.')).toBeInTheDocument(); }); it('Clicking the No button closes the modal', () => { - render(resetButton); + render(resetModal); screen.getByText('Reset').click(); screen.getByText('Cancel').click(); // Using queryByText here allows the function to throw @@ -38,7 +38,7 @@ describe('ResetExamAttemptButton', () => { it('Clicking the Yes button calls the deletion hook', () => { const mockDeleteExamAttempt = jest.fn(); jest.spyOn(hooks, 'useDeleteExamAttempt').mockImplementation(() => mockDeleteExamAttempt); - render(resetButton); + render(resetModal); screen.getByText('Reset').click(); screen.getByText('Yes, I\'m Sure').click(); expect(mockDeleteExamAttempt).toHaveBeenCalledWith(0); diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptButton.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx similarity index 66% rename from src/pages/ExamsPage/components/ReviewExamAttemptButton.jsx rename to src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index 5ed8078..41d4226 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptButton.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -1,7 +1,8 @@ import PropTypes from 'prop-types'; +import { useState } from 'react'; import { - Button, useToggle, ModalDialog, ActionRow, + Button, useToggle, ModalDialog, ActionRow, StatefulButton, } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Info, Warning } from '@edx/paragon/icons'; @@ -22,13 +23,35 @@ const ReviewRequiredStatuses = [ constants.ExamAttemptStatus.second_review_required, ]; -const ReviewExamAttemptButton = ({ +const VerifyButtonProps = { + labels: { + default: 'Verify', + pending: 'Verifying...', + complete: 'Verified', + error: 'Error' + }, + variant: 'primary', +}; + +const RejectButtonProps = { + labels: { + default: 'Reject', + pending: 'Rejecting...', + complete: 'Rejected', + error: 'Error' + }, + variant: 'primary', +}; + +const ReviewExamAttemptModal = ({ username, examName, attemptId, attemptStatus, severity, submissionReason, }) => { const [isOpen, open, close] = useToggle(false); const modifyExamAttempt = useModifyExamAttempt(); const { currentExam } = useExamsData(); const { formatMessage } = useIntl(); + const [rejectButtonStatus, setRejectButtonStatus] = useState(""); + const [verifyButtonStatus, setVerifyButtonStatus] = useState(""); const getButton = (status) => { if (ReviewRequiredStatuses.includes(status)) { @@ -70,7 +93,7 @@ const ReviewExamAttemptButton = ({ return ( <>
    - { getButton(attemptStatus) } + {getButton(attemptStatus)}
    - {formatMessage(messages.ReviewExamAttemptButtonModalTitle)} + {formatMessage(messages.ReviewExamAttemptModalTitle)} @@ -106,29 +129,31 @@ const ReviewExamAttemptButton = ({ - {formatMessage(messages.ReviewExamAttemptButtonCancel)} + {formatMessage(messages.ReviewExamAttemptModalCancel)} - { attemptStatus !== constants.ExamAttemptStatus.verified + {attemptStatus !== constants.ExamAttemptStatus.verified && ( - + { // eslint-disable-line no-unused-vars + setVerifyButtonStatus("pending"); + modifyExamAttempt(attemptId, constants.ExamAttemptActions.verify); + }} + > + {formatMessage(messages.ReviewExamAttemptModalVerify)} + )} - { attemptStatus !== constants.ExamAttemptStatus.rejected + {attemptStatus !== constants.ExamAttemptStatus.rejected && ( - + { // eslint-disable-line no-unused-vars + setRejectButtonStatus("pending"); + modifyExamAttempt(attemptId, constants.ExamAttemptActions.reject); + }} + > + {formatMessage(messages.ReviewExamAttemptModalReject)} + )} @@ -137,7 +162,7 @@ const ReviewExamAttemptButton = ({ ); }; -ReviewExamAttemptButton.propTypes = { +ReviewExamAttemptModal.propTypes = { username: PropTypes.string.isRequired, examName: PropTypes.string.isRequired, attemptId: PropTypes.number.isRequired, @@ -146,9 +171,9 @@ ReviewExamAttemptButton.propTypes = { submissionReason: PropTypes.string, }; -ReviewExamAttemptButton.defaultProps = { +ReviewExamAttemptModal.defaultProps = { severity: null, submissionReason: null, }; -export default ReviewExamAttemptButton; +export default ReviewExamAttemptModal; diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptButton.test.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx similarity index 83% rename from src/pages/ExamsPage/components/ReviewExamAttemptButton.test.jsx rename to src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx index 16af672..4b63b99 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptButton.test.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import * as constants from 'data/constants'; -import ReviewExamAttemptButton from './ReviewExamAttemptButton'; +import ReviewExamAttemptModal from './ReviewExamAttemptModal'; import * as testUtils from '../../../testUtils'; import * as hooks from '../hooks'; @@ -16,8 +16,8 @@ const mockMakeNetworkRequest = jest.fn(); // normally mocked for unit tests but required for rendering/snapshots jest.unmock('react'); -const reviewButton = (status = constants.ExamAttemptStatus.second_review_required) => ( - ( + ); -describe('ReviewExamAttemptButton', () => { +describe('ReviewExamAttemptModal', () => { beforeEach(() => { jest.restoreAllMocks(); hooks.useModifyExamAttempt.mockReturnValue(mockMakeNetworkRequest); hooks.useExamsData.mockReturnValue(testUtils.defaultExamsData); }); - it('Test that the ReviewExamAttemptButton matches snapshot', () => { - expect(render(reviewButton())).toMatchSnapshot(); + it('Test that the ReviewExamAttemptModal matches snapshot', () => { + expect(render(reviewModal())).toMatchSnapshot(); }); it('Modal appears upon clicking button', () => { - render(reviewButton()); + render(reviewModal()); screen.getByText('Review Required').click(); expect(screen.getByText('Update review status')).toBeInTheDocument(); }); it('Clicking the Cancel button closes the modal', () => { - render(reviewButton()); + render(reviewModal()); screen.getByText('Review Required').click(); screen.getByText('Cancel').click(); // Using queryByText here allows the function to throw @@ -51,7 +51,7 @@ describe('ReviewExamAttemptButton', () => { it('Clicking the Verify button calls the modify exam attempt hook', () => { const mockModifyExamAttempt = jest.fn(); jest.spyOn(hooks, 'useModifyExamAttempt').mockImplementation(() => mockModifyExamAttempt); - render(reviewButton()); + render(reviewModal()); screen.getByText('Review Required').click(); screen.getByText('Verify').click(); expect(mockModifyExamAttempt).toHaveBeenCalledWith(0, constants.ExamAttemptActions.verify); @@ -59,14 +59,14 @@ describe('ReviewExamAttemptButton', () => { it('Clicking the Reject button calls the modify exam attempt hook', () => { const mockModifyExamAttempt = jest.fn(); jest.spyOn(hooks, 'useModifyExamAttempt').mockImplementation(() => mockModifyExamAttempt); - render(reviewButton()); + render(reviewModal()); screen.getByText('Review Required').click(); screen.getByText('Reject').click(); expect(mockModifyExamAttempt).toHaveBeenCalledWith(0, constants.ExamAttemptActions.reject); }); it('Does not show the modal if the attempt is not reviewable', () => { render( - { }); describe('Shows the correct text based on the attempt status', () => { test('review required', () => { - render(reviewButton()); + render(reviewModal()); screen.getByText('Review Required').click(); expect(screen.getByText(/attempt requires manual review/i)).toBeInTheDocument(); }); test('existing review', () => { - render(reviewButton(constants.ExamAttemptStatus.verified)); + render(reviewModal(constants.ExamAttemptStatus.verified)); screen.getByText('Manage Review').click(); expect(screen.getByText(/attempt has a verified review/i)).toBeInTheDocument(); }); test('error status', () => { - render(reviewButton(constants.ExamAttemptStatus.error)); + render(reviewModal(constants.ExamAttemptStatus.error)); screen.getByText('Review Required').click(); expect(screen.getByText(/attempt has been terminated due to an error/i)).toBeInTheDocument(); }); diff --git a/src/pages/ExamsPage/messages.js b/src/pages/ExamsPage/messages.js index f97343c..b076bb5 100644 --- a/src/pages/ExamsPage/messages.js +++ b/src/pages/ExamsPage/messages.js @@ -130,103 +130,103 @@ const messages = defineMessages({ description: 'Message that appears in the table if no data is found', }, - // ResetExamAttemptButton - ResetExamAttemptButtonTitle: { - id: 'ResetExamAttemptButton.title', + // ResetExamAttemptModal + ResetExamAttemptModalTitle: { + id: 'ResetExamAttemptModal.title', defaultMessage: 'Reset', description: 'Title for the button to reset exam attempts', }, - ResetExamAttemptButtonModalTitle: { - id: 'ResetExamAttemptButton.confirmation_modal_title', + ResetExamAttemptModalModalTitle: { + id: 'ResetExamAttemptModal.confirmation_modal_title', defaultMessage: 'Please confirm your choice.', description: 'Title header of the modal that appears to confirm the reset of an exam attempt', }, - ResetExamAttemptButtonModalBody: { - id: 'ResetExamAttemptButton.confirmation_modal_body', + ResetExamAttemptModalModalBody: { + id: 'ResetExamAttemptModal.confirmation_modal_body', defaultMessage: 'Are you sure you want to remove the exam attempt with the following data?:', description: 'Body text of the modal that appears to confirm the reset of an exam attempt', }, - ResetExamAttemptButtonCancel: { - id: 'ResetExamAttemptButton.cancel_button', + ResetExamAttemptModalCancel: { + id: 'ResetExamAttemptModal.cancel_button', defaultMessage: 'Cancel', description: 'Text for the button to cancel resetting an exam attempt', }, - ResetExamAttemptButtonConfirm: { - id: 'ResetExamAttemptButton.confirm_button', + ResetExamAttemptModalConfirm: { + id: 'ResetExamAttemptModal.confirm_button', defaultMessage: 'Yes, I\'m Sure', description: 'Text for the button to confirm the reset of an exam attempt', }, - // ReviewExamAttemptButton + // ReviewExamAttemptModal ReviewableButtonTitle: { - id: 'ReviewExamAttemptButton.required.title', + id: 'ReviewExamAttemptModal.required.title', defaultMessage: 'Manage Review', description: 'Title for the button to review exam attempts', }, ReviewRequiredButtonTitle: { - id: 'ReviewExamAttemptButton.title', + id: 'ReviewExamAttemptModal.title', defaultMessage: 'Review Required', description: 'Title for the button to review exam attempts', }, - ReviewExamAttemptButtonModalTitle: { - id: 'ReviewExamAttemptButton.confirmation_modal_title', + ReviewExamAttemptModalTitle: { + id: 'ReviewExamAttemptModal.confirmation_modal_title', defaultMessage: 'Update review status', description: 'Title header of the modal that appears to confirm the review of an exam attempt', }, ReviewExamAttemptModalBodyReviewRequried: { - id: 'ReviewExamAttemptButton.confirmation_modal_body.review_required', + id: 'ReviewExamAttemptModal.confirmation_modal_body.review_required', defaultMessage: 'Due to suspicious activity, this exam attempt requires manual review.', description: 'Body text of the review modal that appears when an exam attempt requires review', }, ReviewExamAttemptModalBodyManageReview: { - id: 'ReviewExamAttemptButton.confirmation_modal_body.review_manage', + id: 'ReviewExamAttemptModal.confirmation_modal_body.review_manage', defaultMessage: 'This exam attempt has a {statusLabel} review. You may manually override the review status for this session.', description: 'Body text of the review modal that appears when an exam attempt has an existing review', }, ReviewExamAttemptModalBodyError: { - id: 'ReviewExamAttemptButton.confirmation_modal_body.error_status', + id: 'ReviewExamAttemptModal.confirmation_modal_body.error_status', defaultMessage: 'This exam attempt has been terminated due to an error; student progress may be incomplete. You may manually complete this session with a review.', description: 'Body text of the review modal that appears when an exam attempt has an error status', }, ReviewExamAttemptModalBodySessionInfo: { - id: 'ReviewExamAttemptButton.confirmation_modal_body.session_info', + id: 'ReviewExamAttemptModal.confirmation_modal_body.session_info', defaultMessage: 'A session recording and details of suspicious behavior can be found on the review dashboard for the proctoring tool.', description: 'Body text of the modal directing the user to an external dashboard to view session details', }, - ReviewExamAttemptButtonCancel: { - id: 'ReviewExamAttemptButton.cancel_button', + ReviewExamAttemptModalCancel: { + id: 'ReviewExamAttemptModal.cancel_button', defaultMessage: 'Cancel', description: 'Text for the button to cancel reviewing an exam attempt', }, - ReviewExamAttemptButtonVerify: { - id: 'ReviewExamAttemptButton.verify_button', + ReviewExamAttemptModalVerify: { + id: 'ReviewExamAttemptModal.verify_button', defaultMessage: 'Verify', description: 'Text for the button to verify an exam attempt', }, - ReviewExamAttemptButtonReject: { - id: 'ReviewExamAttemptButton.reject_button', + ReviewExamAttemptModalReject: { + id: 'ReviewExamAttemptModal.reject_button', defaultMessage: 'Reject', description: 'Text for the button to reject an exam attempt', }, // Labels for exam attempt info for review/reset modals Username: { - id: 'ExamAttemptButton.username', + id: 'ExamAttemptModal.username', defaultMessage: 'Username: ', description: 'Username label for exam attempt data in the review/reset modal', }, ExamName: { - id: 'ExamAttemptButton.exam_name', + id: 'ExamAttemptModal.exam_name', defaultMessage: 'Exam Name: ', description: 'Exam name label for exam attempt data in the review/reset modal', }, SuspicionLevel: { - id: 'ExamAttemptButton.suspicion_level', + id: 'ExamAttemptModal.suspicion_level', defaultMessage: 'Suspicion Level: ', description: 'Suspicion level label for exam attempt data in the review/reset modal', }, SubmissionReason: { - id: 'ExamAttemptButton.submission_reason', + id: 'ExamAttemptModal.submission_reason', defaultMessage: 'Submission Reason: ', description: 'Submission reason label for exam attempt data in the review/reset modal', }, From f8b39a1d43deadc463d509d1209b4415f443abb9 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Tue, 12 Dec 2023 14:41:13 -0500 Subject: [PATCH 02/23] chore: lint --- .../components/ResetExamAttemptModal.jsx | 11 +++++----- .../components/ReviewExamAttemptModal.jsx | 20 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx index d44c633..2659036 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx @@ -13,7 +13,7 @@ const ResetButtonProps = { default: 'Reset', pending: 'Resetting...', complete: 'Reset', - error: 'Error' + error: 'Error', }, variant: 'primary', }; @@ -22,8 +22,7 @@ const ResetExamAttemptModal = ({ username, examName, attemptId }) => { const [isOpen, open, close] = useToggle(false); const resetExamAttempt = useDeleteExamAttempt(); const { formatMessage } = useIntl(); - const [resetButtonStatus, setResetButtonStatus] = useState(""); - + const [resetButtonStatus, setResetButtonStatus] = useState(''); return ( <> @@ -60,10 +59,12 @@ const ResetExamAttemptModal = ({ username, examName, attemptId }) => { {formatMessage(messages.ResetExamAttemptModalCancel)} - { // eslint-disable-line no-unused-vars - setResetButtonStatus("pending"); + setResetButtonStatus('pending'); resetExamAttempt(attemptId); }} > diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index 41d4226..daf4d56 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -28,7 +28,7 @@ const VerifyButtonProps = { default: 'Verify', pending: 'Verifying...', complete: 'Verified', - error: 'Error' + error: 'Error', }, variant: 'primary', }; @@ -38,7 +38,7 @@ const RejectButtonProps = { default: 'Reject', pending: 'Rejecting...', complete: 'Rejected', - error: 'Error' + error: 'Error', }, variant: 'primary', }; @@ -50,8 +50,8 @@ const ReviewExamAttemptModal = ({ const modifyExamAttempt = useModifyExamAttempt(); const { currentExam } = useExamsData(); const { formatMessage } = useIntl(); - const [rejectButtonStatus, setRejectButtonStatus] = useState(""); - const [verifyButtonStatus, setVerifyButtonStatus] = useState(""); + const [rejectButtonStatus, setRejectButtonStatus] = useState(''); + const [verifyButtonStatus, setVerifyButtonStatus] = useState(''); const getButton = (status) => { if (ReviewRequiredStatuses.includes(status)) { @@ -133,10 +133,12 @@ const ReviewExamAttemptModal = ({ {attemptStatus !== constants.ExamAttemptStatus.verified && ( - { // eslint-disable-line no-unused-vars - setVerifyButtonStatus("pending"); + setVerifyButtonStatus('pending'); modifyExamAttempt(attemptId, constants.ExamAttemptActions.verify); }} > @@ -145,10 +147,12 @@ const ReviewExamAttemptModal = ({ )} {attemptStatus !== constants.ExamAttemptStatus.rejected && ( - { // eslint-disable-line no-unused-vars - setRejectButtonStatus("pending"); + setRejectButtonStatus('pending'); modifyExamAttempt(attemptId, constants.ExamAttemptActions.reject); }} > From 79ce456de9bf6db92629dc8a1bf6206339100ef9 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Wed, 13 Dec 2023 10:14:38 -0500 Subject: [PATCH 03/23] test: update snapshots --- .../ResetExamAttemptModal.test.jsx.snap | 84 ++++++++++++++ .../ReviewExamAttemptModal.test.jsx.snap | 108 ++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 src/pages/ExamsPage/components/__snapshots__/ResetExamAttemptModal.test.jsx.snap create mode 100644 src/pages/ExamsPage/components/__snapshots__/ReviewExamAttemptModal.test.jsx.snap diff --git a/src/pages/ExamsPage/components/__snapshots__/ResetExamAttemptModal.test.jsx.snap b/src/pages/ExamsPage/components/__snapshots__/ResetExamAttemptModal.test.jsx.snap new file mode 100644 index 0000000..9badc10 --- /dev/null +++ b/src/pages/ExamsPage/components/__snapshots__/ResetExamAttemptModal.test.jsx.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResetExamAttemptModal Test that the ResetExamAttemptModal matches snapshot 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
    +
    + +
    +
    + , + "container":
    +
    + +
    +
    , + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; diff --git a/src/pages/ExamsPage/components/__snapshots__/ReviewExamAttemptModal.test.jsx.snap b/src/pages/ExamsPage/components/__snapshots__/ReviewExamAttemptModal.test.jsx.snap new file mode 100644 index 0000000..4c0ca09 --- /dev/null +++ b/src/pages/ExamsPage/components/__snapshots__/ReviewExamAttemptModal.test.jsx.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReviewExamAttemptModal Test that the ReviewExamAttemptModal matches snapshot 1`] = ` +Object { + "asFragment": [Function], + "baseElement": +
    +
    + +
    +
    + , + "container":
    +
    + +
    +
    , + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; From 0b9f5d764a475df09508d8bf78782a9a6730f9d4 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Wed, 13 Dec 2023 10:18:16 -0500 Subject: [PATCH 04/23] test: remove old snapshots --- .../ResetExamAttemptButton.test.jsx.snap | 84 -------------- .../ReviewExamAttemptButton.test.jsx.snap | 108 ------------------ 2 files changed, 192 deletions(-) delete mode 100644 src/pages/ExamsPage/components/__snapshots__/ResetExamAttemptButton.test.jsx.snap delete mode 100644 src/pages/ExamsPage/components/__snapshots__/ReviewExamAttemptButton.test.jsx.snap diff --git a/src/pages/ExamsPage/components/__snapshots__/ResetExamAttemptButton.test.jsx.snap b/src/pages/ExamsPage/components/__snapshots__/ResetExamAttemptButton.test.jsx.snap deleted file mode 100644 index b9f439d..0000000 --- a/src/pages/ExamsPage/components/__snapshots__/ResetExamAttemptButton.test.jsx.snap +++ /dev/null @@ -1,84 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ResetExamAttemptButton Test that the ResetExamAttemptButton matches snapshot 1`] = ` -Object { - "asFragment": [Function], - "baseElement": -
    -
    - -
    -
    - , - "container":
    -
    - -
    -
    , - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; diff --git a/src/pages/ExamsPage/components/__snapshots__/ReviewExamAttemptButton.test.jsx.snap b/src/pages/ExamsPage/components/__snapshots__/ReviewExamAttemptButton.test.jsx.snap deleted file mode 100644 index 352d2fe..0000000 --- a/src/pages/ExamsPage/components/__snapshots__/ReviewExamAttemptButton.test.jsx.snap +++ /dev/null @@ -1,108 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReviewExamAttemptButton Test that the ReviewExamAttemptButton matches snapshot 1`] = ` -Object { - "asFragment": [Function], - "baseElement": -
    -
    - -
    -
    - , - "container":
    -
    - -
    -
    , - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; From 592ef0781952fbdac2eab1247ae004430fb64657 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Thu, 14 Dec 2023 11:53:33 -0500 Subject: [PATCH 05/23] feat: button labels moved to translation msgs --- .../components/ResetExamAttemptModal.jsx | 22 +++---- .../components/ReviewExamAttemptModal.jsx | 48 ++++++++------ src/pages/ExamsPage/data/api.js | 6 ++ src/pages/ExamsPage/messages.js | 65 ++++++++++++++++++- 4 files changed, 108 insertions(+), 33 deletions(-) diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx index 2659036..2f0a335 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx @@ -8,21 +8,21 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { useDeleteExamAttempt } from '../hooks'; import messages from '../messages'; -const ResetButtonProps = { - labels: { - default: 'Reset', - pending: 'Resetting...', - complete: 'Reset', - error: 'Error', - }, - variant: 'primary', -}; - const ResetExamAttemptModal = ({ username, examName, attemptId }) => { const [isOpen, open, close] = useToggle(false); const resetExamAttempt = useDeleteExamAttempt(); - const { formatMessage } = useIntl(); const [resetButtonStatus, setResetButtonStatus] = useState(''); + const { formatMessage } = useIntl(); + + const ResetButtonProps = { + labels: { + default: formatMessage(messages.ResetExamAttemptButtonDefaultLabel), + pending: formatMessage(messages.ResetExamAttemptButtonPendingLabel), + complete: formatMessage(messages.ResetExamAttemptButtonCompelteLabel), + error: formatMessage(messages.ResetExamAttemptButtonErrorLabel), + }, + variant: 'primary', + }; return ( <> diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index daf4d56..7acd7fe 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -23,35 +23,35 @@ const ReviewRequiredStatuses = [ constants.ExamAttemptStatus.second_review_required, ]; -const VerifyButtonProps = { - labels: { - default: 'Verify', - pending: 'Verifying...', - complete: 'Verified', - error: 'Error', - }, - variant: 'primary', -}; - -const RejectButtonProps = { - labels: { - default: 'Reject', - pending: 'Rejecting...', - complete: 'Rejected', - error: 'Error', - }, - variant: 'primary', -}; - const ReviewExamAttemptModal = ({ username, examName, attemptId, attemptStatus, severity, submissionReason, }) => { const [isOpen, open, close] = useToggle(false); const modifyExamAttempt = useModifyExamAttempt(); const { currentExam } = useExamsData(); - const { formatMessage } = useIntl(); const [rejectButtonStatus, setRejectButtonStatus] = useState(''); const [verifyButtonStatus, setVerifyButtonStatus] = useState(''); + const { formatMessage } = useIntl(); + + const VerifyButtonProps = { + labels: { + default: formatMessage(messages.VerifyExamAttemptButtonDefaultLabel), + pending: formatMessage(messages.VerifyExamAttemptButtonPendingLabel), + complete: formatMessage(messages.VerifyExamAttemptButtonCompelteLabel), + error: formatMessage(messages.ReviewExamAttemptButtonErrorLabel), + }, + variant: 'primary', + }; + + const RejectButtonProps = { + labels: { + default: formatMessage(messages.RejectExamAttemptButtonDefaultLabel), + pending: formatMessage(messages.RejectExamAttemptButtonPendingLabel), + complete: formatMessage(messages.RejectExamAttemptButtonCompelteLabel), + error: formatMessage(messages.ReviewExamAttemptButtonErrorLabel), + }, + variant: 'primary', + }; const getButton = (status) => { if (ReviewRequiredStatuses.includes(status)) { @@ -140,6 +140,11 @@ const ReviewExamAttemptModal = ({ onClick={e => { // eslint-disable-line no-unused-vars setVerifyButtonStatus('pending'); modifyExamAttempt(attemptId, constants.ExamAttemptActions.verify); + // TODO: Make this complete state only show after an exam attempt has been successfully modified + // Or alternatively, we could have a toast message show up. Either way, we need to await the + // call to modifyExamAttempt() before we show confirmation that it worked, so let's figure out + // how to do that + setRejectButtonStatus('complete'); }} > {formatMessage(messages.ReviewExamAttemptModalVerify)} @@ -154,6 +159,7 @@ const ReviewExamAttemptModal = ({ onClick={e => { // eslint-disable-line no-unused-vars setRejectButtonStatus('pending'); modifyExamAttempt(attemptId, constants.ExamAttemptActions.reject); + setRejectButtonStatus('complete'); }} > {formatMessage(messages.ReviewExamAttemptModalReject)} diff --git a/src/pages/ExamsPage/data/api.js b/src/pages/ExamsPage/data/api.js index 3477420..20c62c7 100644 --- a/src/pages/ExamsPage/data/api.js +++ b/src/pages/ExamsPage/data/api.js @@ -26,7 +26,13 @@ export async function deleteExamAttempt(attemptId) { return response.data; } +// function sleep(ms) { +// return new Promise(resolve => setTimeout(resolve, ms)); +// } + export async function modifyExamAttempt(attemptId, action) { + // Temporary sleep code so I can see the button in the pending state, will remove in final version + // await sleep(2000); const url = `${getExamsBaseUrl()}/api/v1/exams/attempt/${attemptId}`; const payload = { action }; const response = await getAuthenticatedHttpClient().put(url, payload); diff --git a/src/pages/ExamsPage/messages.js b/src/pages/ExamsPage/messages.js index b076bb5..34898f2 100644 --- a/src/pages/ExamsPage/messages.js +++ b/src/pages/ExamsPage/messages.js @@ -209,6 +209,69 @@ const messages = defineMessages({ description: 'Text for the button to reject an exam attempt', }, + // Reset Button Labels + ResetExamAttemptButtonDefaultLabel: { + id: 'ResetExamAttemptModal.reset_button_default_label', + defaultMessage: 'Reset', + description: 'Label for the button to reset an exam attempt in its default state', + }, + ResetExamAttemptButtonPendingLabel: { + id: 'ResetExamAttemptModal.reset_button_pending_label', + defaultMessage: 'Resetting...', + description: 'Label for the button to reset an exam attempt in its pending state', + }, + ResetExamAttemptButtonCompelteLabel: { + id: 'ResetExamAttemptModal.reset_button_complete_label', + defaultMessage: 'Reset', + description: 'Label for the button to reset an exam attempt in its completed state', + }, + + // Verify Button Labels + VerifyExamAttemptButtonDefaultLabel: { + id: 'ReviewExamAttemptModal.verify_button_default_label', + defaultMessage: 'Verify', + description: 'Label for the button to verify an exam attempt in its default state', + }, + VerifyExamAttemptButtonPendingLabel: { + id: 'ReviewExamAttemptModal.verify_button_pending_label', + defaultMessage: 'Verifying...', + description: 'Label for the button to verify an exam attempt in its pending state', + }, + VerifyExamAttemptButtonCompelteLabel: { + id: 'ReviewExamAttemptModal.verify_button_complete_label', + defaultMessage: 'Verified', + description: 'Label for the button to verify an exam attempt in its completed state', + }, + + // Reject Button Labels + RejectExamAttemptButtonDefaultLabel: { + id: 'ReviewExamAttemptModal.reject_button_default_label', + defaultMessage: 'Reject', + description: 'Label for the button to reject an exam attempt in its default state', + }, + RejectExamAttemptButtonPendingLabel: { + id: 'ReviewExamAttemptModal.reject_button_pending_label', + defaultMessage: 'Rejecting...', + description: 'Label for the button to reject an exam attempt in its pending state', + }, + RejectExamAttemptButtonCompelteLabel: { + id: 'ReviewExamAttemptModal.reject_button_complete_label', + defaultMessage: 'Rejected', + description: 'Label for the button to reject an exam attempt in its completed state', + }, + + // Error Button Labels (Same across all buttons, split in two since message id's are component-specific) + ResetExamAttemptButtonErrorLabel: { + id: 'ResetExamAttemptModal.any_button_error_label', + defaultMessage: 'Error', + description: 'Label for a button to an modify an exam attempt in its errored state', + }, + ReviewExamAttemptButtonErrorLabel: { + id: 'ReviewExamAttemptModal.any_button_error_label', + defaultMessage: 'Error', + description: 'Label for a button to an modify an exam attempt in its errored state', + }, + // Labels for exam attempt info for review/reset modals Username: { id: 'ExamAttemptModal.username', @@ -231,12 +294,12 @@ const messages = defineMessages({ description: 'Submission reason label for exam attempt data in the review/reset modal', }, + // Labels for review dashboard button ReviewDashboardOpenLTITool: { id: 'ExternalReviewDashboard.open_lti_tool', defaultMessage: 'Open the Review Dashboard for {exam_name}', description: 'Text for button to open instructor LTI tool in a new window', }, - ReviewDashboardPleaseSelectExam: { id: 'ExternalReviewDashboard.please_select_exam', defaultMessage: 'Please select an exam from the dropdown above.', From dabb0d80191fdbac0a871f3374a70eec91c7b3bd Mon Sep 17 00:00:00 2001 From: ilee2u Date: Thu, 14 Dec 2023 16:12:13 -0500 Subject: [PATCH 06/23] temp: trying to link button state to request --- .../components/ReviewExamAttemptModal.jsx | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index 7acd7fe..5766d2d 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -10,6 +10,11 @@ import * as constants from 'data/constants'; import { useExamsData, useModifyExamAttempt } from '../hooks'; import messages from '../messages'; import { getLaunchUrlByExamId, getMessageLabelForStatus } from '../utils'; +import { RequestKeys } from 'data/constants'; +import * as reduxHooks from 'data/redux/hooks'; +import { async } from 'regenerator-runtime'; + + const ReviewableStatuses = [ constants.ExamAttemptStatus.error, @@ -53,6 +58,27 @@ const ReviewExamAttemptModal = ({ variant: 'primary', }; + const getRequestStatus = () => { + console.log("OH BOY"); + if (reduxHooks.useRequestIsPending(RequestKeys.modifyExamAttempt)) { + return 'pending'; + } else if (reduxHooks.useRequestIsCompleted(RequestKeys.modifyExamAttempt)) { + return 'complete'; + } else if (reduxHooks.useRequestError(RequestKeys.modifyExamAttempt)) { + return 'error'; + }; + return ''; + }; + + // Set the status of the button + const updateButtonStatus = (buttonType) => { + if (buttonType == 'verify') { + setVerifyButtonStatus(getRequestStatus()); + } else if (buttonType == 'reject') { + setRejectButtonStatus(getRequestStatus()); + } + }; + const getButton = (status) => { if (ReviewRequiredStatuses.includes(status)) { return ( @@ -137,14 +163,10 @@ const ReviewExamAttemptModal = ({ state={verifyButtonStatus} {...VerifyButtonProps} variant="success" - onClick={e => { // eslint-disable-line no-unused-vars - setVerifyButtonStatus('pending'); - modifyExamAttempt(attemptId, constants.ExamAttemptActions.verify); - // TODO: Make this complete state only show after an exam attempt has been successfully modified - // Or alternatively, we could have a toast message show up. Either way, we need to await the - // call to modifyExamAttempt() before we show confirmation that it worked, so let's figure out - // how to do that - setRejectButtonStatus('complete'); + onClick={async e => { // eslint-disable-line no-unused-vars + updateButtonStatus('reject'); + await modifyExamAttempt(attemptId, constants.ExamAttemptActions.verify); + // updateButtonStatus() }} > {formatMessage(messages.ReviewExamAttemptModalVerify)} @@ -157,9 +179,9 @@ const ReviewExamAttemptModal = ({ {...RejectButtonProps} variant="danger" onClick={e => { // eslint-disable-line no-unused-vars - setRejectButtonStatus('pending'); + // setRejectButtonStatus('pending'); modifyExamAttempt(attemptId, constants.ExamAttemptActions.reject); - setRejectButtonStatus('complete'); + // setRejectButtonStatus('complete'); }} > {formatMessage(messages.ReviewExamAttemptModalReject)} From e75c81af491d914447122481aaa1673b40fe4672 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Thu, 14 Dec 2023 16:56:09 -0500 Subject: [PATCH 07/23] temp: 2nd attempt of hooks in component --- .../components/ReviewExamAttemptModal.jsx | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index 5766d2d..c96bb04 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -10,11 +10,7 @@ import * as constants from 'data/constants'; import { useExamsData, useModifyExamAttempt } from '../hooks'; import messages from '../messages'; import { getLaunchUrlByExamId, getMessageLabelForStatus } from '../utils'; -import { RequestKeys } from 'data/constants'; import * as reduxHooks from 'data/redux/hooks'; -import { async } from 'regenerator-runtime'; - - const ReviewableStatuses = [ constants.ExamAttemptStatus.error, @@ -38,47 +34,6 @@ const ReviewExamAttemptModal = ({ const [verifyButtonStatus, setVerifyButtonStatus] = useState(''); const { formatMessage } = useIntl(); - const VerifyButtonProps = { - labels: { - default: formatMessage(messages.VerifyExamAttemptButtonDefaultLabel), - pending: formatMessage(messages.VerifyExamAttemptButtonPendingLabel), - complete: formatMessage(messages.VerifyExamAttemptButtonCompelteLabel), - error: formatMessage(messages.ReviewExamAttemptButtonErrorLabel), - }, - variant: 'primary', - }; - - const RejectButtonProps = { - labels: { - default: formatMessage(messages.RejectExamAttemptButtonDefaultLabel), - pending: formatMessage(messages.RejectExamAttemptButtonPendingLabel), - complete: formatMessage(messages.RejectExamAttemptButtonCompelteLabel), - error: formatMessage(messages.ReviewExamAttemptButtonErrorLabel), - }, - variant: 'primary', - }; - - const getRequestStatus = () => { - console.log("OH BOY"); - if (reduxHooks.useRequestIsPending(RequestKeys.modifyExamAttempt)) { - return 'pending'; - } else if (reduxHooks.useRequestIsCompleted(RequestKeys.modifyExamAttempt)) { - return 'complete'; - } else if (reduxHooks.useRequestError(RequestKeys.modifyExamAttempt)) { - return 'error'; - }; - return ''; - }; - - // Set the status of the button - const updateButtonStatus = (buttonType) => { - if (buttonType == 'verify') { - setVerifyButtonStatus(getRequestStatus()); - } else if (buttonType == 'reject') { - setRejectButtonStatus(getRequestStatus()); - } - }; - const getButton = (status) => { if (ReviewRequiredStatuses.includes(status)) { return ( @@ -116,6 +71,50 @@ const ReviewExamAttemptModal = ({ return null; // we should not get here }; + const VerifyButtonProps = { + labels: { + default: formatMessage(messages.VerifyExamAttemptButtonDefaultLabel), + pending: formatMessage(messages.VerifyExamAttemptButtonPendingLabel), + complete: formatMessage(messages.VerifyExamAttemptButtonCompelteLabel), + error: formatMessage(messages.ReviewExamAttemptButtonErrorLabel), + }, + variant: 'primary', + }; + + const RejectButtonProps = { + labels: { + default: formatMessage(messages.RejectExamAttemptButtonDefaultLabel), + pending: formatMessage(messages.RejectExamAttemptButtonPendingLabel), + complete: formatMessage(messages.RejectExamAttemptButtonCompelteLabel), + error: formatMessage(messages.ReviewExamAttemptButtonErrorLabel), + }, + variant: 'primary', + }; + + const isPending = reduxHooks.useRequestIsPending(constants.RequestKeys.modifyExamAttempt); + const isCompleted = reduxHooks.useRequestIsCompleted(constants.RequestKeys.modifyExamAttempt); + const isError = reduxHooks.useRequestError(constants.RequestKeys.modifyExamAttempt); + + const getRequestStatus = () => { + if (isPending) { + return 'pending'; + } else if (isCompleted) { + return 'complete'; + } else if (isError) { + return 'error'; + }; + return ''; + }; + + // Set the status of the button + const updateButtonStatus = (buttonType) => { + if (buttonType == 'verify') { + setVerifyButtonStatus(getRequestStatus()); + } else if (buttonType == 'reject') { + setRejectButtonStatus(getRequestStatus()); + } + }; + return ( <>
    From f3559f5928a620e868df43da3da9c8c8b8f4422c Mon Sep 17 00:00:00 2001 From: ilee2u Date: Fri, 15 Dec 2023 10:45:36 -0500 Subject: [PATCH 08/23] docs: comments for request initial states --- src/data/redux/requests/reducer.js | 9 +++ src/data/redux/requests/selectors.js | 17 +++++- .../components/ReviewExamAttemptModal.jsx | 55 ++++++++++++++----- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/data/redux/requests/reducer.js b/src/data/redux/requests/reducer.js index fd9ec54..0e340d8 100644 --- a/src/data/redux/requests/reducer.js +++ b/src/data/redux/requests/reducer.js @@ -2,8 +2,17 @@ import { createSlice } from '@reduxjs/toolkit'; import { RequestStates, RequestKeys } from 'data/constants'; +// NOTE: Anytime a new request is coded, or when a request has its RequestKey changed, +// It must be added to this variable in the following format: + // [RequestKeys.fetchCourseExams]: { status: RequestStates.inactive } +// This order to prevents an error form being thrown when we call for the status of these requests, +// as calling for the status of an undefined/yet-to-be called request will throw an error. +// See src/data/redux/requests/selectors.js for more info. const initialState = { [RequestKeys.fetchCourseExams]: { status: RequestStates.inactive }, + [RequestKeys.fetchExamAttempts]: { status: RequestStates.inactive }, + [RequestKeys.deleteExamAttempt]: { status: RequestStates.inactive }, + [RequestKeys.modifyExamAttempt]: { status: RequestStates.inactive }, }; // eslint-disable-next-line no-unused-vars diff --git a/src/data/redux/requests/selectors.js b/src/data/redux/requests/selectors.js index 6842681..64535ff 100644 --- a/src/data/redux/requests/selectors.js +++ b/src/data/redux/requests/selectors.js @@ -2,7 +2,22 @@ import { RequestStates } from 'data/constants'; export const requestStatus = (state, { requestKey }) => state.requests[requestKey]; -const statusSelector = (fn) => (requestKey) => (state) => fn(state.requests[requestKey]); + +const statusSelector = (fn) => (requestKey) => (state) => { + + // console.log("requestKey:"+requestKey) + if (requestKey === 'modifyExamAttempt') { + console.log("status:"+state.requests[requestKey].status); + // debugger; + } + if (state.requests[requestKey] === undefined) { + throw new TypeError(`The request with RequestKey ${requestKey} is missing an initial state. \ + If you see this error, then you must initialize your request statuses in the initialState variable in \ + src/data/redux/requests/reducer.js \n\n \ + This error replaces this otherwise vague error: TypeError: Cannot destructure property 'status' of '_ref3' as it is undefined.`) + } + fn(state.requests[requestKey]); +}; export const isInactive = ({ status }) => status === RequestStates.inactive; export const isPending = ({ status }) => status === RequestStates.pending; diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index c96bb04..0abd5a3 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Button, useToggle, ModalDialog, ActionRow, StatefulButton, } from '@edx/paragon'; @@ -33,6 +33,9 @@ const ReviewExamAttemptModal = ({ const [rejectButtonStatus, setRejectButtonStatus] = useState(''); const [verifyButtonStatus, setVerifyButtonStatus] = useState(''); const { formatMessage } = useIntl(); + const isPending = reduxHooks.useRequestIsPending(constants.RequestKeys.modifyExamAttempt); + const isCompleted = reduxHooks.useRequestIsCompleted(constants.RequestKeys.modifyExamAttempt); + const isError = reduxHooks.useRequestError(constants.RequestKeys.modifyExamAttempt); const getButton = (status) => { if (ReviewRequiredStatuses.includes(status)) { @@ -91,10 +94,6 @@ const ReviewExamAttemptModal = ({ variant: 'primary', }; - const isPending = reduxHooks.useRequestIsPending(constants.RequestKeys.modifyExamAttempt); - const isCompleted = reduxHooks.useRequestIsCompleted(constants.RequestKeys.modifyExamAttempt); - const isError = reduxHooks.useRequestError(constants.RequestKeys.modifyExamAttempt); - const getRequestStatus = () => { if (isPending) { return 'pending'; @@ -106,15 +105,37 @@ const ReviewExamAttemptModal = ({ return ''; }; - // Set the status of the button - const updateButtonStatus = (buttonType) => { - if (buttonType == 'verify') { - setVerifyButtonStatus(getRequestStatus()); - } else if (buttonType == 'reject') { - setRejectButtonStatus(getRequestStatus()); + useEffect(() => { + const status = ''; + if (isPending) { + status = 'pending'; + } else if (isCompleted) { + status = 'complete'; + } else if (isError) { + status = 'error'; + } + console.log("STATUS: ", isPending, isCompleted, isError); + }, [isPending, isCompleted, isError]) + + // insert above code here later + const updateAttemptStatusStuff = (action) => { + // Set the status of the button + const status = ''; + if (isPending) { + status = 'pending'; + } else if (isCompleted) { + status = 'complete'; + } else if (isError) { + status = 'error'; } - }; + if (action == 'verify') { + setVerifyButtonStatus(); + } else if (action == 'reject') { + setRejectButtonStatus(); + } + } + return ( <>
    @@ -163,8 +184,10 @@ const ReviewExamAttemptModal = ({ {...VerifyButtonProps} variant="success" onClick={async e => { // eslint-disable-line no-unused-vars - updateButtonStatus('reject'); - await modifyExamAttempt(attemptId, constants.ExamAttemptActions.verify); + // updateButtonStatus('reject'); + modifyExamAttempt(attemptId, constants.ExamAttemptActions.verify); + updateAttemptStatusStuff(constants.ExamAttemptActions.verify); + // modifyExamAttempt(constants.ExamAttemptActions.verify); // updateButtonStatus() }} > @@ -177,9 +200,11 @@ const ReviewExamAttemptModal = ({ state={rejectButtonStatus} {...RejectButtonProps} variant="danger" - onClick={e => { // eslint-disable-line no-unused-vars + onClick={async e => { // eslint-disable-line no-unused-vars // setRejectButtonStatus('pending'); modifyExamAttempt(attemptId, constants.ExamAttemptActions.reject); + updateAttemptStatusStuff(constants.ExamAttemptActions.reject); + // modifyExamAttempt(constants.ExamAttemptActions.reject); // setRejectButtonStatus('complete'); }} > From ee15faf6cefe4c787b9ba36f0e088025244a4a4d Mon Sep 17 00:00:00 2001 From: ilee2u Date: Wed, 20 Dec 2023 14:37:06 -0500 Subject: [PATCH 09/23] feat: cleaned code, moved redux hook -> utils --- src/data/redux/requests/selectors.js | 4 +- .../components/ReviewExamAttemptModal.jsx | 63 +------------------ src/pages/ExamsPage/data/api.js | 6 -- src/pages/ExamsPage/utils.js | 12 ++++ 4 files changed, 17 insertions(+), 68 deletions(-) diff --git a/src/data/redux/requests/selectors.js b/src/data/redux/requests/selectors.js index 64535ff..cd8eacb 100644 --- a/src/data/redux/requests/selectors.js +++ b/src/data/redux/requests/selectors.js @@ -7,7 +7,7 @@ const statusSelector = (fn) => (requestKey) => (state) => { // console.log("requestKey:"+requestKey) if (requestKey === 'modifyExamAttempt') { - console.log("status:"+state.requests[requestKey].status); + console.log("status: "+state.requests[requestKey].status); // debugger; } if (state.requests[requestKey] === undefined) { @@ -16,7 +16,7 @@ const statusSelector = (fn) => (requestKey) => (state) => { src/data/redux/requests/reducer.js \n\n \ This error replaces this otherwise vague error: TypeError: Cannot destructure property 'status' of '_ref3' as it is undefined.`) } - fn(state.requests[requestKey]); + return fn(state.requests[requestKey]); }; export const isInactive = ({ status }) => status === RequestStates.inactive; diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index 0abd5a3..00d56db 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; -import { useEffect, useState } from 'react'; import { Button, useToggle, ModalDialog, ActionRow, StatefulButton, } from '@edx/paragon'; @@ -9,8 +8,7 @@ import { Info, Warning } from '@edx/paragon/icons'; import * as constants from 'data/constants'; import { useExamsData, useModifyExamAttempt } from '../hooks'; import messages from '../messages'; -import { getLaunchUrlByExamId, getMessageLabelForStatus } from '../utils'; -import * as reduxHooks from 'data/redux/hooks'; +import { getLaunchUrlByExamId, getMessageLabelForStatus, getUpdateRequestStatusFromRedux } from '../utils'; const ReviewableStatuses = [ constants.ExamAttemptStatus.error, @@ -30,12 +28,7 @@ const ReviewExamAttemptModal = ({ const [isOpen, open, close] = useToggle(false); const modifyExamAttempt = useModifyExamAttempt(); const { currentExam } = useExamsData(); - const [rejectButtonStatus, setRejectButtonStatus] = useState(''); - const [verifyButtonStatus, setVerifyButtonStatus] = useState(''); const { formatMessage } = useIntl(); - const isPending = reduxHooks.useRequestIsPending(constants.RequestKeys.modifyExamAttempt); - const isCompleted = reduxHooks.useRequestIsCompleted(constants.RequestKeys.modifyExamAttempt); - const isError = reduxHooks.useRequestError(constants.RequestKeys.modifyExamAttempt); const getButton = (status) => { if (ReviewRequiredStatuses.includes(status)) { @@ -94,48 +87,6 @@ const ReviewExamAttemptModal = ({ variant: 'primary', }; - const getRequestStatus = () => { - if (isPending) { - return 'pending'; - } else if (isCompleted) { - return 'complete'; - } else if (isError) { - return 'error'; - }; - return ''; - }; - - useEffect(() => { - const status = ''; - if (isPending) { - status = 'pending'; - } else if (isCompleted) { - status = 'complete'; - } else if (isError) { - status = 'error'; - } - console.log("STATUS: ", isPending, isCompleted, isError); - }, [isPending, isCompleted, isError]) - - // insert above code here later - const updateAttemptStatusStuff = (action) => { - // Set the status of the button - const status = ''; - if (isPending) { - status = 'pending'; - } else if (isCompleted) { - status = 'complete'; - } else if (isError) { - status = 'error'; - } - - if (action == 'verify') { - setVerifyButtonStatus(); - } else if (action == 'reject') { - setRejectButtonStatus(); - } - } - return ( <>
    @@ -180,15 +131,11 @@ const ReviewExamAttemptModal = ({ {attemptStatus !== constants.ExamAttemptStatus.verified && ( { // eslint-disable-line no-unused-vars - // updateButtonStatus('reject'); modifyExamAttempt(attemptId, constants.ExamAttemptActions.verify); - updateAttemptStatusStuff(constants.ExamAttemptActions.verify); - // modifyExamAttempt(constants.ExamAttemptActions.verify); - // updateButtonStatus() }} > {formatMessage(messages.ReviewExamAttemptModalVerify)} @@ -197,15 +144,11 @@ const ReviewExamAttemptModal = ({ {attemptStatus !== constants.ExamAttemptStatus.rejected && ( { // eslint-disable-line no-unused-vars - // setRejectButtonStatus('pending'); modifyExamAttempt(attemptId, constants.ExamAttemptActions.reject); - updateAttemptStatusStuff(constants.ExamAttemptActions.reject); - // modifyExamAttempt(constants.ExamAttemptActions.reject); - // setRejectButtonStatus('complete'); }} > {formatMessage(messages.ReviewExamAttemptModalReject)} diff --git a/src/pages/ExamsPage/data/api.js b/src/pages/ExamsPage/data/api.js index 20c62c7..3477420 100644 --- a/src/pages/ExamsPage/data/api.js +++ b/src/pages/ExamsPage/data/api.js @@ -26,13 +26,7 @@ export async function deleteExamAttempt(attemptId) { return response.data; } -// function sleep(ms) { -// return new Promise(resolve => setTimeout(resolve, ms)); -// } - export async function modifyExamAttempt(attemptId, action) { - // Temporary sleep code so I can see the button in the pending state, will remove in final version - // await sleep(2000); const url = `${getExamsBaseUrl()}/api/v1/exams/attempt/${attemptId}`; const payload = { action }; const response = await getAuthenticatedHttpClient().put(url, payload); diff --git a/src/pages/ExamsPage/utils.js b/src/pages/ExamsPage/utils.js index bbbf393..cc2f6d6 100644 --- a/src/pages/ExamsPage/utils.js +++ b/src/pages/ExamsPage/utils.js @@ -1,6 +1,9 @@ import messages from './messages'; import { getExamsBaseUrl } from './data/api'; +const isPending = reduxHooks.useRequestIsPending(constants.RequestKeys.modifyExamAttempt); +const isError = reduxHooks.useRequestError(constants.RequestKeys.modifyExamAttempt); + const examAttemptStatusLabels = { created: messages.statusLabelCreated, download_software_clicked: messages.statusLabelDownloadSoftwareClicked, @@ -18,3 +21,12 @@ const examAttemptStatusLabels = { export const getLaunchUrlByExamId = (id) => `${getExamsBaseUrl()}/lti/exam/${id}/instructor_tool`; export const getMessageLabelForStatus = (status) => examAttemptStatusLabels[status]; + +export const getUpdateRequestStatusFromRedux = () => { + if (isPending) { + return 'pending'; + } else if (isError) { + return 'error'; + } + return; +}; From 4aa80223f374f7dc6b1ef27674384994d792fbb7 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Wed, 20 Dec 2023 14:55:13 -0500 Subject: [PATCH 10/23] temp: rollback point for moving utils code --- src/data/redux/requests/reducer.js | 2 +- src/data/redux/requests/selectors.js | 9 +-------- .../ExamsPage/components/ResetExamAttemptModal.jsx | 7 +++---- .../ExamsPage/components/ReviewExamAttemptModal.jsx | 6 +++--- src/pages/ExamsPage/utils.js | 12 ++++++------ 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/data/redux/requests/reducer.js b/src/data/redux/requests/reducer.js index 0e340d8..4f0dd85 100644 --- a/src/data/redux/requests/reducer.js +++ b/src/data/redux/requests/reducer.js @@ -4,7 +4,7 @@ import { RequestStates, RequestKeys } from 'data/constants'; // NOTE: Anytime a new request is coded, or when a request has its RequestKey changed, // It must be added to this variable in the following format: - // [RequestKeys.fetchCourseExams]: { status: RequestStates.inactive } +// [RequestKeys.fetchCourseExams]: { status: RequestStates.inactive } // This order to prevents an error form being thrown when we call for the status of these requests, // as calling for the status of an undefined/yet-to-be called request will throw an error. // See src/data/redux/requests/selectors.js for more info. diff --git a/src/data/redux/requests/selectors.js b/src/data/redux/requests/selectors.js index cd8eacb..56329ec 100644 --- a/src/data/redux/requests/selectors.js +++ b/src/data/redux/requests/selectors.js @@ -2,19 +2,12 @@ import { RequestStates } from 'data/constants'; export const requestStatus = (state, { requestKey }) => state.requests[requestKey]; - const statusSelector = (fn) => (requestKey) => (state) => { - - // console.log("requestKey:"+requestKey) - if (requestKey === 'modifyExamAttempt') { - console.log("status: "+state.requests[requestKey].status); - // debugger; - } if (state.requests[requestKey] === undefined) { throw new TypeError(`The request with RequestKey ${requestKey} is missing an initial state. \ If you see this error, then you must initialize your request statuses in the initialState variable in \ src/data/redux/requests/reducer.js \n\n \ - This error replaces this otherwise vague error: TypeError: Cannot destructure property 'status' of '_ref3' as it is undefined.`) + This error replaces this otherwise vague error: TypeError: Cannot destructure property 'status' of '_ref3' as it is undefined.`); } return fn(state.requests[requestKey]); }; diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx index 2f0a335..1e50b23 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx @@ -1,17 +1,17 @@ import PropTypes from 'prop-types'; -import { useState } from 'react'; import { Button, useToggle, ModalDialog, ActionRow, StatefulButton, } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; +import * as constants from 'data/constants'; import { useDeleteExamAttempt } from '../hooks'; import messages from '../messages'; +import { getRequestStatusFromRedux } from '../utils'; const ResetExamAttemptModal = ({ username, examName, attemptId }) => { const [isOpen, open, close] = useToggle(false); const resetExamAttempt = useDeleteExamAttempt(); - const [resetButtonStatus, setResetButtonStatus] = useState(''); const { formatMessage } = useIntl(); const ResetButtonProps = { @@ -60,11 +60,10 @@ const ResetExamAttemptModal = ({ username, examName, attemptId }) => { {formatMessage(messages.ResetExamAttemptModalCancel)} { // eslint-disable-line no-unused-vars - setResetButtonStatus('pending'); resetExamAttempt(attemptId); }} > diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index 00d56db..840fbe8 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -8,7 +8,7 @@ import { Info, Warning } from '@edx/paragon/icons'; import * as constants from 'data/constants'; import { useExamsData, useModifyExamAttempt } from '../hooks'; import messages from '../messages'; -import { getLaunchUrlByExamId, getMessageLabelForStatus, getUpdateRequestStatusFromRedux } from '../utils'; +import { getLaunchUrlByExamId, getMessageLabelForStatus, getRequestStatusFromRedux } from '../utils'; const ReviewableStatuses = [ constants.ExamAttemptStatus.error, @@ -131,7 +131,7 @@ const ReviewExamAttemptModal = ({ {attemptStatus !== constants.ExamAttemptStatus.verified && ( { // eslint-disable-line no-unused-vars @@ -144,7 +144,7 @@ const ReviewExamAttemptModal = ({ {attemptStatus !== constants.ExamAttemptStatus.rejected && ( { // eslint-disable-line no-unused-vars diff --git a/src/pages/ExamsPage/utils.js b/src/pages/ExamsPage/utils.js index cc2f6d6..1bfc258 100644 --- a/src/pages/ExamsPage/utils.js +++ b/src/pages/ExamsPage/utils.js @@ -1,9 +1,7 @@ +import * as reduxHooks from 'data/redux/hooks'; import messages from './messages'; import { getExamsBaseUrl } from './data/api'; -const isPending = reduxHooks.useRequestIsPending(constants.RequestKeys.modifyExamAttempt); -const isError = reduxHooks.useRequestError(constants.RequestKeys.modifyExamAttempt); - const examAttemptStatusLabels = { created: messages.statusLabelCreated, download_software_clicked: messages.statusLabelDownloadSoftwareClicked, @@ -22,11 +20,13 @@ export const getLaunchUrlByExamId = (id) => `${getExamsBaseUrl()}/lti/exam/${id} export const getMessageLabelForStatus = (status) => examAttemptStatusLabels[status]; -export const getUpdateRequestStatusFromRedux = () => { +export const getRequestStatusFromRedux = (requestKey) => { + const isPending = reduxHooks.useRequestIsPending(requestKey); + const isError = reduxHooks.useRequestError(requestKey); if (isPending) { return 'pending'; - } else if (isError) { + } if (isError) { return 'error'; } - return; + return ''; }; From d2f0673423845d2b5c436e23a23442d0daa1d7c4 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Wed, 3 Jan 2024 14:46:01 -0500 Subject: [PATCH 11/23] temp: tried moving utils to redux folder - it didn't work --- src/data/redux/utils.js | 30 +++++++++++++++++++ .../components/ResetExamAttemptModal.jsx | 2 +- .../components/ReviewExamAttemptModal.jsx | 3 +- src/pages/ExamsPage/utils.js | 29 ------------------ 4 files changed, 33 insertions(+), 31 deletions(-) create mode 100644 src/data/redux/utils.js diff --git a/src/data/redux/utils.js b/src/data/redux/utils.js new file mode 100644 index 0000000..8d938b3 --- /dev/null +++ b/src/data/redux/utils.js @@ -0,0 +1,30 @@ +import * as reduxHooks from 'data/redux/hooks'; +import messages from 'pages/ExamsPage/messages'; + + +const examAttemptStatusLabels = { + created: messages.statusLabelCreated, + download_software_clicked: messages.statusLabelDownloadSoftwareClicked, + ready_to_start: messages.statusLabelReadyToStart, + started: messages.statusLabelStarted, + ready_to_submit: messages.statusLabelReadyToSubmit, + submitted: messages.statusLabelSubmitted, + verified: messages.statusLabelVerified, + rejected: messages.statusLabelRejected, + expired: messages.statusLabelExpired, + second_review_required: messages.statusLabelSecondReviewRequired, + error: messages.statusLabelError, +}; + +export const getMessageLabelForStatus = (status) => examAttemptStatusLabels[status]; + +export const getRequestStatusFromRedux = (requestKey) => { + const isPending = reduxHooks.useRequestIsPending(requestKey); + const isError = reduxHooks.useRequestError(requestKey); + if (isPending) { + return 'pending'; + } if (isError) { + return 'error'; + } + return ''; +}; \ No newline at end of file diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx index 1e50b23..1c2051c 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx @@ -7,7 +7,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import * as constants from 'data/constants'; import { useDeleteExamAttempt } from '../hooks'; import messages from '../messages'; -import { getRequestStatusFromRedux } from '../utils'; +import { getRequestStatusFromRedux } from 'data/redux/utils'; const ResetExamAttemptModal = ({ username, examName, attemptId }) => { const [isOpen, open, close] = useToggle(false); diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index 840fbe8..d735cc7 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -8,7 +8,8 @@ import { Info, Warning } from '@edx/paragon/icons'; import * as constants from 'data/constants'; import { useExamsData, useModifyExamAttempt } from '../hooks'; import messages from '../messages'; -import { getLaunchUrlByExamId, getMessageLabelForStatus, getRequestStatusFromRedux } from '../utils'; +import { getLaunchUrlByExamId } from '../utils'; +import { getMessageLabelForStatus, getRequestStatusFromRedux } from 'data/redux/utils'; const ReviewableStatuses = [ constants.ExamAttemptStatus.error, diff --git a/src/pages/ExamsPage/utils.js b/src/pages/ExamsPage/utils.js index 1bfc258..4aacde8 100644 --- a/src/pages/ExamsPage/utils.js +++ b/src/pages/ExamsPage/utils.js @@ -1,32 +1,3 @@ -import * as reduxHooks from 'data/redux/hooks'; -import messages from './messages'; import { getExamsBaseUrl } from './data/api'; -const examAttemptStatusLabels = { - created: messages.statusLabelCreated, - download_software_clicked: messages.statusLabelDownloadSoftwareClicked, - ready_to_start: messages.statusLabelReadyToStart, - started: messages.statusLabelStarted, - ready_to_submit: messages.statusLabelReadyToSubmit, - submitted: messages.statusLabelSubmitted, - verified: messages.statusLabelVerified, - rejected: messages.statusLabelRejected, - expired: messages.statusLabelExpired, - second_review_required: messages.statusLabelSecondReviewRequired, - error: messages.statusLabelError, -}; - export const getLaunchUrlByExamId = (id) => `${getExamsBaseUrl()}/lti/exam/${id}/instructor_tool`; - -export const getMessageLabelForStatus = (status) => examAttemptStatusLabels[status]; - -export const getRequestStatusFromRedux = (requestKey) => { - const isPending = reduxHooks.useRequestIsPending(requestKey); - const isError = reduxHooks.useRequestError(requestKey); - if (isPending) { - return 'pending'; - } if (isError) { - return 'error'; - } - return ''; -}; From cf6e2fe39efdced6ff63607d558288180074d765 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Wed, 3 Jan 2024 15:05:23 -0500 Subject: [PATCH 12/23] chore: nit --- src/data/redux/utils.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/data/redux/utils.js b/src/data/redux/utils.js index 8d938b3..370060b 100644 --- a/src/data/redux/utils.js +++ b/src/data/redux/utils.js @@ -1,7 +1,6 @@ import * as reduxHooks from 'data/redux/hooks'; import messages from 'pages/ExamsPage/messages'; - const examAttemptStatusLabels = { created: messages.statusLabelCreated, download_software_clicked: messages.statusLabelDownloadSoftwareClicked, @@ -27,4 +26,4 @@ export const getRequestStatusFromRedux = (requestKey) => { return 'error'; } return ''; -}; \ No newline at end of file +}; From 9efb629ba217403ce35de2384e2ff130e0e6f99c Mon Sep 17 00:00:00 2001 From: ilee2u Date: Wed, 3 Jan 2024 15:58:50 -0500 Subject: [PATCH 13/23] temp: moving req. status to custom hook --- src/data/redux/utils.js | 29 ------------------- .../components/ResetExamAttemptModal.jsx | 6 ++-- .../components/ReviewExamAttemptModal.jsx | 10 +++---- src/pages/ExamsPage/hooks.js | 13 +++++++++ src/pages/ExamsPage/utils.js | 17 +++++++++++ 5 files changed, 38 insertions(+), 37 deletions(-) delete mode 100644 src/data/redux/utils.js diff --git a/src/data/redux/utils.js b/src/data/redux/utils.js deleted file mode 100644 index 370060b..0000000 --- a/src/data/redux/utils.js +++ /dev/null @@ -1,29 +0,0 @@ -import * as reduxHooks from 'data/redux/hooks'; -import messages from 'pages/ExamsPage/messages'; - -const examAttemptStatusLabels = { - created: messages.statusLabelCreated, - download_software_clicked: messages.statusLabelDownloadSoftwareClicked, - ready_to_start: messages.statusLabelReadyToStart, - started: messages.statusLabelStarted, - ready_to_submit: messages.statusLabelReadyToSubmit, - submitted: messages.statusLabelSubmitted, - verified: messages.statusLabelVerified, - rejected: messages.statusLabelRejected, - expired: messages.statusLabelExpired, - second_review_required: messages.statusLabelSecondReviewRequired, - error: messages.statusLabelError, -}; - -export const getMessageLabelForStatus = (status) => examAttemptStatusLabels[status]; - -export const getRequestStatusFromRedux = (requestKey) => { - const isPending = reduxHooks.useRequestIsPending(requestKey); - const isError = reduxHooks.useRequestError(requestKey); - if (isPending) { - return 'pending'; - } if (isError) { - return 'error'; - } - return ''; -}; diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx index 1c2051c..9b2ce40 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx @@ -5,13 +5,13 @@ import { } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import * as constants from 'data/constants'; -import { useDeleteExamAttempt } from '../hooks'; +import { useDeleteExamAttempt, useRequestStatusFromRedux } from '../hooks'; import messages from '../messages'; -import { getRequestStatusFromRedux } from 'data/redux/utils'; const ResetExamAttemptModal = ({ username, examName, attemptId }) => { const [isOpen, open, close] = useToggle(false); const resetExamAttempt = useDeleteExamAttempt(); + const getRequestStatus = useRequestStatusFromRedux(); const { formatMessage } = useIntl(); const ResetButtonProps = { @@ -60,7 +60,7 @@ const ResetExamAttemptModal = ({ username, examName, attemptId }) => { {formatMessage(messages.ResetExamAttemptModalCancel)} { // eslint-disable-line no-unused-vars diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index d735cc7..1f3cb4e 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -6,10 +6,9 @@ import { import { useIntl } from '@edx/frontend-platform/i18n'; import { Info, Warning } from '@edx/paragon/icons'; import * as constants from 'data/constants'; -import { useExamsData, useModifyExamAttempt } from '../hooks'; +import { useExamsData, useModifyExamAttempt, useRequestStatusFromRedux } from '../hooks'; import messages from '../messages'; -import { getLaunchUrlByExamId } from '../utils'; -import { getMessageLabelForStatus, getRequestStatusFromRedux } from 'data/redux/utils'; +import { getLaunchUrlByExamId, getMessageLabelForStatus } from '../utils'; const ReviewableStatuses = [ constants.ExamAttemptStatus.error, @@ -28,6 +27,7 @@ const ReviewExamAttemptModal = ({ }) => { const [isOpen, open, close] = useToggle(false); const modifyExamAttempt = useModifyExamAttempt(); + const getRequestStatus = useRequestStatusFromRedux(); const { currentExam } = useExamsData(); const { formatMessage } = useIntl(); @@ -132,7 +132,7 @@ const ReviewExamAttemptModal = ({ {attemptStatus !== constants.ExamAttemptStatus.verified && ( { // eslint-disable-line no-unused-vars @@ -145,7 +145,7 @@ const ReviewExamAttemptModal = ({ {attemptStatus !== constants.ExamAttemptStatus.rejected && ( { // eslint-disable-line no-unused-vars diff --git a/src/pages/ExamsPage/hooks.js b/src/pages/ExamsPage/hooks.js index 25e8fd3..ca6aed8 100644 --- a/src/pages/ExamsPage/hooks.js +++ b/src/pages/ExamsPage/hooks.js @@ -104,3 +104,16 @@ export const useExamAttemptsData = () => { const attemptsList = useSelector(selectors.courseExamAttemptsList); return { attemptsList }; }; + +export const useRequestStatusFromRedux = (requestKey) => { + const isPending = reduxHooks.useRequestIsPending(requestKey); + const isError = reduxHooks.useRequestError(requestKey); + return () => { + if (isPending) { + return 'pending'; + } if (isError) { + return 'error'; + } + return ''; + }; +}; diff --git a/src/pages/ExamsPage/utils.js b/src/pages/ExamsPage/utils.js index 4aacde8..1baad04 100644 --- a/src/pages/ExamsPage/utils.js +++ b/src/pages/ExamsPage/utils.js @@ -1,3 +1,20 @@ import { getExamsBaseUrl } from './data/api'; +import messages from 'pages/ExamsPage/messages'; export const getLaunchUrlByExamId = (id) => `${getExamsBaseUrl()}/lti/exam/${id}/instructor_tool`; + +const examAttemptStatusLabels = { + created: messages.statusLabelCreated, + download_software_clicked: messages.statusLabelDownloadSoftwareClicked, + ready_to_start: messages.statusLabelReadyToStart, + started: messages.statusLabelStarted, + ready_to_submit: messages.statusLabelReadyToSubmit, + submitted: messages.statusLabelSubmitted, + verified: messages.statusLabelVerified, + rejected: messages.statusLabelRejected, + expired: messages.statusLabelExpired, + second_review_required: messages.statusLabelSecondReviewRequired, + error: messages.statusLabelError, +}; + +export const getMessageLabelForStatus = (status) => examAttemptStatusLabels[status]; \ No newline at end of file From 663e42808f499772737fbee2a1b66e290ef4d7f0 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Wed, 3 Jan 2024 16:45:01 -0500 Subject: [PATCH 14/23] fix: mocks in tests for new hook --- src/data/redux/requests/reducer.js | 1 + .../ExamsPage/components/AttemptList.test.jsx | 2 ++ .../components/ResetExamAttemptModal.jsx | 2 +- .../components/ResetExamAttemptModal.test.jsx | 4 +++- .../components/ReviewExamAttemptModal.jsx | 2 -- .../components/ReviewExamAttemptModal.test.jsx | 2 ++ src/pages/ExamsPage/index.test.jsx | 3 ++- src/pages/ExamsPage/messages.js | 15 --------------- 8 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/data/redux/requests/reducer.js b/src/data/redux/requests/reducer.js index 4f0dd85..c332556 100644 --- a/src/data/redux/requests/reducer.js +++ b/src/data/redux/requests/reducer.js @@ -13,6 +13,7 @@ const initialState = { [RequestKeys.fetchExamAttempts]: { status: RequestStates.inactive }, [RequestKeys.deleteExamAttempt]: { status: RequestStates.inactive }, [RequestKeys.modifyExamAttempt]: { status: RequestStates.inactive }, + [undefined]: { status: RequestStates.inactive }, }; // eslint-disable-next-line no-unused-vars diff --git a/src/pages/ExamsPage/components/AttemptList.test.jsx b/src/pages/ExamsPage/components/AttemptList.test.jsx index c7d3d40..2d9f07b 100644 --- a/src/pages/ExamsPage/components/AttemptList.test.jsx +++ b/src/pages/ExamsPage/components/AttemptList.test.jsx @@ -13,12 +13,14 @@ jest.mock('../hooks', () => ({ useDeleteExamAttempt: jest.fn(), useModifyExamAttempt: jest.fn(), useExamsData: jest.fn(), + useRequestStatusFromRedux: jest.fn(), })); describe('AttemptList', () => { beforeEach(() => { hooks.useDeleteExamAttempt.mockReturnValue(jest.fn()); hooks.useModifyExamAttempt.mockReturnValue(jest.fn()); + hooks.useRequestStatusFromRedux.mockReturnValue(jest.fn()); hooks.useExamsData.mockReturnValue(testUtils.defaultExamsData); }); it('Test that the AttemptList matches snapshot', () => { diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx index 9b2ce40..a6ef6f7 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx @@ -60,6 +60,7 @@ const ResetExamAttemptModal = ({ username, examName, attemptId }) => { {formatMessage(messages.ResetExamAttemptModalCancel)} { resetExamAttempt(attemptId); }} > - {formatMessage(messages.ResetExamAttemptModalConfirm)} diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx index 1b2644b..54dca49 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx @@ -6,6 +6,7 @@ import * as hooks from '../hooks'; jest.mock('../hooks', () => ({ useDeleteExamAttempt: jest.fn(), + useRequestStatusFromRedux: jest.fn(), })); const mockMakeNetworkRequest = jest.fn(); @@ -19,6 +20,7 @@ describe('ResetExamAttemptModal', () => { beforeEach(() => { jest.restoreAllMocks(); hooks.useDeleteExamAttempt.mockReturnValue(mockMakeNetworkRequest); + hooks.useRequestStatusFromRedux.mockReturnValue(mockMakeNetworkRequest); }); it('Test that the ResetExamAttemptModal matches snapshot', () => { expect(render(resetModal)).toMatchSnapshot(); @@ -40,7 +42,7 @@ describe('ResetExamAttemptModal', () => { jest.spyOn(hooks, 'useDeleteExamAttempt').mockImplementation(() => mockDeleteExamAttempt); render(resetModal); screen.getByText('Reset').click(); - screen.getByText('Yes, I\'m Sure').click(); + screen.getByTestId('reset-stateful-button').click(); expect(mockDeleteExamAttempt).toHaveBeenCalledWith(0); }); }); diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index 1f3cb4e..87124b9 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -139,7 +139,6 @@ const ReviewExamAttemptModal = ({ modifyExamAttempt(attemptId, constants.ExamAttemptActions.verify); }} > - {formatMessage(messages.ReviewExamAttemptModalVerify)} )} {attemptStatus !== constants.ExamAttemptStatus.rejected @@ -152,7 +151,6 @@ const ReviewExamAttemptModal = ({ modifyExamAttempt(attemptId, constants.ExamAttemptActions.reject); }} > - {formatMessage(messages.ReviewExamAttemptModalReject)} )} diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx index 4b63b99..41b7b0c 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx @@ -8,6 +8,7 @@ import * as hooks from '../hooks'; jest.mock('../hooks', () => ({ useModifyExamAttempt: jest.fn(), + useRequestStatusFromRedux: jest.fn(), useExamsData: jest.fn(), })); @@ -31,6 +32,7 @@ describe('ReviewExamAttemptModal', () => { beforeEach(() => { jest.restoreAllMocks(); hooks.useModifyExamAttempt.mockReturnValue(mockMakeNetworkRequest); + hooks.useRequestStatusFromRedux.mockReturnValue(mockMakeNetworkRequest); hooks.useExamsData.mockReturnValue(testUtils.defaultExamsData); }); it('Test that the ReviewExamAttemptModal matches snapshot', () => { diff --git a/src/pages/ExamsPage/index.test.jsx b/src/pages/ExamsPage/index.test.jsx index 475cebd..8729101 100644 --- a/src/pages/ExamsPage/index.test.jsx +++ b/src/pages/ExamsPage/index.test.jsx @@ -14,6 +14,7 @@ jest.mock('./hooks', () => ({ useFetchExamAttempts: jest.fn(), useDeleteExamAttempt: jest.fn(), useModifyExamAttempt: jest.fn(), + useRequestStatusFromRedux: jest.fn(), })); describe('ExamsPage', () => { @@ -24,7 +25,7 @@ describe('ExamsPage', () => { test('exams and attempts loaded', () => { // temporary, this won't fire on useEffect once we have an exam selection handler hooks.useFetchExamAttempts.mockReturnValue(jest.fn()); - + hooks.useRequestStatusFromRedux.mockReturnValue(jest.fn()); hooks.useExamsData.mockReturnValue(testUtils.defaultExamsData); expect(render()).toMatchSnapshot(); }); diff --git a/src/pages/ExamsPage/messages.js b/src/pages/ExamsPage/messages.js index 34898f2..9e5a6b2 100644 --- a/src/pages/ExamsPage/messages.js +++ b/src/pages/ExamsPage/messages.js @@ -151,11 +151,6 @@ const messages = defineMessages({ defaultMessage: 'Cancel', description: 'Text for the button to cancel resetting an exam attempt', }, - ResetExamAttemptModalConfirm: { - id: 'ResetExamAttemptModal.confirm_button', - defaultMessage: 'Yes, I\'m Sure', - description: 'Text for the button to confirm the reset of an exam attempt', - }, // ReviewExamAttemptModal ReviewableButtonTitle: { @@ -198,16 +193,6 @@ const messages = defineMessages({ defaultMessage: 'Cancel', description: 'Text for the button to cancel reviewing an exam attempt', }, - ReviewExamAttemptModalVerify: { - id: 'ReviewExamAttemptModal.verify_button', - defaultMessage: 'Verify', - description: 'Text for the button to verify an exam attempt', - }, - ReviewExamAttemptModalReject: { - id: 'ReviewExamAttemptModal.reject_button', - defaultMessage: 'Reject', - description: 'Text for the button to reject an exam attempt', - }, // Reset Button Labels ResetExamAttemptButtonDefaultLabel: { From 2203ebff1e9374c6ece2d36158b3fbf0e9139800 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Thu, 4 Jan 2024 09:30:40 -0500 Subject: [PATCH 15/23] temp: trying to make new test work --- src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx index 41b7b0c..ae2f9c2 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx @@ -56,6 +56,8 @@ describe('ReviewExamAttemptModal', () => { render(reviewModal()); screen.getByText('Review Required').click(); screen.getByText('Verify').click(); + // Using queryByText here allows the function to throw + expect(screen.queryByText('Verifying...')).toBeInTheDocument(); expect(mockModifyExamAttempt).toHaveBeenCalledWith(0, constants.ExamAttemptActions.verify); }); it('Clicking the Reject button calls the modify exam attempt hook', () => { @@ -64,6 +66,7 @@ describe('ReviewExamAttemptModal', () => { render(reviewModal()); screen.getByText('Review Required').click(); screen.getByText('Reject').click(); + expect(screen.queryByText('Rejecting...')).toBeInTheDocument(); expect(mockModifyExamAttempt).toHaveBeenCalledWith(0, constants.ExamAttemptActions.reject); }); it('Does not show the modal if the attempt is not reviewable', () => { From 77d3b52fefeb434c6997971c637324e6f0b97f35 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Thu, 4 Jan 2024 10:42:55 -0500 Subject: [PATCH 16/23] temp: fiddling with test --- .../components/ReviewExamAttemptModal.test.jsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx index ae2f9c2..bc1889b 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, waitFor, screen } from '@testing-library/react'; import * as constants from 'data/constants'; import ReviewExamAttemptModal from './ReviewExamAttemptModal'; @@ -50,14 +50,18 @@ describe('ReviewExamAttemptModal', () => { // Using queryByText here allows the function to throw expect(screen.queryByText('Update review status')).not.toBeInTheDocument(); }); - it('Clicking the Verify button calls the modify exam attempt hook', () => { + it('Clicking the Verify button calls the modify exam attempt hook', async () => { const mockModifyExamAttempt = jest.fn(); jest.spyOn(hooks, 'useModifyExamAttempt').mockImplementation(() => mockModifyExamAttempt); render(reviewModal()); screen.getByText('Review Required').click(); screen.getByText('Verify').click(); - // Using queryByText here allows the function to throw - expect(screen.queryByText('Verifying...')).toBeInTheDocument(); + // expect(screen.getByRole('button', { name: 'Verify' })).toBeInTheDocument(); + // screen.getByRole('button', { name: 'Verify' }).click(); + await waitFor(() => { + expect(queryByText('Verifying...')).toBeInTheDocument() + }) + // await screen.findByText('Verifying...'); expect(mockModifyExamAttempt).toHaveBeenCalledWith(0, constants.ExamAttemptActions.verify); }); it('Clicking the Reject button calls the modify exam attempt hook', () => { From 00a565977d0d881028842ef265291a86d1f03c67 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Mon, 8 Jan 2024 11:48:26 -0500 Subject: [PATCH 17/23] test: complete test for button state --- .../ReviewExamAttemptModal.test.jsx | 35 +++++++++++++++---- src/pages/ExamsPage/data/selectors.js | 4 ++- src/pages/ExamsPage/hooks.js | 2 ++ src/testUtils.js | 31 +++++++++++++++- 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx index bc1889b..b75c941 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx @@ -5,6 +5,18 @@ import ReviewExamAttemptModal from './ReviewExamAttemptModal'; import * as testUtils from '../../../testUtils'; import * as hooks from '../hooks'; +import * as api from '../data/api'; + +jest.mock('../data/api', () => { + const originalModule = jest.requireActual('../data/api'); + + return { + __esModule: true, + ...originalModule, + // Only modifyExamAttempt is mocked and everything else from api.js is the same + modifyExamAttempt: jest.fn(), + }; +}); jest.mock('../hooks', () => ({ useModifyExamAttempt: jest.fn(), @@ -50,27 +62,36 @@ describe('ReviewExamAttemptModal', () => { // Using queryByText here allows the function to throw expect(screen.queryByText('Update review status')).not.toBeInTheDocument(); }); + it('Clicking the Verify button displays the correct label based on the request state', () => { + api.modifyExamAttempt.mockResolvedValue({exam_id: 0}); + hooks.useRequestStatusFromRedux.mockReturnValue(() => 'pending'); // for testing button label state + const exams = { exams: testUtils.defaultExamsData }; + testUtils.renderWithProviders(reviewModal(), { preloadedState: exams }); + screen.getByText('Review Required').click(); + expect(screen.queryByText('Verifying...')).toBeInTheDocument(); // The button should be in the pending state + }); it('Clicking the Verify button calls the modify exam attempt hook', async () => { const mockModifyExamAttempt = jest.fn(); jest.spyOn(hooks, 'useModifyExamAttempt').mockImplementation(() => mockModifyExamAttempt); render(reviewModal()); screen.getByText('Review Required').click(); screen.getByText('Verify').click(); - // expect(screen.getByRole('button', { name: 'Verify' })).toBeInTheDocument(); - // screen.getByRole('button', { name: 'Verify' }).click(); - await waitFor(() => { - expect(queryByText('Verifying...')).toBeInTheDocument() - }) - // await screen.findByText('Verifying...'); expect(mockModifyExamAttempt).toHaveBeenCalledWith(0, constants.ExamAttemptActions.verify); }); + it('Clicking the Reject button displays the correct label based on the request state', () => { + api.modifyExamAttempt.mockResolvedValue({exam_id: 0}); + hooks.useRequestStatusFromRedux.mockReturnValue(() => 'pending'); // for testing button label state + const exams = { exams: testUtils.defaultExamsData }; + testUtils.renderWithProviders(reviewModal(), { preloadedState: exams }); + screen.getByText('Review Required').click(); + expect(screen.queryByText('Rejecting...')).toBeInTheDocument(); // The button should be in the pending state + }); it('Clicking the Reject button calls the modify exam attempt hook', () => { const mockModifyExamAttempt = jest.fn(); jest.spyOn(hooks, 'useModifyExamAttempt').mockImplementation(() => mockModifyExamAttempt); render(reviewModal()); screen.getByText('Review Required').click(); screen.getByText('Reject').click(); - expect(screen.queryByText('Rejecting...')).toBeInTheDocument(); expect(mockModifyExamAttempt).toHaveBeenCalledWith(0, constants.ExamAttemptActions.reject); }); it('Does not show the modal if the attempt is not reviewable', () => { diff --git a/src/pages/ExamsPage/data/selectors.js b/src/pages/ExamsPage/data/selectors.js index fc0117d..e2de600 100644 --- a/src/pages/ExamsPage/data/selectors.js +++ b/src/pages/ExamsPage/data/selectors.js @@ -1,7 +1,9 @@ export const courseExamsList = state => state.exams.examsList; // derived from currentExamIndex rather than duplicating in state -export const currentExam = state => state.exams.examsList[state.exams.currentExamIndex]; +export const currentExam = state => { + return state.exams.examsList[state.exams.currentExamIndex] +}; export const courseExamAttemptsList = state => state.exams.attemptsList; diff --git a/src/pages/ExamsPage/hooks.js b/src/pages/ExamsPage/hooks.js index ca6aed8..8d9014c 100644 --- a/src/pages/ExamsPage/hooks.js +++ b/src/pages/ExamsPage/hooks.js @@ -54,6 +54,8 @@ export const useDeleteExamAttempt = () => { }; export const useModifyExamAttempt = () => { + // something's up with useMakeNetworkRequest here? + // ok it says line 1473... but there's only like 200 in this file const makeNetworkRequest = reduxHooks.useMakeNetworkRequest(); const dispatch = useDispatch(); return (attemptId, action) => ( diff --git a/src/testUtils.js b/src/testUtils.js index 71264f4..260becc 100644 --- a/src/testUtils.js +++ b/src/testUtils.js @@ -1,4 +1,30 @@ /* eslint-disable import/prefer-default-export */ +import React from 'react' +import { render } from '@testing-library/react' +import { configureStore } from '@reduxjs/toolkit' +import { Provider } from 'react-redux' +// As a basic setup, import your same slice reducers +import { reducer as examReducer } from './pages/ExamsPage/data/reducer'; +import { reducer as requestsReducer } from './data/redux/requests'; + +// Hi, just added the line you just posted and just ran the tests again, i'm just looking at the results now +export function renderWithProviders( + ui, + { + preloadedState = {}, + // yeah doesn't work, so we need to pass it in + store = configureStore({ reducer: { exams: examReducer, requests: requestsReducer, }, preloadedState: preloadedState }), + ...renderOptions + } = {} +) { + function Wrapper({ children }) { + return {children} + } + + // Return an object with the store and all of RTL's query functions + return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) } +} + /** * Mocked formatMessage provided by react-intl */ @@ -19,12 +45,14 @@ export const formatMessage = (msg, values) => { return message; }; +// oh should i pass this into renderwithproviders? export const defaultExamsData = { examsList: [ { id: 1, name: 'exam1' }, ], + currentExamIndex: 0, currentExam: { id: 1, name: 'exam1' }, - setCurrentExam: jest.fn(), + // setCurrentExam: jest.fn(), }; export const defaultAttemptsData = { @@ -52,3 +80,4 @@ export const defaultAttemptsData = { attempt_id: 1, }], }; + From 46bfb7306031ae80c44da4ebd482e0307e0ff11b Mon Sep 17 00:00:00 2001 From: ilee2u Date: Mon, 8 Jan 2024 12:14:17 -0500 Subject: [PATCH 18/23] test: hooks test --- .../components/ResetExamAttemptModal.jsx | 3 +- .../components/ReviewExamAttemptModal.jsx | 6 ++-- .../ReviewExamAttemptModal.test.jsx | 13 ++++---- src/pages/ExamsPage/data/selectors.js | 4 +-- src/pages/ExamsPage/hooks.test.js | 22 ++++++++++++++ src/pages/ExamsPage/utils.js | 4 +-- src/testUtils.js | 30 ++----------------- 7 files changed, 35 insertions(+), 47 deletions(-) diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx index a6ef6f7..7c28f85 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx @@ -67,8 +67,7 @@ const ResetExamAttemptModal = ({ username, examName, attemptId }) => { onClick={e => { // eslint-disable-line no-unused-vars resetExamAttempt(attemptId); }} - > - + /> diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index 87124b9..fe1d594 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -138,8 +138,7 @@ const ReviewExamAttemptModal = ({ onClick={async e => { // eslint-disable-line no-unused-vars modifyExamAttempt(attemptId, constants.ExamAttemptActions.verify); }} - > - + /> )} {attemptStatus !== constants.ExamAttemptStatus.rejected && ( @@ -150,8 +149,7 @@ const ReviewExamAttemptModal = ({ onClick={async e => { // eslint-disable-line no-unused-vars modifyExamAttempt(attemptId, constants.ExamAttemptActions.reject); }} - > - + /> )} diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx index b75c941..5c36534 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx @@ -1,4 +1,4 @@ -import { render, waitFor, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import * as constants from 'data/constants'; import ReviewExamAttemptModal from './ReviewExamAttemptModal'; @@ -62,11 +62,10 @@ describe('ReviewExamAttemptModal', () => { // Using queryByText here allows the function to throw expect(screen.queryByText('Update review status')).not.toBeInTheDocument(); }); - it('Clicking the Verify button displays the correct label based on the request state', () => { - api.modifyExamAttempt.mockResolvedValue({exam_id: 0}); + it.only('Clicking the Verify button displays the correct label based on the request state', () => { + api.modifyExamAttempt.mockResolvedValue({ exam_id: 0 }); hooks.useRequestStatusFromRedux.mockReturnValue(() => 'pending'); // for testing button label state - const exams = { exams: testUtils.defaultExamsData }; - testUtils.renderWithProviders(reviewModal(), { preloadedState: exams }); + render(reviewModal()); screen.getByText('Review Required').click(); expect(screen.queryByText('Verifying...')).toBeInTheDocument(); // The button should be in the pending state }); @@ -79,10 +78,8 @@ describe('ReviewExamAttemptModal', () => { expect(mockModifyExamAttempt).toHaveBeenCalledWith(0, constants.ExamAttemptActions.verify); }); it('Clicking the Reject button displays the correct label based on the request state', () => { - api.modifyExamAttempt.mockResolvedValue({exam_id: 0}); + api.modifyExamAttempt.mockResolvedValue({ exam_id: 0 }); hooks.useRequestStatusFromRedux.mockReturnValue(() => 'pending'); // for testing button label state - const exams = { exams: testUtils.defaultExamsData }; - testUtils.renderWithProviders(reviewModal(), { preloadedState: exams }); screen.getByText('Review Required').click(); expect(screen.queryByText('Rejecting...')).toBeInTheDocument(); // The button should be in the pending state }); diff --git a/src/pages/ExamsPage/data/selectors.js b/src/pages/ExamsPage/data/selectors.js index e2de600..fc0117d 100644 --- a/src/pages/ExamsPage/data/selectors.js +++ b/src/pages/ExamsPage/data/selectors.js @@ -1,9 +1,7 @@ export const courseExamsList = state => state.exams.examsList; // derived from currentExamIndex rather than duplicating in state -export const currentExam = state => { - return state.exams.examsList[state.exams.currentExamIndex] -}; +export const currentExam = state => state.exams.examsList[state.exams.currentExamIndex]; export const courseExamAttemptsList = state => state.exams.attemptsList; diff --git a/src/pages/ExamsPage/hooks.test.js b/src/pages/ExamsPage/hooks.test.js index 315354f..26520cf 100644 --- a/src/pages/ExamsPage/hooks.test.js +++ b/src/pages/ExamsPage/hooks.test.js @@ -16,6 +16,7 @@ jest.mock('react-redux', () => ({ jest.mock('data/redux/hooks', () => ({ useMakeNetworkRequest: jest.fn(), useRequestIsPending: jest.fn(), + useRequestError: jest.fn(), })); jest.mock('./data/api'); @@ -169,4 +170,25 @@ describe('ExamsPage hooks', () => { expect(mockFetchExamAttempts).toHaveBeenCalledWith(1); }); }); + + describe('useRequestStatusFromRedux', () => { + it('returns empty string if no request made', () => { + reduxHooks.useRequestIsPending.mockReturnValue(false); + reduxHooks.useRequestError.mockReturnValue(false); + const getRequestStatus = hooks.useRequestStatusFromRedux(constants.modifyExamAttempt); + expect(getRequestStatus()).toBe(''); + }); + it('', () => { + reduxHooks.useRequestIsPending.mockReturnValue(true); + reduxHooks.useRequestError.mockReturnValue(false); + const getRequestStatus = hooks.useRequestStatusFromRedux(constants.modifyExamAttempt); + expect(getRequestStatus()).toBe('pending'); + }); + it('', () => { + reduxHooks.useRequestIsPending.mockReturnValue(false); + reduxHooks.useRequestError.mockReturnValue(true); + const getRequestStatus = hooks.useRequestStatusFromRedux(constants.modifyExamAttempt); + expect(getRequestStatus()).toBe('error'); + }); + }); }); diff --git a/src/pages/ExamsPage/utils.js b/src/pages/ExamsPage/utils.js index 1baad04..5a4654d 100644 --- a/src/pages/ExamsPage/utils.js +++ b/src/pages/ExamsPage/utils.js @@ -1,5 +1,5 @@ -import { getExamsBaseUrl } from './data/api'; import messages from 'pages/ExamsPage/messages'; +import { getExamsBaseUrl } from './data/api'; export const getLaunchUrlByExamId = (id) => `${getExamsBaseUrl()}/lti/exam/${id}/instructor_tool`; @@ -17,4 +17,4 @@ const examAttemptStatusLabels = { error: messages.statusLabelError, }; -export const getMessageLabelForStatus = (status) => examAttemptStatusLabels[status]; \ No newline at end of file +export const getMessageLabelForStatus = (status) => examAttemptStatusLabels[status]; diff --git a/src/testUtils.js b/src/testUtils.js index 260becc..c6ce54a 100644 --- a/src/testUtils.js +++ b/src/testUtils.js @@ -1,29 +1,5 @@ /* eslint-disable import/prefer-default-export */ -import React from 'react' -import { render } from '@testing-library/react' -import { configureStore } from '@reduxjs/toolkit' -import { Provider } from 'react-redux' -// As a basic setup, import your same slice reducers -import { reducer as examReducer } from './pages/ExamsPage/data/reducer'; -import { reducer as requestsReducer } from './data/redux/requests'; - -// Hi, just added the line you just posted and just ran the tests again, i'm just looking at the results now -export function renderWithProviders( - ui, - { - preloadedState = {}, - // yeah doesn't work, so we need to pass it in - store = configureStore({ reducer: { exams: examReducer, requests: requestsReducer, }, preloadedState: preloadedState }), - ...renderOptions - } = {} -) { - function Wrapper({ children }) { - return {children} - } - - // Return an object with the store and all of RTL's query functions - return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) } -} +import React from 'react'; /** * Mocked formatMessage provided by react-intl @@ -45,14 +21,13 @@ export const formatMessage = (msg, values) => { return message; }; -// oh should i pass this into renderwithproviders? export const defaultExamsData = { examsList: [ { id: 1, name: 'exam1' }, ], currentExamIndex: 0, currentExam: { id: 1, name: 'exam1' }, - // setCurrentExam: jest.fn(), + setCurrentExam: jest.fn(), }; export const defaultAttemptsData = { @@ -80,4 +55,3 @@ export const defaultAttemptsData = { attempt_id: 1, }], }; - From 8db99c3d3aecfc346355bec8d90825ed123b8fe1 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Tue, 9 Jan 2024 09:54:23 -0500 Subject: [PATCH 19/23] test: added reset tests - also fixed some nits --- src/pages/ExamsPage/components/AttemptList.test.jsx | 4 ++-- .../ExamsPage/components/ResetExamAttemptModal.jsx | 4 ++-- .../components/ResetExamAttemptModal.test.jsx | 10 ++++++++-- .../ExamsPage/components/ReviewExamAttemptModal.jsx | 4 ++-- .../components/ReviewExamAttemptModal.test.jsx | 13 ++++++------- src/pages/ExamsPage/hooks.js | 2 +- src/pages/ExamsPage/hooks.test.js | 12 ++++++------ src/pages/ExamsPage/index.test.jsx | 4 ++-- src/testUtils.js | 1 - 9 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/pages/ExamsPage/components/AttemptList.test.jsx b/src/pages/ExamsPage/components/AttemptList.test.jsx index 2d9f07b..964292b 100644 --- a/src/pages/ExamsPage/components/AttemptList.test.jsx +++ b/src/pages/ExamsPage/components/AttemptList.test.jsx @@ -13,14 +13,14 @@ jest.mock('../hooks', () => ({ useDeleteExamAttempt: jest.fn(), useModifyExamAttempt: jest.fn(), useExamsData: jest.fn(), - useRequestStatusFromRedux: jest.fn(), + useButtonStateFromRequestStatus: jest.fn(), })); describe('AttemptList', () => { beforeEach(() => { hooks.useDeleteExamAttempt.mockReturnValue(jest.fn()); hooks.useModifyExamAttempt.mockReturnValue(jest.fn()); - hooks.useRequestStatusFromRedux.mockReturnValue(jest.fn()); + hooks.useButtonStateFromRequestStatus.mockReturnValue(jest.fn()); hooks.useExamsData.mockReturnValue(testUtils.defaultExamsData); }); it('Test that the AttemptList matches snapshot', () => { diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx index 7c28f85..655df6d 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx @@ -5,13 +5,13 @@ import { } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import * as constants from 'data/constants'; -import { useDeleteExamAttempt, useRequestStatusFromRedux } from '../hooks'; +import { useDeleteExamAttempt, useButtonStateFromRequestStatus } from '../hooks'; import messages from '../messages'; const ResetExamAttemptModal = ({ username, examName, attemptId }) => { const [isOpen, open, close] = useToggle(false); const resetExamAttempt = useDeleteExamAttempt(); - const getRequestStatus = useRequestStatusFromRedux(); + const getRequestStatus = useButtonStateFromRequestStatus(); const { formatMessage } = useIntl(); const ResetButtonProps = { diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx index 54dca49..2a905ae 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx @@ -6,7 +6,7 @@ import * as hooks from '../hooks'; jest.mock('../hooks', () => ({ useDeleteExamAttempt: jest.fn(), - useRequestStatusFromRedux: jest.fn(), + useButtonStateFromRequestStatus: jest.fn(), })); const mockMakeNetworkRequest = jest.fn(); @@ -20,7 +20,7 @@ describe('ResetExamAttemptModal', () => { beforeEach(() => { jest.restoreAllMocks(); hooks.useDeleteExamAttempt.mockReturnValue(mockMakeNetworkRequest); - hooks.useRequestStatusFromRedux.mockReturnValue(mockMakeNetworkRequest); + hooks.useButtonStateFromRequestStatus.mockReturnValue(mockMakeNetworkRequest); }); it('Test that the ResetExamAttemptModal matches snapshot', () => { expect(render(resetModal)).toMatchSnapshot(); @@ -37,6 +37,12 @@ describe('ResetExamAttemptModal', () => { // Using queryByText here allows the function to throw expect(screen.queryByText('Please confirm your choice.')).not.toBeInTheDocument(); }); + it('Clicking the Reset button displays the correct label based on the request state', () => { + render(resetModal); + hooks.useButtonStateFromRequestStatus.mockReturnValue(() => 'pending'); // for testing button label state + screen.getByText('Reset').click(); + expect(screen.queryByText('Resetting...')).toBeInTheDocument(); // The button should be in the pending state + }); it('Clicking the Yes button calls the deletion hook', () => { const mockDeleteExamAttempt = jest.fn(); jest.spyOn(hooks, 'useDeleteExamAttempt').mockImplementation(() => mockDeleteExamAttempt); diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index fe1d594..170d6aa 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -6,7 +6,7 @@ import { import { useIntl } from '@edx/frontend-platform/i18n'; import { Info, Warning } from '@edx/paragon/icons'; import * as constants from 'data/constants'; -import { useExamsData, useModifyExamAttempt, useRequestStatusFromRedux } from '../hooks'; +import { useExamsData, useModifyExamAttempt, useButtonStateFromRequestStatus } from '../hooks'; import messages from '../messages'; import { getLaunchUrlByExamId, getMessageLabelForStatus } from '../utils'; @@ -27,7 +27,7 @@ const ReviewExamAttemptModal = ({ }) => { const [isOpen, open, close] = useToggle(false); const modifyExamAttempt = useModifyExamAttempt(); - const getRequestStatus = useRequestStatusFromRedux(); + const getRequestStatus = useButtonStateFromRequestStatus(); const { currentExam } = useExamsData(); const { formatMessage } = useIntl(); diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx index 5c36534..999f489 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx @@ -20,7 +20,7 @@ jest.mock('../data/api', () => { jest.mock('../hooks', () => ({ useModifyExamAttempt: jest.fn(), - useRequestStatusFromRedux: jest.fn(), + useButtonStateFromRequestStatus: jest.fn(), useExamsData: jest.fn(), })); @@ -44,7 +44,7 @@ describe('ReviewExamAttemptModal', () => { beforeEach(() => { jest.restoreAllMocks(); hooks.useModifyExamAttempt.mockReturnValue(mockMakeNetworkRequest); - hooks.useRequestStatusFromRedux.mockReturnValue(mockMakeNetworkRequest); + hooks.useButtonStateFromRequestStatus.mockReturnValue(mockMakeNetworkRequest); hooks.useExamsData.mockReturnValue(testUtils.defaultExamsData); }); it('Test that the ReviewExamAttemptModal matches snapshot', () => { @@ -62,9 +62,8 @@ describe('ReviewExamAttemptModal', () => { // Using queryByText here allows the function to throw expect(screen.queryByText('Update review status')).not.toBeInTheDocument(); }); - it.only('Clicking the Verify button displays the correct label based on the request state', () => { - api.modifyExamAttempt.mockResolvedValue({ exam_id: 0 }); - hooks.useRequestStatusFromRedux.mockReturnValue(() => 'pending'); // for testing button label state + it('Clicking the Verify button displays the correct label based on the request state', () => { + hooks.useButtonStateFromRequestStatus.mockReturnValue(() => 'pending'); // for testing button label state render(reviewModal()); screen.getByText('Review Required').click(); expect(screen.queryByText('Verifying...')).toBeInTheDocument(); // The button should be in the pending state @@ -78,8 +77,8 @@ describe('ReviewExamAttemptModal', () => { expect(mockModifyExamAttempt).toHaveBeenCalledWith(0, constants.ExamAttemptActions.verify); }); it('Clicking the Reject button displays the correct label based on the request state', () => { - api.modifyExamAttempt.mockResolvedValue({ exam_id: 0 }); - hooks.useRequestStatusFromRedux.mockReturnValue(() => 'pending'); // for testing button label state + render(reviewModal()); + hooks.useButtonStateFromRequestStatus.mockReturnValue(() => 'pending'); // for testing button label state screen.getByText('Review Required').click(); expect(screen.queryByText('Rejecting...')).toBeInTheDocument(); // The button should be in the pending state }); diff --git a/src/pages/ExamsPage/hooks.js b/src/pages/ExamsPage/hooks.js index 8d9014c..9136fda 100644 --- a/src/pages/ExamsPage/hooks.js +++ b/src/pages/ExamsPage/hooks.js @@ -107,7 +107,7 @@ export const useExamAttemptsData = () => { return { attemptsList }; }; -export const useRequestStatusFromRedux = (requestKey) => { +export const useButtonStateFromRequestStatus = (requestKey) => { const isPending = reduxHooks.useRequestIsPending(requestKey); const isError = reduxHooks.useRequestError(requestKey); return () => { diff --git a/src/pages/ExamsPage/hooks.test.js b/src/pages/ExamsPage/hooks.test.js index 26520cf..91acc8e 100644 --- a/src/pages/ExamsPage/hooks.test.js +++ b/src/pages/ExamsPage/hooks.test.js @@ -171,23 +171,23 @@ describe('ExamsPage hooks', () => { }); }); - describe('useRequestStatusFromRedux', () => { + describe('useButtonStateFromRequestStatus', () => { it('returns empty string if no request made', () => { reduxHooks.useRequestIsPending.mockReturnValue(false); reduxHooks.useRequestError.mockReturnValue(false); - const getRequestStatus = hooks.useRequestStatusFromRedux(constants.modifyExamAttempt); + const getRequestStatus = hooks.useButtonStateFromRequestStatus(constants.modifyExamAttempt); expect(getRequestStatus()).toBe(''); }); - it('', () => { + it('returns pending if request is pending', () => { reduxHooks.useRequestIsPending.mockReturnValue(true); reduxHooks.useRequestError.mockReturnValue(false); - const getRequestStatus = hooks.useRequestStatusFromRedux(constants.modifyExamAttempt); + const getRequestStatus = hooks.useButtonStateFromRequestStatus(constants.modifyExamAttempt); expect(getRequestStatus()).toBe('pending'); }); - it('', () => { + it('returns error if request errors', () => { reduxHooks.useRequestIsPending.mockReturnValue(false); reduxHooks.useRequestError.mockReturnValue(true); - const getRequestStatus = hooks.useRequestStatusFromRedux(constants.modifyExamAttempt); + const getRequestStatus = hooks.useButtonStateFromRequestStatus(constants.modifyExamAttempt); expect(getRequestStatus()).toBe('error'); }); }); diff --git a/src/pages/ExamsPage/index.test.jsx b/src/pages/ExamsPage/index.test.jsx index 8729101..4dddca0 100644 --- a/src/pages/ExamsPage/index.test.jsx +++ b/src/pages/ExamsPage/index.test.jsx @@ -14,7 +14,7 @@ jest.mock('./hooks', () => ({ useFetchExamAttempts: jest.fn(), useDeleteExamAttempt: jest.fn(), useModifyExamAttempt: jest.fn(), - useRequestStatusFromRedux: jest.fn(), + useButtonStateFromRequestStatus: jest.fn(), })); describe('ExamsPage', () => { @@ -25,7 +25,7 @@ describe('ExamsPage', () => { test('exams and attempts loaded', () => { // temporary, this won't fire on useEffect once we have an exam selection handler hooks.useFetchExamAttempts.mockReturnValue(jest.fn()); - hooks.useRequestStatusFromRedux.mockReturnValue(jest.fn()); + hooks.useButtonStateFromRequestStatus.mockReturnValue(jest.fn()); hooks.useExamsData.mockReturnValue(testUtils.defaultExamsData); expect(render()).toMatchSnapshot(); }); diff --git a/src/testUtils.js b/src/testUtils.js index c6ce54a..474b521 100644 --- a/src/testUtils.js +++ b/src/testUtils.js @@ -1,5 +1,4 @@ /* eslint-disable import/prefer-default-export */ -import React from 'react'; /** * Mocked formatMessage provided by react-intl From 778946c91602d6b00721c34510e47492e6669012 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Tue, 9 Jan 2024 09:56:33 -0500 Subject: [PATCH 20/23] chore: lint --- src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx index 999f489..9560806 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx @@ -5,7 +5,6 @@ import ReviewExamAttemptModal from './ReviewExamAttemptModal'; import * as testUtils from '../../../testUtils'; import * as hooks from '../hooks'; -import * as api from '../data/api'; jest.mock('../data/api', () => { const originalModule = jest.requireActual('../data/api'); From 79f075cc6ab4c8d1bac877f94396879bff56a634 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Tue, 9 Jan 2024 14:19:56 -0500 Subject: [PATCH 21/23] docs: added button state description --- src/pages/ExamsPage/components/ResetExamAttemptModal.jsx | 2 ++ src/pages/ExamsPage/components/ResetExamAttemptModal.test.jsx | 2 ++ src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx | 3 +++ 3 files changed, 7 insertions(+) diff --git a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx index 655df6d..df09f23 100644 --- a/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ResetExamAttemptModal.jsx @@ -61,6 +61,8 @@ const ResetExamAttemptModal = ({ username, examName, attemptId }) => { { jest.spyOn(hooks, 'useDeleteExamAttempt').mockImplementation(() => mockDeleteExamAttempt); render(resetModal); screen.getByText('Reset').click(); + // Need to use a test id here b/c there are two button with label 'Reset', + // One in the table, and one in the modal screen.getByTestId('reset-stateful-button').click(); expect(mockDeleteExamAttempt).toHaveBeenCalledWith(0); }); diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx index 170d6aa..5548e6f 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.jsx @@ -132,6 +132,8 @@ const ReviewExamAttemptModal = ({ {attemptStatus !== constants.ExamAttemptStatus.verified && ( Date: Tue, 9 Jan 2024 15:21:18 -0500 Subject: [PATCH 22/23] chore: removed unecessary mock --- .../components/ReviewExamAttemptModal.test.jsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx index 9560806..9fa6f77 100644 --- a/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx +++ b/src/pages/ExamsPage/components/ReviewExamAttemptModal.test.jsx @@ -6,17 +6,6 @@ import * as testUtils from '../../../testUtils'; import * as hooks from '../hooks'; -jest.mock('../data/api', () => { - const originalModule = jest.requireActual('../data/api'); - - return { - __esModule: true, - ...originalModule, - // Only modifyExamAttempt is mocked and everything else from api.js is the same - modifyExamAttempt: jest.fn(), - }; -}); - jest.mock('../hooks', () => ({ useModifyExamAttempt: jest.fn(), useButtonStateFromRequestStatus: jest.fn(), From c9153e8ccee6781214c8d63c30a46382ef263629 Mon Sep 17 00:00:00 2001 From: ilee2u Date: Wed, 10 Jan 2024 09:37:02 -0500 Subject: [PATCH 23/23] chore: nit --- src/pages/ExamsPage/hooks.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/ExamsPage/hooks.js b/src/pages/ExamsPage/hooks.js index 9136fda..18878ab 100644 --- a/src/pages/ExamsPage/hooks.js +++ b/src/pages/ExamsPage/hooks.js @@ -54,8 +54,6 @@ export const useDeleteExamAttempt = () => { }; export const useModifyExamAttempt = () => { - // something's up with useMakeNetworkRequest here? - // ok it says line 1473... but there's only like 200 in this file const makeNetworkRequest = reduxHooks.useMakeNetworkRequest(); const dispatch = useDispatch(); return (attemptId, action) => (