From 4b8832f0801905155be74ab72188aa345953f809 Mon Sep 17 00:00:00 2001 From: Michael Roytman Date: Fri, 2 Feb 2024 11:29:25 -0500 Subject: [PATCH] Refactor references to examStore to use the useDispatch and useSelector hooks API. (#131) * feat: rename examStore to specialExams in preparation for use in frontend-app-learning This commit renames the examStore to specialExams. This is in preparation for the use of the exam reducer in the frontend-app-learning React application. * refactor: replace use of context with thunks and Redux store in instructions components This commit replaces the use of the ExamStateContext with the use of thunks and the Redux store in the instructions components. The original pattern was to use the withExamStore higher-order component to provide context to the instructions components. This context contained provided the Redux store state and action creators as props by using the connect API. This posed a problem for our need to merge the frontend-app-learning and frontend-lib-special-exams stores, because the special exams store is initialized in this repository and used by the higher-order component. In order to eventually be able to remove the creation of the store in this repository, we have to remove references to the store by interfacing with the Redux more directly by using the useDispatch and useSelector hooks. * test: remove references to ExamStateProvider from tests for instructions components This commit removes references to the ExamStateProvider from tests for instructions components. Because these components have been refactored to no longe rely on the ExamStateProvider, they are not required in the component tree. --- src/api.js | 6 +- src/api.test.jsx | 4 +- src/core/ExamStateProvider.jsx | 2 +- src/core/OuterExamTimer.test.jsx | 4 +- src/data/__factories__/examState.factory.js | 2 +- src/data/__snapshots__/redux.test.jsx.snap | 8 +- src/data/index.js | 1 + src/data/redux.test.jsx | 144 ++++---- src/data/store.js | 2 +- src/data/thunks.js | 32 +- src/exam/ExamAPIError.test.jsx | 12 +- src/exam/ExamWrapper.test.jsx | 28 +- src/instructions/Instructions.test.jsx | 344 +++++++----------- src/instructions/SubmitInstructions.jsx | 13 +- src/instructions/index.jsx | 7 +- .../EntranceOnboardingExamInstructions.jsx | 14 +- .../ErrorOnboardingExamInstructions.jsx | 11 +- .../RejectedOnboardingExamInstructions.jsx | 14 +- .../SubmittedOnboardingExamInstructions.jsx | 15 +- .../VerifiedOnboardingExamInstructions.jsx | 9 +- .../EntrancePracticeExamInstructions.jsx | 11 +- .../ErrorPracticeExamInstructions.jsx | 11 +- .../SubmittedPracticeExamInstructions.jsx | 11 +- .../EntranceProctoredExamInstructions.jsx | 14 +- .../ErrorProctoredExamInstructions.jsx | 12 +- .../OnboardingErrorExamInstructions.jsx | 7 +- .../ProctoredExamInstructions.test.jsx | 107 +++--- .../ReadyToStartProctoredExamInstructions.jsx | 20 +- .../SkipProctoredExamInstruction.jsx | 11 +- .../SubmitProctoredExamInstructions.jsx | 17 +- .../download-instructions/index.jsx | 17 +- .../prerequisites-instructions/index.jsx | 8 +- .../timed_exam/StartTimedExamInstructions.jsx | 7 +- .../SubmitTimedExamInstructions.jsx | 11 +- .../SubmittedTimedExamInstructions.jsx | 8 +- src/setupTest.js | 4 +- src/timer/CountDownTimer.test.jsx | 28 +- src/timer/TimerProvider.jsx | 2 +- 38 files changed, 459 insertions(+), 519 deletions(-) diff --git a/src/api.js b/src/api.js index 68ec4e50..bc514238 100644 --- a/src/api.js +++ b/src/api.js @@ -1,12 +1,12 @@ import { examRequiresAccessToken, store } from './data'; export function isExam() { - const { exam } = store.getState().examState; + const { exam } = store.getState().specialExams; return !!exam?.id; } export function getExamAccess() { - const { exam, examAccessToken } = store.getState().examState; + const { exam, examAccessToken } = store.getState().specialExams; if (!exam) { return ''; } @@ -14,7 +14,7 @@ export function getExamAccess() { } export async function fetchExamAccess() { - const { exam } = store.getState().examState; + const { exam } = store.getState().specialExams; const { dispatch } = store; if (!exam) { return Promise.resolve(); diff --git a/src/api.test.jsx b/src/api.test.jsx index 1cabd935..850218f9 100644 --- a/src/api.test.jsx +++ b/src/api.test.jsx @@ -9,7 +9,7 @@ describe('External API integration tests', () => { jest.mock('./data'); const mockExam = Factory.build('exam', { attempt: Factory.build('attempt') }); const mockToken = Factory.build('examAccessToken'); - const mockState = { examState: { exam: mockExam, examAccessToken: mockToken } }; + const mockState = { specialExams: { exam: mockExam, examAccessToken: mockToken } }; store.getState = jest.fn().mockReturnValue(mockState); }); @@ -35,7 +35,7 @@ describe('External API integration tests', () => { describe('Test isExam without exam', () => { beforeAll(() => { jest.mock('./data'); - const mockState = { examState: { exam: null, examAccessToken: null } }; + const mockState = { specialExams: { exam: null, examAccessToken: null } }; store.getState = jest.fn().mockReturnValue(mockState); }); diff --git a/src/core/ExamStateProvider.jsx b/src/core/ExamStateProvider.jsx index ced1d07c..174a2b61 100644 --- a/src/core/ExamStateProvider.jsx +++ b/src/core/ExamStateProvider.jsx @@ -24,7 +24,7 @@ const StateProvider = ({ children, ...state }) => { ); }; -const mapStateToProps = (state) => ({ ...state.examState }); +const mapStateToProps = (state) => ({ ...state.specialExams }); const ExamStateProvider = withExamStore( StateProvider, diff --git a/src/core/OuterExamTimer.test.jsx b/src/core/OuterExamTimer.test.jsx index fb6984c2..056bfca6 100644 --- a/src/core/OuterExamTimer.test.jsx +++ b/src/core/OuterExamTimer.test.jsx @@ -28,7 +28,7 @@ describe('OuterExamTimer', () => { attempt_status: ExamStatus.STARTED, }); store.getState = () => ({ - examState: { + specialExams: { activeAttempt: attempt, exam: { time_limit_mins: 60, @@ -45,7 +45,7 @@ describe('OuterExamTimer', () => { it('does not render timer if there is no exam in progress', () => { store.getState = () => ({ - examState: { + specialExams: { activeAttempt: {}, exam: {}, }, diff --git a/src/data/__factories__/examState.factory.js b/src/data/__factories__/examState.factory.js index af775148..ccb6e411 100644 --- a/src/data/__factories__/examState.factory.js +++ b/src/data/__factories__/examState.factory.js @@ -4,7 +4,7 @@ import './exam.factory'; import './proctoringSettings.factory'; import './examAccessToken.factory'; -Factory.define('examState') +Factory.define('specialExams') .attr('proctoringSettings', Factory.build('proctoringSettings')) .attr('exam', Factory.build('exam')) .attr('examAccessToken', Factory.build('examAccessToken')) diff --git a/src/data/__snapshots__/redux.test.jsx.snap b/src/data/__snapshots__/redux.test.jsx.snap index 69475b13..24344fd5 100644 --- a/src/data/__snapshots__/redux.test.jsx.snap +++ b/src/data/__snapshots__/redux.test.jsx.snap @@ -9,7 +9,7 @@ Object { exports[`Data layer integration tests Test getExamAttemptsData Should get, and save exam and attempt 1`] = ` Object { - "examState": Object { + "specialExams": Object { "activeAttempt": Object { "attempt_code": "", "attempt_id": 1, @@ -93,7 +93,7 @@ Object { exports[`Data layer integration tests Test getLatestAttemptData with edx-proctoring as a backend (no EXAMS_BASE_URL) Should get, and save latest attempt 1`] = ` Object { - "examState": Object { + "specialExams": Object { "activeAttempt": Object { "attempt_code": "", "attempt_id": 1, @@ -245,7 +245,7 @@ Object { exports[`Data layer integration tests Test resetExam Should reset exam attempt 1`] = ` Object { - "examState": Object { + "specialExams": Object { "activeAttempt": null, "allowProctoringOptOut": false, "apiErrorMsg": "", @@ -314,7 +314,7 @@ Object { exports[`Data layer integration tests Test resetExam with edx-proctoring as backend (no EXAMS_BASE_URL) Should reset exam attempt 1`] = ` Object { - "examState": Object { + "specialExams": Object { "activeAttempt": null, "allowProctoringOptOut": false, "apiErrorMsg": "", 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/data/redux.test.jsx b/src/data/redux.test.jsx index b56e0ee9..b4dffc7a 100644 --- a/src/data/redux.test.jsx +++ b/src/data/redux.test.jsx @@ -70,7 +70,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getAllowProctoringOptOut(true), store.dispatch); const state = store.getState(); - expect(state.examState.allowProctoringOptOut).toEqual(true); + expect(state.specialExams.allowProctoringOptOut).toEqual(true); }); describe('Test getExamAttemptsData', () => { @@ -94,7 +94,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); const state = store.getState(); - expect(state.examState.exam.total_time).toBe('30 minutes'); + expect(state.specialExams.exam.total_time).toBe('30 minutes'); }); it('Should fail to fetch if error occurs', async () => { @@ -103,7 +103,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); const state = store.getState(); - expect(state.examState.apiErrorMsg).toBe('Network Error'); + expect(state.specialExams.apiErrorMsg).toBe('Network Error'); }); }); @@ -123,7 +123,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getProctoringSettings(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.proctoringSettings).toMatchSnapshot(); + expect(state.specialExams.proctoringSettings).toMatchSnapshot(); }); }); @@ -134,7 +134,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getProctoringSettings(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.proctoringSettings).toMatchSnapshot(); + expect(state.specialExams.proctoringSettings).toMatchSnapshot(); }); it('Should fail to fetch if error occurs', async () => { @@ -144,7 +144,7 @@ describe('Data layer integration tests', () => { const state = store.getState(); expect(loggingService.logError).toHaveBeenCalled(); - expect(state.examState.proctoringSettings).toMatchSnapshot(); + expect(state.specialExams.proctoringSettings).toMatchSnapshot(); }); it('Should fail to fetch if error occurs', async () => { @@ -154,7 +154,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getProctoringSettings(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.apiErrorMsg).toBe('Network Error'); + expect(state.specialExams.apiErrorMsg).toBe('Network Error'); }); }); @@ -174,7 +174,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamReviewPolicy(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.exam.reviewPolicy).toEqual(reviewPolicy); + expect(state.specialExams.exam.reviewPolicy).toEqual(reviewPolicy); }); it('Should fail to fetch if error occurs', async () => { @@ -185,7 +185,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamReviewPolicy(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.apiErrorMsg).toBe('Network Error'); + expect(state.specialExams.apiErrorMsg).toBe('Network Error'); }); it('Should fail to fetch if no exam id', async () => { @@ -195,7 +195,7 @@ describe('Data layer integration tests', () => { const state = store.getState(); expect(loggingService.logError).toHaveBeenCalled(); - expect(state.examState.exam.reviewPolicy).toBeUndefined(); + expect(state.specialExams.exam.reviewPolicy).toBeUndefined(); }); }); @@ -212,11 +212,11 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.activeAttempt).toBeNull(); + expect(state.specialExams.activeAttempt).toBeNull(); await executeThunk(thunks.startTimedExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.activeAttempt).toMatchSnapshot(); + expect(state.specialExams.activeAttempt).toMatchSnapshot(); expect(axiosMock.history.post[0].data).toEqual(JSON.stringify({ exam_id: exam.id, start_clock: 'true', @@ -233,7 +233,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.startTimedExam(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.activeAttempt).toMatchSnapshot(); + expect(state.specialExams.activeAttempt).toMatchSnapshot(); expect(axiosMock.history.post[0].data).toEqual(JSON.stringify({ exam_id: exam.id, start_clock: 'true', @@ -258,7 +258,7 @@ describe('Data layer integration tests', () => { const state = store.getState(); expect(loggingService.logError).toHaveBeenCalled(); - expect(state.examState.apiErrorMsg).toBe('Failed to start exam. No exam id was found.'); + expect(state.specialExams.apiErrorMsg).toBe('Failed to start exam. No exam id was found.'); }); }); @@ -278,11 +278,11 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); await executeThunk(thunks.stopExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.READY_TO_SUBMIT); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.READY_TO_SUBMIT); expect(axiosMock.history.put[0].url).toEqual(updateAttemptStatusLegacyUrl); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ action: 'stop' })); }); @@ -299,7 +299,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); const state = store.getState(); - expect(state.examState.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); await executeThunk(thunks.stopExam(), store.dispatch, store.getState); expect(axiosMock.history.put[0].url).toEqual(updateAttemptStatusLegacyUrl); @@ -312,7 +312,7 @@ describe('Data layer integration tests', () => { it('Should stop exam, and update attempt', async () => { await initWithExamAttempt(); let state = store.getState(); - expect(state.examState.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); axiosMock.onPut(`${createUpdateAttemptURL}/${readyToSubmitAttempt.attempt_id}`).reply(200, { exam_attempt_id: readyToSubmitAttempt.attempt_id }); axiosMock.onGet(fetchExamAttemptsDataUrl).reply(200, { exam: readyToSubmitExam }); @@ -320,7 +320,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.stopExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.READY_TO_SUBMIT); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.READY_TO_SUBMIT); expect(axiosMock.history.put[0].url).toEqual(`${createUpdateAttemptURL}/${readyToSubmitAttempt.attempt_id}`); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ action: 'stop' })); }); @@ -334,7 +334,7 @@ describe('Data layer integration tests', () => { await initWithExamAttempt({}, attempt); const state = store.getState(); - expect(state.examState.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); axiosMock.onPut(`${createUpdateAttemptURL}/${readyToSubmitAttempt.attempt_id}`).reply(200, { exam_attempt_id: readyToSubmitAttempt.attempt_id }); @@ -358,13 +358,13 @@ describe('Data layer integration tests', () => { it('Should fail to fetch if error occurs', async () => { await initWithExamAttempt(); let state = store.getState(); - expect(state.examState.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); axiosMock.onPut(`${createUpdateAttemptURL}/${attempt.attempt_id}`).networkError(); await executeThunk(thunks.stopExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.apiErrorMsg).toBe('Network Error'); + expect(state.specialExams.apiErrorMsg).toBe('Network Error'); }); it('Should fail to fetch if no active attempt', async () => { @@ -374,7 +374,7 @@ describe('Data layer integration tests', () => { const state = store.getState(); expect(loggingService.logError).toHaveBeenCalled(); - expect(state.examState.apiErrorMsg).toBe('Failed to stop exam. No active attempt was found.'); + expect(state.specialExams.apiErrorMsg).toBe('Failed to stop exam. No active attempt was found.'); }); }); @@ -394,11 +394,11 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.READY_TO_SUBMIT); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.READY_TO_SUBMIT); await executeThunk(thunks.continueExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); expect(axiosMock.history.put[0].url).toEqual(updateAttemptStatusLegacyUrl); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ action: 'start' })); }); @@ -407,7 +407,7 @@ describe('Data layer integration tests', () => { it('Should return to exam, and update attempt', async () => { await initWithExamAttempt(readyToSubmitExam, {}); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.READY_TO_SUBMIT); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.READY_TO_SUBMIT); axiosMock.onGet(fetchExamAttemptsDataUrl).reply(200, { exam }); axiosMock.onGet(latestAttemptURL).reply(200, { attempt }); @@ -415,7 +415,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.continueExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); expect(axiosMock.history.put[0].url).toEqual(`${createUpdateAttemptURL}/${attempt.attempt_id}`); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ action: 'start' })); }); @@ -428,7 +428,7 @@ describe('Data layer integration tests', () => { const state = store.getState(); expect(loggingService.logError).toHaveBeenCalled(); - expect(state.examState.apiErrorMsg).toBe('Failed to continue exam. No attempt id was found.'); + expect(state.specialExams.apiErrorMsg).toBe('Failed to continue exam. No attempt id was found.'); }); }); @@ -456,12 +456,12 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); await executeThunk(thunks.resetExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.CREATED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.CREATED); expect(state).toMatchSnapshot(); expect(axiosMock.history.put[0].url).toEqual(updateAttemptStatusLegacyUrl); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ action: 'reset_attempt' })); @@ -471,7 +471,7 @@ describe('Data layer integration tests', () => { it('Should reset exam attempt', async () => { await initWithExamAttempt(); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); axiosMock.onGet(fetchExamAttemptsDataUrl).reply(200, { exam: examWithCreatedAttempt }); axiosMock.onGet(latestAttemptURL).reply(200, {}); @@ -480,7 +480,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.resetExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.CREATED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.CREATED); expect(state).toMatchSnapshot(); expect(axiosMock.history.put[0].url).toEqual(`${createUpdateAttemptURL}/${attempt.attempt_id}`); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ action: 'reset_attempt' })); @@ -494,7 +494,7 @@ describe('Data layer integration tests', () => { const state = store.getState(); expect(loggingService.logError).toHaveBeenCalled(); - expect(state.examState.apiErrorMsg).toBe('Failed to reset exam attempt. No attempt id was found.'); + expect(state.specialExams.apiErrorMsg).toBe('Failed to reset exam attempt. No attempt id was found.'); }); }); @@ -514,25 +514,25 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); await executeThunk(thunks.submitExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.SUBMITTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.SUBMITTED); }); }); it('Should submit exam, and update attempt and exam', async () => { await initWithExamAttempt(); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); axiosMock.onGet(fetchExamAttemptsDataUrl).reply(200, { exam: submittedExam }); axiosMock.onPut(`${createUpdateAttemptURL}/${attempt.attempt_id}`).reply(200, { exam_attempt_id: submittedAttempt.attempt_id }); await executeThunk(thunks.submitExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.SUBMITTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.SUBMITTED); expect(axiosMock.history.put[0].url).toEqual(`${createUpdateAttemptURL}/${attempt.attempt_id}`); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ action: 'submit' })); }); @@ -547,7 +547,7 @@ describe('Data layer integration tests', () => { const state = store.getState(); expect(loggingService.logError).toHaveBeenCalled(); - expect(state.examState.apiErrorMsg).toBe('Failed to submit exam. No active attempt was found.'); + expect(state.specialExams.apiErrorMsg).toBe('Failed to submit exam. No active attempt was found.'); }); it('Should submit exam and redirect to sequence if no exam attempt', async () => { @@ -562,7 +562,7 @@ describe('Data layer integration tests', () => { await initWithExamAttempt({}, attempt); const state = store.getState(); - expect(state.examState.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); axiosMock.onPut(`${createUpdateAttemptURL}/${attempt.attempt_id}`).reply(200, { exam_attempt_id: submittedAttempt.attempt_id }); @@ -600,19 +600,19 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); await executeThunk(thunks.expireExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.SUBMITTED); - expect(state.examState.timeIsOver).toBe(true); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.SUBMITTED); + expect(state.specialExams.timeIsOver).toBe(true); }); }); it('Should submit expired exam, and update attempt', async () => { await initWithExamAttempt(); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); axiosMock.onGet(fetchExamAttemptsDataUrl).reply(200, { exam: submittedExam }); axiosMock.onGet(latestAttemptURL).reply(200, submittedAttempt); @@ -620,8 +620,8 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.expireExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.SUBMITTED); - expect(state.examState.timeIsOver).toBe(true); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.SUBMITTED); + expect(state.specialExams.timeIsOver).toBe(true); expect(axiosMock.history.put[0].url).toEqual(`${createUpdateAttemptURL}/${attempt.attempt_id}`); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ action: 'submit' })); }); @@ -632,7 +632,7 @@ describe('Data layer integration tests', () => { const state = store.getState(); expect(loggingService.logError).toHaveBeenCalled(); - expect(state.examState.apiErrorMsg).toBe('Failed to expire exam. No attempt id was found.'); + expect(state.specialExams.apiErrorMsg).toBe('Failed to expire exam. No attempt id was found.'); }); }); @@ -656,11 +656,11 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.STARTED); await executeThunk(thunks.startProctoringSoftwareDownload(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.DOWNLOAD_SOFTWARE_CLICKED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.DOWNLOAD_SOFTWARE_CLICKED); }); }); @@ -672,7 +672,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.startProctoringSoftwareDownload(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.DOWNLOAD_SOFTWARE_CLICKED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.DOWNLOAD_SOFTWARE_CLICKED); expect(axiosMock.history.put[0].url).toEqual(`${createUpdateAttemptURL}/${softwareDownloadedAttempt.attempt_id}`); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ action: 'click_download_software' })); }); @@ -694,11 +694,11 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.exam.attempt).toEqual({}); + expect(state.specialExams.exam.attempt).toEqual({}); await executeThunk(thunks.createProctoredExamAttempt(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.CREATED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.CREATED); }); }); @@ -711,7 +711,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.createProctoredExamAttempt(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.CREATED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.CREATED); expect(axiosMock.history.post.length).toBe(1); }); @@ -754,18 +754,18 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.activeAttempt.attempt_status).toBe(ExamStatus.CREATED); + expect(state.specialExams.activeAttempt.attempt_status).toBe(ExamStatus.CREATED); await executeThunk(thunks.startProctoredExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.activeAttempt).toMatchSnapshot(); + expect(state.specialExams.activeAttempt).toMatchSnapshot(); }); }); it('Should start exam, and update attempt and exam', async () => { await initWithExamAttempt(createdExam, createdAttempt); let state = store.getState(); - expect(state.examState.activeAttempt.attempt_status).toBe(ExamStatus.CREATED); + expect(state.specialExams.activeAttempt.attempt_status).toBe(ExamStatus.CREATED); axiosMock.onPost(createUpdateAttemptURL).reply(200, { exam_attempt_id: startedAttempt.attempt_id }); axiosMock.onGet(fetchExamAttemptsDataUrl).reply(200, { exam: startedExam }); @@ -773,7 +773,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.startProctoredExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.activeAttempt).toMatchSnapshot(); + expect(state.specialExams.activeAttempt).toMatchSnapshot(); }); it('Should fail to fetch if no exam id', async () => { @@ -850,11 +850,11 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.exam.attempt).toEqual({}); + expect(state.specialExams.exam.attempt).toEqual({}); await executeThunk(thunks.skipProctoringExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.DECLINED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.DECLINED); }); it('Should change existing attempt status to declined, and update attempt and exam', async () => { @@ -864,11 +864,11 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toEqual(ExamStatus.CREATED); + expect(state.specialExams.exam.attempt.attempt_status).toEqual(ExamStatus.CREATED); await executeThunk(thunks.skipProctoringExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.DECLINED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.DECLINED); }); }); @@ -879,21 +879,21 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.skipProctoringExam(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.DECLINED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.DECLINED); expect(axiosMock.history.post.length).toBe(1); }); it('Should change existing attempt status to declined, and update attempt and exam', async () => { await initWithExamAttempt(createdExam, {}); let state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toEqual(ExamStatus.CREATED); + expect(state.specialExams.exam.attempt.attempt_status).toEqual(ExamStatus.CREATED); axiosMock.onGet(fetchExamAttemptsDataUrl).reply(200, { exam: declinedExam, active_attempt: {} }); axiosMock.onPut(`${createUpdateAttemptURL}/${declinedAttempt.attempt_id}`).reply(200, { exam_attempt_id: declinedAttempt.attempt_id }); await executeThunk(thunks.skipProctoringExam(), store.dispatch, store.getState); state = store.getState(); - expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.DECLINED); + expect(state.specialExams.exam.attempt.attempt_status).toBe(ExamStatus.DECLINED); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ action: 'decline' })); }); @@ -921,12 +921,12 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); - expect(state.examState.exam.attempt).toMatchSnapshot(); + expect(state.specialExams.exam.attempt).toMatchSnapshot(); await executeThunk(thunks.pollAttempt(attempt.exam_started_poll_url), store.dispatch, store.getState); state = store.getState(); const expectedPollUrl = `${getConfig().LMS_BASE_URL}${attempt.exam_started_poll_url}`; - expect(state.examState.exam.attempt).toMatchSnapshot(); + expect(state.specialExams.exam.attempt).toMatchSnapshot(); expect(axiosMock.history.get[1].url).toEqual(expectedPollUrl); }); }); @@ -942,7 +942,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.pollAttempt(attempt.exam_started_poll_url), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.activeAttempt).toMatchSnapshot(); + expect(state.specialExams.activeAttempt).toMatchSnapshot(); }); describe('pollAttempt api called directly', () => { @@ -1028,7 +1028,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getLatestAttemptData(courseId), store.dispatch); const state = store.getState(); - expect(state.examState.activeAttempt.attempt_id).toEqual(1234); + expect(state.specialExams.activeAttempt.attempt_id).toEqual(1234); }); }); @@ -1046,8 +1046,8 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.examRequiresAccessToken(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.exam.id).toBe(exam.id); - expect(state.examState.examAccessToken.exam_access_token).toBe(''); + expect(state.specialExams.exam.id).toBe(exam.id); + expect(state.specialExams.examAccessToken.exam_access_token).toBe(''); }); }); @@ -1059,7 +1059,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.examRequiresAccessToken(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.examAccessToken).toMatchSnapshot(); + expect(state.specialExams.examAccessToken).toMatchSnapshot(); }); it('Should fail to fetch if no exam id', async () => { @@ -1067,7 +1067,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.examRequiresAccessToken(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.examAccessToken.exam_access_token).toBe(''); + expect(state.specialExams.examAccessToken.exam_access_token).toBe(''); }); it('Should fail to fetch if API error occurs', async () => { @@ -1077,7 +1077,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.examRequiresAccessToken(), store.dispatch, store.getState); const state = store.getState(); - expect(state.examState.examAccessToken.exam_access_token).toBe(''); + expect(state.specialExams.examAccessToken.exam_access_token).toBe(''); }); }); diff --git a/src/data/store.js b/src/data/store.js index 916de85c..9fdef030 100644 --- a/src/data/store.js +++ b/src/data/store.js @@ -3,6 +3,6 @@ import examReducer from './slice'; export default configureStore({ reducer: { - examState: examReducer, + specialExams: examReducer, }, }); diff --git a/src/data/thunks.js b/src/data/thunks.js index d1d06cf6..da9268b8 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -101,7 +101,7 @@ export function getLatestAttemptData(courseId) { export function getProctoringSettings() { return async (dispatch, getState) => { - const { exam } = getState().examState; + const { exam } = getState().specialExams; if (!exam.id) { logError('Failed to get exam settings. No exam id.'); handleAPIError( @@ -124,7 +124,7 @@ export function examRequiresAccessToken() { if (!getConfig().EXAMS_BASE_URL) { return; } - const { exam } = getState().examState; + const { exam } = getState().specialExams; if (!exam.id) { logError('Failed to get exam access token. No exam id.'); return; @@ -143,7 +143,7 @@ export function examRequiresAccessToken() { */ export function startTimedExam() { return async (dispatch, getState) => { - const { exam } = getState().examState; + const { exam } = getState().specialExams; if (!exam.id) { logError('Failed to start exam. No exam id.'); handleAPIError( @@ -162,7 +162,7 @@ export function startTimedExam() { export function createProctoredExamAttempt() { return async (dispatch, getState) => { - const { exam } = getState().examState; + const { exam } = getState().specialExams; if (!exam.id) { logError('Failed to create exam attempt. No exam id.'); return; @@ -180,7 +180,7 @@ export function createProctoredExamAttempt() { */ export function startProctoredExam() { return async (dispatch, getState) => { - const { exam } = getState().examState; + const { exam } = getState().specialExams; const { attempt } = exam || {}; if (!exam.id) { logError('Failed to start proctored exam. No exam id.'); @@ -231,7 +231,7 @@ export function startProctoredExam() { export function skipProctoringExam() { return async (dispatch, getState) => { - const { exam } = getState().examState; + const { exam } = getState().specialExams; if (!exam.id) { logError('Failed to skip proctored exam. No exam id.'); return; @@ -260,7 +260,7 @@ export function skipProctoringExam() { */ export function pollAttempt(url) { return async (dispatch, getState) => { - const currentAttempt = getState().examState.activeAttempt; + const currentAttempt = getState().specialExams.activeAttempt; // If the learner is in a state where they've finished the exam // and the attempt can be submitted (i.e. they are "ready_to_submit"), @@ -291,7 +291,7 @@ export function pollAttempt(url) { export function stopExam() { return async (dispatch, getState) => { - const { exam, activeAttempt } = getState().examState; + const { exam, activeAttempt } = getState().specialExams; if (!activeAttempt) { logError('Failed to stop exam. No active attempt.'); @@ -323,7 +323,7 @@ export function stopExam() { export function continueExam() { return async (dispatch, getState) => { - const { exam } = getState().examState; + const { exam } = getState().specialExams; const attemptId = exam.attempt.attempt_id; const useLegacyAttemptAPI = exam.attempt.use_legacy_attempt_api; if (!attemptId) { @@ -344,7 +344,7 @@ export function continueExam() { export function resetExam() { return async (dispatch, getState) => { - const { exam } = getState().examState; + const { exam } = getState().specialExams; const attemptId = exam.attempt.attempt_id; const useLegacyAttemptAPI = exam.attempt.use_legacy_attempt_api; if (!attemptId) { @@ -361,7 +361,7 @@ export function resetExam() { export function submitExam() { return async (dispatch, getState) => { - const { exam, activeAttempt } = getState().examState; + const { exam, activeAttempt } = getState().specialExams; const { desktop_application_js_url: workerUrl, external_id: attemptExternalId } = activeAttempt || {}; const useWorker = window.Worker && activeAttempt && workerUrl; @@ -409,7 +409,7 @@ export function submitExam() { export function expireExam() { return async (dispatch, getState) => { - const { exam, activeAttempt } = getState().examState; + const { exam, activeAttempt } = getState().specialExams; const { desktop_application_js_url: workerUrl, attempt_id: attemptId, @@ -452,7 +452,7 @@ export function expireExam() { */ export function pingAttempt(timeoutInSeconds, workerUrl) { return async (dispatch, getState) => { - const { exam, activeAttempt } = getState().examState; + const { exam, activeAttempt } = getState().specialExams; await pingApplication(timeoutInSeconds, activeAttempt.external_id, workerUrl) .catch(async (error) => { const message = error?.message || 'Worker failed to respond.'; @@ -480,7 +480,7 @@ export function pingAttempt(timeoutInSeconds, workerUrl) { export function startProctoringSoftwareDownload() { return async (dispatch, getState) => { - const { exam } = getState().examState; + const { exam } = getState().specialExams; const attemptId = exam.attempt.attempt_id; const useLegacyAttemptAPI = exam.attempt.use_legacy_attempt_api; if (!attemptId) { @@ -501,7 +501,7 @@ export function startProctoringSoftwareDownload() { export function getExamReviewPolicy() { return async (dispatch, getState) => { - const { exam } = getState().examState; + const { exam } = getState().specialExams; if (!exam.id) { logError('Failed to fetch exam review policy. No exam id.'); handleAPIError( @@ -536,7 +536,7 @@ export function getAllowProctoringOptOut(allowProctoringOptOut) { */ export function checkExamEntry() { return async (dispatch, getState) => { - const { exam } = getState().examState; + const { exam } = getState().specialExams; // Check only applies to LTI exams if ( !exam?.attempt diff --git a/src/exam/ExamAPIError.test.jsx b/src/exam/ExamAPIError.test.jsx index b8d276d9..edb16d1d 100644 --- a/src/exam/ExamAPIError.test.jsx +++ b/src/exam/ExamAPIError.test.jsx @@ -23,7 +23,7 @@ describe('ExamAPIError', () => { const defaultMessage = 'A system error has occurred with your exam.'; it('renders with the default information', () => { - store.getState = () => ({ examState: {} }); + store.getState = () => ({ specialExams: {} }); const tree = render( @@ -42,7 +42,7 @@ describe('ExamAPIError', () => { }; getConfig.mockImplementation(() => config); - store.getState = () => ({ examState: {} }); + store.getState = () => ({ specialExams: {} }); const { getByTestId } = render( @@ -58,7 +58,7 @@ describe('ExamAPIError', () => { it('renders error details when provided', () => { store.getState = () => ({ - examState: { apiErrorMsg: 'Something bad has happened' }, + specialExams: { apiErrorMsg: 'Something bad has happened' }, }); const { queryByTestId } = render( @@ -68,12 +68,12 @@ describe('ExamAPIError', () => { { store }, ); - expect(queryByTestId('error-details')).toHaveTextContent(store.getState().examState.apiErrorMsg); + expect(queryByTestId('error-details')).toHaveTextContent(store.getState().specialExams.apiErrorMsg); }); it('renders default message when error is HTML', () => { store.getState = () => ({ - examState: { apiErrorMsg: '' }, + specialExams: { apiErrorMsg: '' }, }); const { queryByTestId } = render( @@ -88,7 +88,7 @@ describe('ExamAPIError', () => { it('renders default message when there is no error message', () => { store.getState = () => ({ - examState: { apiErrorMsg: '' }, + specialExams: { apiErrorMsg: '' }, }); const { queryByTestId } = render( diff --git a/src/exam/ExamWrapper.test.jsx b/src/exam/ExamWrapper.test.jsx index bee6ed74..04e43ab1 100644 --- a/src/exam/ExamWrapper.test.jsx +++ b/src/exam/ExamWrapper.test.jsx @@ -39,7 +39,7 @@ describe('SequenceExamWrapper', () => { beforeEach(() => { jest.clearAllMocks(); store.getState = () => ({ - examState: Factory.build('examState'), + specialExams: Factory.build('specialExams'), isLoading: false, }); }); @@ -59,7 +59,7 @@ describe('SequenceExamWrapper', () => { it('is successfully rendered and shows instructions for proctored exam', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: ExamType.PROCTORED, }), @@ -78,7 +78,7 @@ describe('SequenceExamWrapper', () => { it('shows loader if isLoading true', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { isLoading: true, }), }); @@ -95,7 +95,7 @@ describe('SequenceExamWrapper', () => { it('shows exam api error component together with other content if there is an error', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { apiErrorMsg: 'Something bad has happened.', }), }); @@ -114,7 +114,7 @@ describe('SequenceExamWrapper', () => { it('does not show exam api error component on a non-exam sequence', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { apiErrorMsg: 'Something bad has happened.', }), }); @@ -147,7 +147,7 @@ describe('SequenceExamWrapper', () => { it('does fetch exam data for non exam sequences if not already loaded', async () => { // this would only occur if the user deeplinks directly to a non-exam sequence store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { isLoading: true, }), }); @@ -192,7 +192,7 @@ describe('SequenceExamWrapper', () => { it('renders exam content without an active attempt if the user is staff', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: ExamType.PROCTORED, }), @@ -211,7 +211,7 @@ describe('SequenceExamWrapper', () => { it('renders exam content for staff masquerading as a learner', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: ExamType.PROCTORED, passed_due_date: false, @@ -236,7 +236,7 @@ describe('SequenceExamWrapper', () => { gated: true, }; store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: ExamType.PROCTORED, }), @@ -255,7 +255,7 @@ describe('SequenceExamWrapper', () => { it('does not display masquerade alert if specified learner is in the middle of the exam', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: ExamType.PROCTORED, attempt: { @@ -280,7 +280,7 @@ describe('SequenceExamWrapper', () => { it('does not display masquerade alert if learner can view the exam after the due date', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: ExamType.TIMED, attempt: { @@ -318,7 +318,7 @@ describe('SequenceExamWrapper', () => { it('shows access denied if learner is not accessible to proctoring exams', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: ExamType.PROCTORED, attempt: null, @@ -345,7 +345,7 @@ describe('SequenceExamWrapper', () => { it('learner has access to timed exams', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: ExamType.TIMED, attempt: null, @@ -372,7 +372,7 @@ describe('SequenceExamWrapper', () => { it('learner has access to content that are not exams', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: '', attempt: null, diff --git a/src/instructions/Instructions.test.jsx b/src/instructions/Instructions.test.jsx index 22cbba99..af9a5200 100644 --- a/src/instructions/Instructions.test.jsx +++ b/src/instructions/Instructions.test.jsx @@ -3,27 +3,26 @@ 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 { render, screen, act, initializeMockApp, } from '../setupTest'; -import ExamStateProvider from '../core/ExamStateProvider'; import { ExamStatus, ExamType, INCOMPLETE_STATUSES, } from '../constants'; 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 +32,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(); @@ -44,14 +44,12 @@ describe('SequenceExamWrapper', () => { }); it('Start exam instructions can be successfully rendered', () => { - store.getState = () => ({ examState: Factory.build('examState') }); + store.getState = () => ({ specialExams: Factory.build('specialExams') }); const { getByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); expect(getByTestId('start-exam-button')).toHaveTextContent('I am ready to start this timed exam.'); @@ -59,7 +57,7 @@ describe('SequenceExamWrapper', () => { it('Instructions are not shown when exam is started', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: ExamType.PROCTORED, attempt: Factory.build('attempt', { @@ -70,11 +68,9 @@ describe('SequenceExamWrapper', () => { }); const { getByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); expect(getByTestId('sequence-content')).toHaveTextContent('Sequence'); @@ -87,7 +83,7 @@ describe('SequenceExamWrapper', () => { ['integration@email.com', 'learner_notification@example.com'], ])('Shows onboarding exam entrance instructions when receives onboarding exam with integration email: "%s", learner email: "%s"', (integrationEmail, learnerEmail) => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { proctoringSettings: Factory.build('proctoringSettings', { learner_notification_from_email: learnerEmail, integration_specific_email: integrationEmail, @@ -99,11 +95,9 @@ describe('SequenceExamWrapper', () => { }); const { queryByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -126,7 +120,7 @@ describe('SequenceExamWrapper', () => { it('Shows practice exam entrance instructions when receives practice exam', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { type: ExamType.PRACTICE, }), @@ -134,11 +128,9 @@ describe('SequenceExamWrapper', () => { }); const { getByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); expect(getByTestId('exam-instructions-title')).toHaveTextContent('Try a proctored exam'); @@ -146,7 +138,7 @@ describe('SequenceExamWrapper', () => { it('Shows failed prerequisites page if user has failed prerequisites for the exam', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { timeIsOver: true, allowProctoringOptOut: true, exam: Factory.build('exam', { @@ -166,11 +158,9 @@ describe('SequenceExamWrapper', () => { }); const { getByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -185,7 +175,7 @@ describe('SequenceExamWrapper', () => { it('Shows pending prerequisites page if user has failed prerequisites for the exam', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { timeIsOver: true, exam: Factory.build('exam', { is_proctored: true, @@ -203,11 +193,9 @@ describe('SequenceExamWrapper', () => { }); const { getByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -218,7 +206,7 @@ describe('SequenceExamWrapper', () => { it('Instructions for error status', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { timeIsOver: true, exam: Factory.build('exam', { type: ExamType.PROCTORED, @@ -230,11 +218,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); expect(screen.getByText('Error with proctored exam')).toBeInTheDocument(); @@ -242,7 +228,7 @@ describe('SequenceExamWrapper', () => { it('Instructions for ready to resume state', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { timeIsOver: true, exam: Factory.build('exam', { type: ExamType.PROCTORED, @@ -255,11 +241,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); expect(screen.getByText('Your exam is ready to be resumed.')).toBeInTheDocument(); @@ -272,7 +256,7 @@ describe('SequenceExamWrapper', () => { attempt_status: ExamStatus.READY_TO_SUBMIT, }); store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: attempt, exam: Factory.build('exam', { attempt, @@ -281,11 +265,9 @@ describe('SequenceExamWrapper', () => { }); const { queryByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -308,7 +290,7 @@ describe('SequenceExamWrapper', () => { it('Instructions for submitted status', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { exam: Factory.build('exam', { attempt: Factory.build('attempt', { attempt_status: ExamStatus.SUBMITTED, @@ -318,11 +300,9 @@ describe('SequenceExamWrapper', () => { }); const { getByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); expect(getByTestId('exam.submittedExamInstructions.title')).toHaveTextContent('You have submitted your timed exam.'); @@ -330,7 +310,7 @@ describe('SequenceExamWrapper', () => { it('Instructions when exam time is over', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { timeIsOver: true, exam: Factory.build('exam', { attempt: Factory.build('attempt', { @@ -341,11 +321,9 @@ describe('SequenceExamWrapper', () => { }); const { getByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); expect(getByTestId('exam.submittedExamInstructions.title')).toHaveTextContent('The time allotted for this exam has expired.'); @@ -353,7 +331,7 @@ describe('SequenceExamWrapper', () => { it.each(['integration@example.com', ''])('Shows correct rejected onboarding exam instructions when attempt is rejected and integration email is "%s"', (integrationEmail) => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { proctoringSettings: Factory.build('proctoringSettings', { integration_specific_email: integrationEmail, }), @@ -368,11 +346,9 @@ describe('SequenceExamWrapper', () => { }); const { queryByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -388,7 +364,7 @@ describe('SequenceExamWrapper', () => { it('Shows submit onboarding exam instructions if exam is onboarding and attempt status is ready_to_submit', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { is_proctored: true, @@ -401,11 +377,9 @@ describe('SequenceExamWrapper', () => { }); const { getByTestId } = render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -414,7 +388,7 @@ describe('SequenceExamWrapper', () => { it('Shows error onboarding exam instructions if exam is onboarding and attempt status is error', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { is_proctored: true, @@ -427,11 +401,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -441,7 +413,7 @@ describe('SequenceExamWrapper', () => { it('Shows submitted onboarding exam instructions if exam is onboarding and attempt status is submitted', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { proctoringSettings: Factory.build('proctoringSettings', { integration_specific_email: 'test@example.com', learner_notification_from_email: 'test_notification@example.com', @@ -458,11 +430,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -479,7 +449,7 @@ describe('SequenceExamWrapper', () => { it('Shows verified onboarding exam instructions if exam is onboarding and attempt status is verified', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { proctoringSettings: Factory.build('proctoringSettings', { integration_specific_email: 'test@example.com', }), @@ -495,11 +465,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -509,7 +477,7 @@ describe('SequenceExamWrapper', () => { it('Shows error practice exam instructions if exam is onboarding and attempt status is error', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { is_proctored: true, @@ -522,11 +490,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -536,7 +502,7 @@ describe('SequenceExamWrapper', () => { it('Shows submitted practice exam instructions if exam is onboarding and attempt status is submitted', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { is_proctored: true, @@ -549,11 +515,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -563,7 +527,7 @@ describe('SequenceExamWrapper', () => { it('Does not show expired page if exam is passed due date and is practice', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { is_proctored: true, @@ -574,11 +538,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -587,7 +549,7 @@ describe('SequenceExamWrapper', () => { it.each([ExamType.TIMED, ExamType.PROCTORED, ExamType.ONBOARDING])('Shows expired page when exam is passed due date and is %s', (examType) => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { is_proctored: true, @@ -599,11 +561,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -615,7 +575,7 @@ describe('SequenceExamWrapper', () => { `Shows expired page when exam is ${examType} and has passed due date and attempt is in %s status`, (item) => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { is_proctored: true, @@ -630,11 +590,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -645,7 +603,7 @@ describe('SequenceExamWrapper', () => { it('Shows exam content for timed exam if attempt status is submitted, due date has passed and hide after due is set to false', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { type: ExamType.TIMED, @@ -659,11 +617,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
children
-
-
, + +
children
+
, { store }, ); @@ -672,7 +628,7 @@ describe('SequenceExamWrapper', () => { it('Shows submitted exam page for proctored exams if attempt status is submitted, due date has passed and hide after due is set to false', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { type: ExamType.PROCTORED, @@ -686,11 +642,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
children
-
-
, + +
children
+
, { store }, ); @@ -699,7 +653,7 @@ describe('SequenceExamWrapper', () => { it('Shows submitted page when proctored exam is in second_review_required status', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { is_proctored: true, @@ -712,11 +666,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -725,7 +677,7 @@ describe('SequenceExamWrapper', () => { it('Shows correct download instructions for LTI provider if attempt status is created, with support email and phone', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, proctoringSettings: Factory.build('proctoringSettings', { provider_name: 'LTI Provider', @@ -743,11 +695,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -762,7 +712,7 @@ describe('SequenceExamWrapper', () => { it('Shows correct download instructions for LTI provider if attempt status is created with support URL', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, proctoringSettings: Factory.build('proctoringSettings', { provider_name: 'LTI Provider', @@ -781,11 +731,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -802,7 +750,7 @@ describe('SequenceExamWrapper', () => { it('Hides support contact info on download instructions for LTI provider if not provided', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, proctoringSettings: Factory.build('proctoringSettings', { provider_name: 'LTI Provider', @@ -818,11 +766,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -840,7 +786,7 @@ describe('SequenceExamWrapper', () => { assign: mockAssign, }; store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, proctoringSettings: Factory.build('proctoringSettings', { provider_name: 'LTI Provider', @@ -861,11 +807,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); fireEvent.click(screen.getByText('Start System Check')); @@ -888,7 +832,7 @@ describe('SequenceExamWrapper', () => { 'instruction 3', ]; store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, proctoringSettings: Factory.build('proctoringSettings', { provider_name: 'Provider Name', @@ -911,11 +855,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -931,7 +873,7 @@ describe('SequenceExamWrapper', () => { it('Shows correct download instructions for legacy rpnow provider if attempt status is created', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, proctoringSettings: Factory.build('proctoringSettings', { provider_name: 'Provider Name', @@ -951,11 +893,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); expect(screen.getByDisplayValue('1234-5678-9012-3456')).toBeInTheDocument(); @@ -966,7 +906,7 @@ describe('SequenceExamWrapper', () => { it('Shows error message if receives unknown attempt status', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { type: ExamType.TIMED, @@ -978,11 +918,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
children
-
-
, + +
children
+
, { store }, ); @@ -991,7 +929,7 @@ describe('SequenceExamWrapper', () => { it('Shows ready to start page when proctored exam is in ready_to_start status', () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { is_proctored: true, @@ -1004,11 +942,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); @@ -1017,7 +953,7 @@ describe('SequenceExamWrapper', () => { it('Shows loading spinner while waiting to start exam', async () => { store.getState = () => ({ - examState: Factory.build('examState', { + specialExams: Factory.build('specialExams', { activeAttempt: {}, exam: Factory.build('exam', { is_proctored: true, @@ -1031,11 +967,9 @@ describe('SequenceExamWrapper', () => { }); render( - - -
Sequence
-
-
, + +
Sequence
+
, { store }, ); 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 && ( -