From 0a13b26e80c9099325aea1710f58174127e174fe Mon Sep 17 00:00:00 2001 From: Sagirov Evgeniy <34642612+UvgenGen@users.noreply.github.com> Date: Fri, 25 Jun 2021 12:07:28 +0300 Subject: [PATCH 1/3] [BD-26] Error status doesn't work properly (#50) * fix: Error status doesn't work properly * fix: Incorrect eye image on the timer block --- src/data/thunks.js | 14 +++++++++----- .../EntranceProctoredExamInstructions.jsx | 2 +- src/timer/CountDownTimer.jsx | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/data/thunks.js b/src/data/thunks.js index 827dbce3..2c35ccdb 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -12,6 +12,7 @@ import { fetchExamReviewPolicy, resetAttempt, declineAttempt, + endExamWithFailure, } from './api'; import { isEmpty } from '../helpers'; import { @@ -340,12 +341,15 @@ export function expireExam() { * @param workerUrl - location of the worker from the provider */ export function pingAttempt(timeoutInSeconds, workerUrl) { - return async (dispatch) => { + return async (dispatch, getState) => { await pingApplication(timeoutInSeconds, workerUrl) - .catch((error) => handleAPIError( - { message: error ? error.message : 'Worker failed to respond.' }, - dispatch, - )); + .catch(async (error) => { + const { exam, activeAttempt } = getState().examState; + const message = error ? error.message : 'Worker failed to respond.'; + await updateAttemptAfter( + exam.course_id, exam.content_id, endExamWithFailure(activeAttempt.attempt_id, message), + )(dispatch); + }); }; } diff --git a/src/instructions/proctored_exam/EntranceProctoredExamInstructions.jsx b/src/instructions/proctored_exam/EntranceProctoredExamInstructions.jsx index fe9080f9..d2190ecb 100644 --- a/src/instructions/proctored_exam/EntranceProctoredExamInstructions.jsx +++ b/src/instructions/proctored_exam/EntranceProctoredExamInstructions.jsx @@ -25,7 +25,7 @@ const EntranceProctoredExamInstructions = ({ skipProctoredExam }) => {

diff --git a/src/timer/CountDownTimer.jsx b/src/timer/CountDownTimer.jsx index 7ceca588..1eb10d90 100644 --- a/src/timer/CountDownTimer.jsx +++ b/src/timer/CountDownTimer.jsx @@ -33,8 +33,8 @@ const CountDownTimer = injectIntl((props) => { })} > {isShowTimer - ? - : } + ? + : } ); From ca23071e2f8bb70fb17b102e9802b6edab6c1fc5 Mon Sep 17 00:00:00 2001 From: Sagirov Evgeniy <34642612+UvgenGen@users.noreply.github.com> Date: Fri, 25 Jun 2021 12:07:44 +0300 Subject: [PATCH 2/3] [BD-26] Timer bar on non-sequence pages (#51) * feat: Timer bar on non-sequence pages * feat: update fetchExamAttempts URL * test: add tests for stopExam --- src/data/api.js | 6 +++++- src/data/redux.test.jsx | 39 +++++++++++++++++++++++++++++++++++++-- src/data/thunks.js | 16 +++++++++++----- src/exam/ExamWrapper.jsx | 8 ++++++-- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/data/api.js b/src/data/api.js index 1aa603c5..67647229 100644 --- a/src/data/api.js +++ b/src/data/api.js @@ -6,8 +6,12 @@ const BASE_API_URL = '/api/edx_proctoring/v1/proctored_exam/attempt'; export async function fetchExamAttemptsData(courseId, sequenceId) { const url = new URL( - `${getConfig().LMS_BASE_URL}${BASE_API_URL}/course_id/${courseId}/content_id/${sequenceId}?is_learning_mfe=true`, + `${getConfig().LMS_BASE_URL}${BASE_API_URL}/course_id/${courseId}`, ); + if (sequenceId) { + url.searchParams.append('content_id', sequenceId); + } + url.searchParams.append('is_learning_mfe', true); const { data } = await getAuthenticatedHttpClient().get(url.href); return data; } diff --git a/src/data/redux.test.jsx b/src/data/redux.test.jsx index b29c16d0..61e90ada 100644 --- a/src/data/redux.test.jsx +++ b/src/data/redux.test.jsx @@ -19,7 +19,8 @@ const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); describe('Data layer integration tests', () => { const exam = Factory.build('exam', { attempt: Factory.build('attempt') }); const { course_id: courseId, content_id: contentId, attempt } = exam; - const fetchExamAttemptsDataUrl = `${getConfig().LMS_BASE_URL}${BASE_API_URL}/course_id/${courseId}/content_id/${contentId}?is_learning_mfe=true`; + const fetchExamAttemptsDataUrl = `${getConfig().LMS_BASE_URL}${BASE_API_URL}/course_id/${courseId}` + + `?content_id=${encodeURIComponent(contentId)}&is_learning_mfe=true`; const updateAttemptStatusUrl = `${getConfig().LMS_BASE_URL}${BASE_API_URL}/${attempt.attempt_id}`; let store; @@ -186,7 +187,7 @@ describe('Data layer integration tests', () => { it('Should stop exam, and update attempt and exam', async () => { axiosMock.onGet(fetchExamAttemptsDataUrl).replyOnce(200, { exam, active_attempt: attempt }); axiosMock.onGet(fetchExamAttemptsDataUrl).reply(200, { exam: readyToSubmitExam, active_attempt: {} }); - axiosMock.onPost(updateAttemptStatusUrl).reply(200, { exam_attempt_id: readyToSubmitAttempt.attempt_id }); + axiosMock.onPut(updateAttemptStatusUrl).reply(200, { exam_attempt_id: readyToSubmitAttempt.attempt_id }); await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); let state = store.getState(); @@ -197,6 +198,40 @@ describe('Data layer integration tests', () => { expect(state.examState.exam.attempt.attempt_status).toBe(ExamStatus.READY_TO_SUBMIT); }); + it('Should stop exam, and redirect to sequence if no exam attempt', async () => { + const { location } = window; + delete window.location; + window.location = { + href: '', + }; + + axiosMock.onGet(fetchExamAttemptsDataUrl).replyOnce(200, { exam: {}, active_attempt: attempt }); + axiosMock.onPut(updateAttemptStatusUrl).reply(200, { exam_attempt_id: readyToSubmitAttempt.attempt_id }); + + await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); + const state = store.getState(); + expect(state.examState.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); + + await executeThunk(thunks.stopExam(), store.dispatch, store.getState); + expect(axiosMock.history.put[0].url).toEqual(updateAttemptStatusUrl); + expect(window.location.href).toEqual(attempt.exam_url_path); + + window.location = location; + }); + + it('Should fail to fetch if error occurs', async () => { + axiosMock.onGet(fetchExamAttemptsDataUrl).replyOnce(200, { exam: {}, active_attempt: attempt }); + axiosMock.onPut(updateAttemptStatusUrl).networkError(); + + await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); + let state = store.getState(); + expect(state.examState.activeAttempt.attempt_status).toBe(ExamStatus.STARTED); + + await executeThunk(thunks.stopExam(), store.dispatch, store.getState); + state = store.getState(); + expect(state.examState.apiErrorMsg).toBe('Network Error'); + }); + it('Should fail to fetch if no active attempt', async () => { axiosMock.onGet(fetchExamAttemptsDataUrl).reply(200, { exam: Factory.build('exam'), active_attempt: {} }); axiosMock.onGet(updateAttemptStatusUrl).reply(200, { exam_attempt_id: readyToSubmitAttempt.attempt_id }); diff --git a/src/data/thunks.js b/src/data/thunks.js index 2c35ccdb..baac0b04 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -232,13 +232,19 @@ export function stopExam() { } const { attempt_id: attemptId, exam_url_path: examUrl } = activeAttempt; + if (!exam.attempt || attemptId !== exam.attempt.attempt_id) { + try { + await stopAttempt(attemptId); + window.location.href = examUrl; + } catch (error) { + handleAPIError(error, dispatch); + } + return; + } + await updateAttemptAfter( exam.course_id, exam.content_id, stopAttempt(attemptId), true, )(dispatch); - - if (attemptId !== exam.attempt.attempt_id) { - window.location.href = examUrl; - } }; } @@ -321,7 +327,7 @@ export function expireExam() { } await updateAttemptAfter( - exam.course_id, exam.content_id, submitAttempt(attemptId), + activeAttempt.course_id, exam.content_id, submitAttempt(attemptId), )(dispatch); dispatch(expireExamAttempt()); diff --git a/src/exam/ExamWrapper.jsx b/src/exam/ExamWrapper.jsx index d74a675f..92aba518 100644 --- a/src/exam/ExamWrapper.jsx +++ b/src/exam/ExamWrapper.jsx @@ -28,12 +28,16 @@ const ExamWrapper = ({ children, ...props }) => { ExamWrapper.propTypes = { sequence: PropTypes.shape({ - id: PropTypes.string.isRequired, + id: PropTypes.string, isTimeLimited: PropTypes.bool, allowProctoringOptOut: PropTypes.bool, - }).isRequired, + }), courseId: PropTypes.string.isRequired, children: PropTypes.element.isRequired, }; +ExamWrapper.defaultProps = { + sequence: {}, +}; + export default ExamWrapper; From 7a4d55d1c6a1cf567b574309d5aaf8403db2ac34 Mon Sep 17 00:00:00 2001 From: Viktor Rusakov Date: Fri, 2 Jul 2021 15:03:53 +0300 Subject: [PATCH 3/3] fix: remove review exam text on submitted page --- .../SubmittedTimedExamInstructions.jsx | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/src/instructions/timed_exam/SubmittedTimedExamInstructions.jsx b/src/instructions/timed_exam/SubmittedTimedExamInstructions.jsx index 81a66cd7..105fd7ae 100644 --- a/src/instructions/timed_exam/SubmittedTimedExamInstructions.jsx +++ b/src/instructions/timed_exam/SubmittedTimedExamInstructions.jsx @@ -6,30 +6,21 @@ const SubmittedTimedExamInstructions = () => { const state = useContext(ExamStateContext); return ( - <> -

- {state.timeIsOver - ? ( - - ) - : ( - - )} -

-

- -

- +

+ {state.timeIsOver + ? ( + + ) + : ( + + )} +

); };