From 0c12634084b788739e66938cfeb64fe9f916bd56 Mon Sep 17 00:00:00 2001 From: Zachary Hancock Date: Wed, 22 Dec 2021 14:21:59 -0500 Subject: [PATCH] Improve Exam Start Failure (#54) * feat: start exam action fail after 2 seconds * feat: pollAttempt log error * feat: loading spinner while waiting for exam start * style: use const for timeout --- src/data/redux.test.jsx | 2 +- src/data/thunks.js | 24 ++++++-- src/instructions/Instructions.test.jsx | 60 +++++++++++++++++++ .../ReadyToStartProctoredExamInstructions.jsx | 14 ++++- 4 files changed, 90 insertions(+), 10 deletions(-) diff --git a/src/data/redux.test.jsx b/src/data/redux.test.jsx index 6a12beab..73403f2f 100644 --- a/src/data/redux.test.jsx +++ b/src/data/redux.test.jsx @@ -546,7 +546,7 @@ describe('Data layer integration tests', () => { await executeThunk(thunks.getExamAttemptsData(courseId, contentId), store.dispatch); await executeThunk(thunks.startProctoredExam(), store.dispatch, store.getState); - expect(loggingService.logInfo).toHaveBeenCalledWith( + expect(loggingService.logError).toHaveBeenCalledWith( Error('test error'), { attemptId: createdWorkerAttempt.attempt_id, courseId: createdWorkerAttempt.course_id, diff --git a/src/data/thunks.js b/src/data/thunks.js index 4f2d54e4..1aa9f6b1 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -1,4 +1,4 @@ -import { logError, logInfo } from '@edx/frontend-platform/logging'; +import { logError } from '@edx/frontend-platform/logging'; import { fetchExamAttemptsData, createExamAttempt, @@ -35,6 +35,8 @@ function handleAPIError(error, dispatch) { dispatch(setApiError({ errorMsg: message || detail })); } +const EXAM_START_TIMEOUT_MILLISECONDS = 5000; + /** * Fetch attempt data and update exam state after performing another action if it is provided. * It is assumed that action somehow modifies attempt in the backend, that's why the state needs @@ -146,13 +148,15 @@ export function startProctoredExam() { const useWorker = window.Worker && workerUrl; if (useWorker) { - workerPromiseForEventNames(actionToMessageTypesMap.start, exam.attempt.desktop_application_js_url)() - .then(() => updateAttemptAfter( - exam.course_id, exam.content_id, continueAttempt(attempt.attempt_id), - )(dispatch)) + const startExamTimeoutMilliseconds = EXAM_START_TIMEOUT_MILLISECONDS; + workerPromiseForEventNames(actionToMessageTypesMap.start, exam.attempt.desktop_application_js_url)( + startExamTimeoutMilliseconds, + ).then(() => updateAttemptAfter( + exam.course_id, exam.content_id, continueAttempt(attempt.attempt_id), + )(dispatch)) .catch(error => { if (error) { - logInfo( + logError( error, { attemptId: attempt.attempt_id, @@ -381,6 +385,14 @@ export function pingAttempt(timeoutInSeconds, workerUrl) { .catch(async (error) => { const { exam, activeAttempt } = getState().examState; const message = error ? error.message : 'Worker failed to respond.'; + logError( + message, + { + attemptId: activeAttempt.attempt_id, + courseId: activeAttempt.course_id, + examId: activeAttempt.exam.id, + }, + ); await updateAttemptAfter( exam.course_id, exam.content_id, endExamWithFailure(activeAttempt.attempt_id, message), )(dispatch); diff --git a/src/instructions/Instructions.test.jsx b/src/instructions/Instructions.test.jsx index 263c12b4..4955226d 100644 --- a/src/instructions/Instructions.test.jsx +++ b/src/instructions/Instructions.test.jsx @@ -891,4 +891,64 @@ describe('SequenceExamWrapper', () => { expect(screen.getByTestId('unknown-status-error')).toBeInTheDocument(); }); + + it('Shows ready to start page when proctored exam is in ready_to_start status', () => { + store.getState = () => ({ + examState: Factory.build('examState', { + proctoringSettings: Factory.build('proctoringSettings', { + platform_name: 'Your Platform', + }), + activeAttempt: {}, + exam: Factory.build('exam', { + is_proctored: true, + type: ExamType.PROCTORED, + attempt: Factory.build('attempt', { + attempt_status: ExamStatus.READY_TO_START, + }), + }), + }), + }); + + render( + + +
Sequence
+
+
, + { store }, + ); + + expect(screen.getByText('You must adhere to the following rules while you complete this exam.')).toBeInTheDocument(); + }); + + it('Shows loading spinner while waiting to start exam', () => { + store.getState = () => ({ + examState: Factory.build('examState', { + proctoringSettings: Factory.build('proctoringSettings', { + platform_name: 'Your Platform', + }), + activeAttempt: {}, + exam: Factory.build('exam', { + is_proctored: true, + type: ExamType.PROCTORED, + attempt: Factory.build('attempt', { + attempt_status: ExamStatus.READY_TO_START, + }), + }), + startProctoredExam: jest.fn(), + }), + }); + + render( + + +
Sequence
+
+
, + { store }, + ); + + fireEvent.click(screen.getByTestId('start-exam-button')); + expect(screen.getByTestId('exam-loading-spinner')).toBeInTheDocument(); + }); }); diff --git a/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx b/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx index 29a6e5ab..60de1307 100644 --- a/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx +++ b/src/instructions/proctored_exam/ReadyToStartProctoredExamInstructions.jsx @@ -1,6 +1,6 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import { Button, Container } from '@edx/paragon'; +import { Button, Container, Spinner } from '@edx/paragon'; import ExamStateContext from '../../context'; import Footer from './Footer'; @@ -16,11 +16,17 @@ const ReadyToStartProctoredExamInstructions = () => { const { total_time: examDuration } = attempt; const { link_urls: linkUrls, platform_name: platformName } = proctoringSettings; const rulesUrl = linkUrls && linkUrls.online_proctoring_rules; + const [beginExamClicked, setBeginExamClicked] = useState(false); useEffect(() => { getExamReviewPolicy(); }, []); + const handleStart = () => { + setBeginExamClicked(true); + startProctoredExam(); + }; + return (
@@ -113,8 +119,10 @@ const ReadyToStartProctoredExamInstructions = () => {