From a4012e6c66a2dd133c4f34326dff90c91f84a75c Mon Sep 17 00:00:00 2001 From: Simon Chen Date: Thu, 23 Sep 2021 09:19:12 -0400 Subject: [PATCH] fix: The special exam timer synced with backend to show accurate count down timer (#45) This is a revert of a previous revert. This reverts commit e6aa31c100102235d04bd8d5507fc924a150719a. This PR would use the proper sermantic-release commit message. The special exam timer shown to learners should be updated with the exam attempt remaining time available for each sync interval. This is to ensure our learners don't see a timer which is running slower than the actual time. Co-authored-by: Simon Chen --- src/timer/CountDownTimer.test.jsx | 63 +++++++++++++++++++++++++++++++ src/timer/TimerProvider.jsx | 19 ++++++---- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/timer/CountDownTimer.test.jsx b/src/timer/CountDownTimer.test.jsx index 4d90c506..a706a264 100644 --- a/src/timer/CountDownTimer.test.jsx +++ b/src/timer/CountDownTimer.test.jsx @@ -52,6 +52,7 @@ describe('ExamTimerBlock', () => { stopExamAttempt={stopExamAttempt} expireExamAttempt={expireExamAttempt} pollExamAttempt={pollAttempt} + submitExam={submitAttempt} />, ); @@ -80,6 +81,7 @@ describe('ExamTimerBlock', () => { stopExamAttempt={stopExamAttempt} expireExamAttempt={expireExamAttempt} pollExamAttempt={pollAttempt} + submitExam={submitAttempt} />, ); expect(container.firstChild).not.toBeInTheDocument(); @@ -92,6 +94,7 @@ describe('ExamTimerBlock', () => { stopExamAttempt={stopExamAttempt} expireExamAttempt={expireExamAttempt} pollExamAttempt={pollAttempt} + submitExam={submitAttempt} />, ); await waitFor(() => expect(screen.getByText('00:00:09')).toBeInTheDocument()); @@ -127,6 +130,7 @@ describe('ExamTimerBlock', () => { stopExamAttempt={stopExamAttempt} expireExamAttempt={expireExamAttempt} pollExamAttempt={pollAttempt} + submitExam={submitAttempt} />, ); await waitFor(() => expect(screen.getByText('00:00:04')).toBeInTheDocument()); @@ -140,6 +144,7 @@ describe('ExamTimerBlock', () => { stopExamAttempt={stopExamAttempt} expireExamAttempt={expireExamAttempt} pollExamAttempt={pollAttempt} + submitExam={submitAttempt} />, ); await waitFor(() => expect(screen.getByText('00:00:09')).toBeInTheDocument()); @@ -162,6 +167,7 @@ describe('ExamTimerBlock', () => { stopExamAttempt={stopExamAttempt} expireExamAttempt={expireExamAttempt} pollExamAttempt={pollAttempt} + submitExam={submitAttempt} />, ); await waitFor(() => expect(screen.getByText('00:00:09')).toBeInTheDocument()); @@ -230,4 +236,61 @@ describe('ExamTimerBlock', () => { fireEvent.click(screen.getByTestId('end-button')); expect(stopExamAttempt).toHaveBeenCalledTimes(1); }); + + it('Update exam timer when attempt time_remaining_seconds is smaller than displayed time', async () => { + const preloadedState = { + examState: { + isLoading: true, + timeIsOver: false, + activeAttempt: { + attempt_status: 'started', + exam_url_path: 'exam_url_path', + exam_display_name: 'exam name', + time_remaining_seconds: 240, + low_threshold_sec: 15, + critically_low_threshold_sec: 5, + exam_started_poll_url: '', + taking_as_proctored: false, + exam_type: 'a timed exam', + }, + proctoringSettings: {}, + exam: {}, + }, + }; + let testStore = await initializeTestStore(preloadedState); + examStore.getState = store.testStore; + attempt = testStore.getState().examState.activeAttempt; + const { rerender } = render( + , + ); + await waitFor(() => expect(screen.getByText('00:03:59')).toBeInTheDocument()); + + preloadedState.examState.activeAttempt = { + ...attempt, + time_remaining_seconds: 20, + }; + testStore = await initializeTestStore(preloadedState); + examStore.getState = store.testStore; + const updatedAttempt = testStore.getState().examState.activeAttempt; + + expect(updatedAttempt.time_remaining_seconds).toBe(20); + + rerender( + , + ); + + await waitFor(() => expect(screen.getByText('00:00:19')).toBeInTheDocument()); + }); }); diff --git a/src/timer/TimerProvider.jsx b/src/timer/TimerProvider.jsx index 781a32c8..3e1bfd05 100644 --- a/src/timer/TimerProvider.jsx +++ b/src/timer/TimerProvider.jsx @@ -39,8 +39,8 @@ const TimerServiceProvider = ({ critically_low_threshold_sec: criticalLowTime, low_threshold_sec: lowTime, } = attempt; - const startValue = Math.floor(timeRemaining); const LIMIT = GRACE_PERIOD_SECS ? 0 - GRACE_PERIOD_SECS : 0; + let liveInterval = null; const getTimeString = () => Object.values(timeState).map( item => { @@ -77,26 +77,29 @@ const TimerServiceProvider = ({ }; useEffect(() => { - let secondsLeft = startValue; let timerTick = 0; - const interval = setInterval(() => { + let secondsLeft = Math.floor(timeRemaining); + liveInterval = setInterval(() => { secondsLeft -= 1; timerTick += 1; setTimeState(getFormattedRemainingTime(secondsLeft)); - processTimeLeft(interval, secondsLeft); + processTimeLeft(liveInterval, secondsLeft); // no polling during grace period if (timerTick % POLL_INTERVAL === 0 && secondsLeft >= 0) { pollExam(); } - // if exam is proctored ping provider app also if (workerUrl && timerTick % pingInterval === pingInterval / 2) { pingHandler(pingInterval, workerUrl); } }, 1000); - - return () => { clearInterval(interval); }; - }, []); + return () => { + if (liveInterval) { + clearInterval(liveInterval); + liveInterval = null; + } + }; + }, [timeRemaining]); return (