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
+ ? (
+
+ )
+ : (
+
+ )}
+
);
};