diff --git a/src/data/index.js b/src/data/index.js index 9c6cb1a3..27409017 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -1,4 +1,5 @@ export { + createProctoredExamAttempt, getExamAttemptsData, getLatestAttemptData, getProctoringSettings, diff --git a/src/instructions/Instructions.test.jsx b/src/instructions/Instructions.test.jsx index df8a2036..00157585 100644 --- a/src/instructions/Instructions.test.jsx +++ b/src/instructions/Instructions.test.jsx @@ -3,9 +3,10 @@ import { Factory } from 'rosie'; import React from 'react'; import { fireEvent, waitFor } from '@testing-library/dom'; import Instructions from './index'; -import { store, getExamAttemptsData, startTimedExam } from '../data'; +import { + store, continueExam, getExamAttemptsData, startProctoredExam, startTimedExam, submitExam, +} from '../data'; import { pollExamAttempt, softwareDownloadAttempt } from '../data/api'; -import { continueExam, submitExam } from '../data/thunks'; import Emitter from '../data/emitter'; import { TIMER_REACHED_NULL } from '../timer/events'; import { @@ -18,12 +19,11 @@ import { jest.mock('../data', () => ({ store: {}, - getExamAttemptsData: jest.fn(), - startTimedExam: jest.fn(), -})); -jest.mock('../data/thunks', () => ({ continueExam: jest.fn(), + getExamAttemptsData: jest.fn(), getExamReviewPolicy: jest.fn(), + startProctoredExam: jest.fn(), + startTimedExam: jest.fn(), submitExam: jest.fn(), })); jest.mock('../data/api', () => ({ @@ -33,6 +33,7 @@ jest.mock('../data/api', () => ({ continueExam.mockReturnValue(jest.fn()); submitExam.mockReturnValue(jest.fn()); getExamAttemptsData.mockReturnValue(jest.fn()); +startProctoredExam.mockReturnValue(jest.fn()); startTimedExam.mockReturnValue(jest.fn()); pollExamAttempt.mockReturnValue(Promise.resolve({})); store.subscribe = jest.fn(); diff --git a/src/instructions/SubmitInstructions.jsx b/src/instructions/SubmitInstructions.jsx index a93a29ef..816c3e3f 100644 --- a/src/instructions/SubmitInstructions.jsx +++ b/src/instructions/SubmitInstructions.jsx @@ -1,17 +1,20 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { Button, Container } from '@edx/paragon'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import Emitter from '../data/emitter'; import { ExamType } from '../constants'; +import { continueExam } from '../data'; import { SubmitProctoredExamInstructions } from './proctored_exam'; import { SubmitTimedExamInstructions } from './timed_exam'; import Footer from './proctored_exam/Footer'; -import ExamStateContext from '../context'; import { TIMER_REACHED_NULL } from '../timer/events'; const SubmitExamInstructions = () => { - const state = useContext(ExamStateContext); - const { exam, continueExam, activeAttempt } = state; + const { exam, activeAttempt } = useSelector(state => state.specialExams); + + const dispatch = useDispatch(); + const { time_remaining_seconds: timeRemaining } = activeAttempt; const { type: examType } = exam || {}; const [canContinue, setCanContinue] = useState(timeRemaining > 0); @@ -33,7 +36,7 @@ const SubmitExamInstructions = () => { ? : } {canContinue && ( - + dispatch(continueExam())} data-testid="continue-exam-button"> { - const state = useContext(ExamStateContext); - const { exam } = state; + const { exam } = useSelector(state => state.specialExams); const { attempt, type: examType, diff --git a/src/instructions/onboarding_exam/EntranceOnboardingExamInstructions.jsx b/src/instructions/onboarding_exam/EntranceOnboardingExamInstructions.jsx index d3cbc8bf..e3b2ca7b 100644 --- a/src/instructions/onboarding_exam/EntranceOnboardingExamInstructions.jsx +++ b/src/instructions/onboarding_exam/EntranceOnboardingExamInstructions.jsx @@ -1,11 +1,15 @@ -import React, { useContext } from 'react'; +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Button, MailtoLink } from '@edx/paragon'; -import ExamStateContext from '../../context'; + +import { createProctoredExamAttempt } from '../../data'; const EntranceOnboardingExamInstructions = () => { - const state = useContext(ExamStateContext); - const { createProctoredExamAttempt, proctoringSettings } = state; + const { proctoringSettings } = useSelector(state => state.specialExams); + + const dispatch = useDispatch(); + const { provider_name: providerName, learner_notification_from_email: learnerNotificationFromEmail, @@ -89,7 +93,7 @@ const EntranceOnboardingExamInstructions = () => { dispatch(createProctoredExamAttempt())} > { - const state = useContext(ExamStateContext); - const { resetExam } = state; + const dispatch = useDispatch(); return ( @@ -25,7 +26,7 @@ const ErrorOnboardingExamInstructions = () => { dispatch(resetExam())} > { - const state = useContext(ExamStateContext); - const { proctoringSettings, resetExam } = state; + const { proctoringSettings } = useSelector(state => state.specialExams); + + const dispatch = useDispatch(); + const { integration_specific_email: integrationSpecificEmail } = proctoringSettings || {}; return ( @@ -34,7 +38,7 @@ const RejectedOnboardingExamInstructions = () => { dispatch(resetExam())} > { const [isConfirm, confirm] = useToggle(false); - const state = useContext(ExamStateContext); - const { proctoringSettings, resetExam } = state; + + const { proctoringSettings } = useSelector(state => state.specialExams); + + const dispatch = useDispatch(); + const { learner_notification_from_email: learnerNotificationFromEmail, integration_specific_email: integrationSpecificEmail, @@ -70,7 +75,7 @@ const SubmittedOnboardingExamInstructions = () => { dispatch(resetExam())} disabled={!isConfirm} > { - const state = useContext(ExamStateContext); + const { proctoringSettings } = useSelector(state => state.specialExams); + const { integration_specific_email: integrationSpecificEmail, - } = state.proctoringSettings || {}; + } = proctoringSettings || {}; return ( diff --git a/src/instructions/practice_exam/EntrancePracticeExamInstructions.jsx b/src/instructions/practice_exam/EntrancePracticeExamInstructions.jsx index 844db80e..a7440705 100644 --- a/src/instructions/practice_exam/EntrancePracticeExamInstructions.jsx +++ b/src/instructions/practice_exam/EntrancePracticeExamInstructions.jsx @@ -1,11 +1,12 @@ -import React, { useContext } from 'react'; +import React from 'react'; +import { useDispatch } from 'react-redux'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Button } from '@edx/paragon'; -import ExamStateContext from '../../context'; + +import { createProctoredExamAttempt } from '../../data'; const EntrancePracticeExamInstructions = () => { - const state = useContext(ExamStateContext); - const { createProctoredExamAttempt } = state; + const dispatch = useDispatch(); return ( <> @@ -26,7 +27,7 @@ const EntrancePracticeExamInstructions = () => { dispatch(createProctoredExamAttempt())} > { - const state = useContext(ExamStateContext); - const { resetExam } = state; + const dispatch = useDispatch(); return ( @@ -37,7 +38,7 @@ const ErrorPracticeExamInstructions = () => { dispatch(resetExam())} > { - const state = useContext(ExamStateContext); - const { resetExam } = state; + const dispatch = useDispatch(); return ( @@ -25,7 +26,7 @@ const SubmittedPracticeExamInstructions = () => { dispatch(resetExam())} > { - const state = useContext(ExamStateContext); - const { exam, createProctoredExamAttempt, allowProctoringOptOut } = state; + const { exam, allowProctoringOptOut } = useSelector(state => state.specialExams); + + const dispatch = useDispatch(); + const { attempt } = exam || {}; const { total_time: totalTime = 0 } = attempt; @@ -54,7 +58,7 @@ const EntranceProctoredExamInstructions = ({ skipProctoredExam }) => { dispatch(createProctoredExamAttempt())} > { - const state = useContext(ExamStateContext); - const { - proctoring_escalation_email: proctoringEscalationEmail, - } = state.proctoringSettings || {}; + const { proctoring_escalation_email: proctoringEscalationEmail } = useSelector( + state => state.specialExams?.proctoringSettings, + ) || {}; + const platformName = getConfig().SITE_NAME; const contactUsUrl = getConfig().CONTACT_URL; diff --git a/src/instructions/proctored_exam/OnboardingErrorExamInstructions.jsx b/src/instructions/proctored_exam/OnboardingErrorExamInstructions.jsx index ea872d9c..74b06081 100644 --- a/src/instructions/proctored_exam/OnboardingErrorExamInstructions.jsx +++ b/src/instructions/proctored_exam/OnboardingErrorExamInstructions.jsx @@ -1,13 +1,12 @@ -import React, { useContext } from 'react'; +import React from 'react'; +import { useSelector } from 'react-redux'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Container, MailtoLink, Hyperlink } from '@edx/paragon'; -import ExamStateContext from '../../context'; import { ExamStatus } from '../../constants'; import Footer from './Footer'; const OnboardingErrorProctoredExamInstructions = () => { - const state = useContext(ExamStateContext); - const { exam, proctoringSettings } = state; + const { exam, proctoringSettings } = useSelector(state => state.specialExams); const { attempt, onboarding_link: onboardingLink } = exam; const { integration_specific_email: integrationSpecificEmail, diff --git a/src/instructions/proctored_exam/ProctoredExamInstructions.test.jsx b/src/instructions/proctored_exam/ProctoredExamInstructions.test.jsx index e79a9f4f..07e07151 100644 --- a/src/instructions/proctored_exam/ProctoredExamInstructions.test.jsx +++ b/src/instructions/proctored_exam/ProctoredExamInstructions.test.jsx @@ -3,8 +3,7 @@ import { Factory } from 'rosie'; import React from 'react'; import { fireEvent, waitFor } from '@testing-library/dom'; import Instructions from '../index'; -import { store, getExamAttemptsData } from '../../data'; -import { submitExam } from '../../data/thunks'; +import { store, getExamAttemptsData, submitExam } from '../../data'; import { initializeMockApp, render, screen } from '../../setupTest'; import ExamStateProvider from '../../core/ExamStateProvider'; import { @@ -16,11 +15,10 @@ import { jest.mock('../../data', () => ({ store: {}, getExamAttemptsData: jest.fn(), -})); -jest.mock('../../data/thunks', () => ({ getExamReviewPolicy: jest.fn(), submitExam: jest.fn(), })); + submitExam.mockReturnValue(jest.fn()); getExamAttemptsData.mockReturnValue(jest.fn()); store.subscribe = jest.fn(); diff --git a/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx b/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx index 49b92acd..7839d00a 100644 --- a/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx +++ b/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx @@ -1,31 +1,31 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; import { Button, Container, Spinner } from '@edx/paragon'; -import ExamStateContext from '../../context'; import Footer from './Footer'; +import { getExamReviewPolicy, startProctoredExam } from '../../data'; + const ReadyToStartProctoredExamInstructions = () => { - const state = useContext(ExamStateContext); - const { - exam, - getExamReviewPolicy, - startProctoredExam, - } = state; + const { exam } = useSelector(state => state.specialExams); const { attempt, reviewPolicy } = exam; + + const dispatch = useDispatch(); + const examDuration = attempt.total_time ? attempt.total_time : exam.total_time; const platformName = getConfig().SITE_NAME; const rulesUrl = getConfig().PROCTORED_EXAM_RULES_URL; const [beginExamClicked, setBeginExamClicked] = useState(false); useEffect(() => { - getExamReviewPolicy(); + dispatch(getExamReviewPolicy()); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleStart = () => { setBeginExamClicked(true); - startProctoredExam(); + dispatch(startProctoredExam()); }; return ( diff --git a/src/instructions/proctored_exam/SkipProctoredExamInstruction.jsx b/src/instructions/proctored_exam/SkipProctoredExamInstruction.jsx index 167b5dc5..9f0b9e30 100644 --- a/src/instructions/proctored_exam/SkipProctoredExamInstruction.jsx +++ b/src/instructions/proctored_exam/SkipProctoredExamInstruction.jsx @@ -1,13 +1,14 @@ -import React, { useContext } from 'react'; +import React from 'react'; +import { useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Button, Container } from '@edx/paragon'; -import ExamStateContext from '../../context'; import Footer from './Footer'; +import { skipProctoringExam } from '../../data'; + const SkipProctoredExamInstruction = ({ cancelSkipProctoredExam }) => { - const state = useContext(ExamStateContext); - const { skipProctoringExam } = state; + const dispatch = useDispatch(); return ( <> @@ -30,7 +31,7 @@ const SkipProctoredExamInstruction = ({ cancelSkipProctoredExam }) => { data-testid="skip-confirm-exam-button" variant="primary" className="mr-3 mb-2" - onClick={skipProctoringExam} + onClick={() => dispatch(skipProctoringExam())} > { - const state = useContext(ExamStateContext); - const { - submitExam, - exam, - activeAttempt, - } = state; + const { exam, activeAttempt } = useSelector(state => state.specialExams); + + const dispatch = useDispatch(); + const { type: examType, attempt } = exam || {}; const { exam_display_name: examName } = activeAttempt; const examHasLtiProvider = !attempt.use_legacy_attempt_api; @@ -21,7 +20,7 @@ const SubmitProctoredExamInstructions = () => { if (examHasLtiProvider) { window.location.assign(submitLtiAttemptUrl); } else { - submitExam(); + dispatch(submitExam()); } }; diff --git a/src/instructions/proctored_exam/download-instructions/index.jsx b/src/instructions/proctored_exam/download-instructions/index.jsx index 298d81c2..626d1157 100644 --- a/src/instructions/proctored_exam/download-instructions/index.jsx +++ b/src/instructions/proctored_exam/download-instructions/index.jsx @@ -1,10 +1,11 @@ -import React, { useContext, useState } from 'react'; +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { getConfig } from '@edx/frontend-platform'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Container } from '@edx/paragon'; -import ExamStateContext from '../../../context'; import { ExamStatus } from '../../../constants'; +import { getExamAttemptsData } from '../../../data'; import WarningModal from '../WarningModal'; import { pollExamAttempt, softwareDownloadAttempt } from '../../../data/api'; import messages from '../messages'; @@ -16,18 +17,20 @@ import Footer from '../Footer'; import SkipProctoredExamButton from '../SkipProctoredExamButton'; const DownloadSoftwareProctoredExamInstructions = ({ intl, skipProctoredExam }) => { - const state = useContext(ExamStateContext); const { proctoringSettings, exam, - getExamAttemptsData, allowProctoringOptOut, - } = state; + } = useSelector(state => state.specialExams); + + const dispatch = useDispatch(); + const { attempt, course_id: courseId, content_id: sequenceId, } = exam; + const { exam_started_poll_url: pollUrl, attempt_code: examCode, @@ -35,6 +38,7 @@ const DownloadSoftwareProctoredExamInstructions = ({ intl, skipProctoredExam }) software_download_url: downloadUrl, use_legacy_attempt_api: useLegacyAttemptApi, } = attempt; + const { provider_name: providerName, provider_tech_support_email: supportEmail, @@ -42,6 +46,7 @@ const DownloadSoftwareProctoredExamInstructions = ({ intl, skipProctoredExam }) provider_tech_support_url: supportURL, exam_proctoring_backend: proctoringBackend, } = proctoringSettings; + const examHasLtiProvider = !useLegacyAttemptApi; const { instructions } = proctoringBackend || {}; const [systemCheckStatus, setSystemCheckStatus] = useState(''); @@ -70,7 +75,7 @@ const DownloadSoftwareProctoredExamInstructions = ({ intl, skipProctoredExam }) pollExamAttempt(pollUrl, sequenceId) .then((data) => ( data.status === ExamStatus.READY_TO_START - ? getExamAttemptsData(courseId, sequenceId) + ? dispatch(getExamAttemptsData(courseId, sequenceId)) : setSystemCheckStatus('failure') )); }; diff --git a/src/instructions/proctored_exam/prerequisites-instructions/index.jsx b/src/instructions/proctored_exam/prerequisites-instructions/index.jsx index 2634678f..8c4670cc 100644 --- a/src/instructions/proctored_exam/prerequisites-instructions/index.jsx +++ b/src/instructions/proctored_exam/prerequisites-instructions/index.jsx @@ -1,15 +1,15 @@ -import React, { useContext } from 'react'; +import React from 'react'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { Container } from '@edx/paragon'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import ExamStateContext from '../../../context'; import PendingPrerequisitesProctoredExamInstructions from './Pending'; import FailedPrerequisitesProctoredExamInstructions from './Failed'; import Footer from '../Footer'; const PrerequisitesProctoredExamInstructions = ({ skipProctoredExam }) => { - const state = useContext(ExamStateContext); - const { exam, allowProctoringOptOut } = state; + const { exam, allowProctoringOptOut } = useSelector(state => state.specialExams); + const { prerequisite_status: prerequisitesData } = exam; const { pending_prerequisites: pending, failed_prerequisites: failed } = prerequisitesData; diff --git a/src/instructions/timed_exam/StartTimedExamInstructions.jsx b/src/instructions/timed_exam/StartTimedExamInstructions.jsx index 8b70afc2..07ead39d 100644 --- a/src/instructions/timed_exam/StartTimedExamInstructions.jsx +++ b/src/instructions/timed_exam/StartTimedExamInstructions.jsx @@ -1,11 +1,10 @@ -import React, { useContext } from 'react'; +import React from 'react'; +import { useSelector } from 'react-redux'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Button } from '@edx/paragon'; -import ExamStateContext from '../../context'; const StartTimedExamInstructions = () => { - const state = useContext(ExamStateContext); - const { exam, startTimedExam } = state; + const { exam, startTimedExam } = useSelector(state => state.specialExams); const examDuration = exam.total_time; return ( diff --git a/src/instructions/timed_exam/SubmitTimedExamInstructions.jsx b/src/instructions/timed_exam/SubmitTimedExamInstructions.jsx index ee6cfe7b..fa3f83e3 100644 --- a/src/instructions/timed_exam/SubmitTimedExamInstructions.jsx +++ b/src/instructions/timed_exam/SubmitTimedExamInstructions.jsx @@ -1,11 +1,12 @@ -import React, { useContext } from 'react'; +import React from 'react'; +import { useDispatch } from 'react-redux'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Button } from '@edx/paragon'; -import ExamStateContext from '../../context'; + +import { submitExam } from '../../data'; const SubmitTimedExamInstructions = () => { - const state = useContext(ExamStateContext); - const { submitExam } = state; + const dispatch = useDispatch(); return ( <> @@ -27,7 +28,7 @@ const SubmitTimedExamInstructions = () => { defaultMessage="After you submit your exam, your exam will be graded." /> - + dispatch(submitExam())} className="mr-2" data-testid="end-exam-button"> { - const state = useContext(ExamStateContext); + const { timeIsOver } = useSelector(state => state.specialExams); return ( - {state.timeIsOver + {timeIsOver ? (