Skip to content

Commit

Permalink
[BD-26] Timer bar on non-sequence pages (#51)
Browse files Browse the repository at this point in the history
* feat: Timer bar on non-sequence pages

* feat: update fetchExamAttempts URL

* test: add tests for stopExam
  • Loading branch information
UvgenGen authored Jun 25, 2021
1 parent 0a13b26 commit ca23071
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 10 deletions.
6 changes: 5 additions & 1 deletion src/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
39 changes: 37 additions & 2 deletions src/data/redux.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -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 });
Expand Down
16 changes: 11 additions & 5 deletions src/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};
}

Expand Down Expand Up @@ -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());

Expand Down
8 changes: 6 additions & 2 deletions src/exam/ExamWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit ca23071

Please sign in to comment.