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 && ( -