From 586a81d6a09fab50943c5b99aa563bf143dd4413 Mon Sep 17 00:00:00 2001 From: Viktor Rusakov <52399399+ViktorRusakov@users.noreply.github.com> Date: Tue, 15 Jun 2021 18:21:05 +0300 Subject: [PATCH] fix: do not show continue button on ready_to_submit pages if timer reached 0 (#22) --- src/instructions/Instructions.test.jsx | 37 +++++++++++-- src/instructions/SubmitInstructions.jsx | 55 ++++++++++++++----- src/instructions/index.jsx | 2 +- .../ProctoredExamInstructions.test.jsx | 15 ++++- .../SubmitProctoredExamInstructions.jsx | 10 +--- .../SubmitTimedExamInstructions.jsx | 11 +--- src/timer/TimerProvider.jsx | 7 +++ src/timer/events.js | 1 + 8 files changed, 96 insertions(+), 42 deletions(-) diff --git a/src/instructions/Instructions.test.jsx b/src/instructions/Instructions.test.jsx index fd5bebbe..78ab827c 100644 --- a/src/instructions/Instructions.test.jsx +++ b/src/instructions/Instructions.test.jsx @@ -3,7 +3,10 @@ import React from 'react'; import { fireEvent } from '@testing-library/dom'; import Instructions from './index'; import { store, getExamAttemptsData, startTimedExam } from '../data'; -import { render, screen } from '../setupTest'; +import { continueExam, submitExam } from '../data/thunks'; +import Emitter from '../data/emitter'; +import { TIMER_REACHED_NULL } from '../timer/events'; +import { render, screen, act } from '../setupTest'; import { ExamStateProvider } from '../index'; import { ExamStatus, ExamType, INCOMPLETE_STATUSES } from '../constants'; @@ -12,6 +15,13 @@ jest.mock('../data', () => ({ getExamAttemptsData: jest.fn(), startTimedExam: jest.fn(), })); +jest.mock('../data/thunks', () => ({ + continueExam: jest.fn(), + getExamReviewPolicy: jest.fn(), + submitExam: jest.fn(), +})); +continueExam.mockReturnValue(jest.fn()); +submitExam.mockReturnValue(jest.fn()); getExamAttemptsData.mockReturnValue(jest.fn()); startTimedExam.mockReturnValue(jest.fn()); store.subscribe = jest.fn(); @@ -319,7 +329,7 @@ describe('SequenceExamWrapper', () => { expect(screen.getByTestId('start-exam-button')).toHaveTextContent('Continue to my proctored exam.'); }); - it('Instructions for ready to submit status', () => { + it.each([10, 0])('Shows correct instructions when attempt status is ready_to_submit and %s seconds left', async (secondsLeft) => { store.getState = () => ({ examState: { isLoading: false, @@ -329,7 +339,9 @@ describe('SequenceExamWrapper', () => { can_verify: true, }, proctoringSettings: {}, - activeAttempt: {}, + activeAttempt: { + time_remaining_seconds: secondsLeft, + }, exam: { type: ExamType.TIMED, time_limit_mins: 30, @@ -341,7 +353,7 @@ describe('SequenceExamWrapper', () => { }, }); - const { getByTestId } = render( + const { queryByTestId } = render(
Sequence
@@ -349,7 +361,22 @@ describe('SequenceExamWrapper', () => {
, { store }, ); - expect(getByTestId('exam-instructions-title')).toHaveTextContent('Are you sure that you want to submit your timed exam?'); + + expect(queryByTestId('exam-instructions-title')).toHaveTextContent('Are you sure that you want to submit your timed exam?'); + fireEvent.click(queryByTestId('end-exam-button')); + expect(submitExam).toHaveBeenCalled(); + const continueButton = queryByTestId('continue-exam-button'); + if (secondsLeft > 0) { + expect(continueButton).toBeInTheDocument(); + fireEvent.click(continueButton); + expect(continueExam).toHaveBeenCalledTimes(1); + act(() => { + Emitter.emit(TIMER_REACHED_NULL); + }); + expect(queryByTestId('continue-exam-button')).not.toBeInTheDocument(); + } else { + expect(continueButton).not.toBeInTheDocument(); + } }); it('Instructions for submitted status', () => { diff --git a/src/instructions/SubmitInstructions.jsx b/src/instructions/SubmitInstructions.jsx index 01b0b544..a93a29ef 100644 --- a/src/instructions/SubmitInstructions.jsx +++ b/src/instructions/SubmitInstructions.jsx @@ -1,24 +1,49 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Container } from '@edx/paragon'; +import React, { useContext, useEffect, useState } from 'react'; +import { Button, Container } from '@edx/paragon'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import Emitter from '../data/emitter'; import { ExamType } from '../constants'; import { SubmitProctoredExamInstructions } from './proctored_exam'; import { SubmitTimedExamInstructions } from './timed_exam'; import Footer from './proctored_exam/Footer'; +import ExamStateContext from '../context'; +import { TIMER_REACHED_NULL } from '../timer/events'; -const SubmitExamInstructions = ({ examType }) => ( -
- - {examType === ExamType.TIMED - ? - : } - - {examType !== ExamType.TIMED &&
-); +const SubmitExamInstructions = () => { + const state = useContext(ExamStateContext); + const { exam, continueExam, activeAttempt } = state; + const { time_remaining_seconds: timeRemaining } = activeAttempt; + const { type: examType } = exam || {}; + const [canContinue, setCanContinue] = useState(timeRemaining > 0); -SubmitExamInstructions.propTypes = { - examType: PropTypes.string.isRequired, + const hideContinueButton = () => setCanContinue(false); + + useEffect(() => { + Emitter.once(TIMER_REACHED_NULL, hideContinueButton); + + return () => { + Emitter.off(TIMER_REACHED_NULL, hideContinueButton); + }; + }, []); + + return ( +
+ + {examType === ExamType.TIMED + ? + : } + {canContinue && ( + + )} + + {examType !== ExamType.TIMED &&
+ ); }; export default SubmitExamInstructions; diff --git a/src/instructions/index.jsx b/src/instructions/index.jsx index 7b4c6440..356d55db 100644 --- a/src/instructions/index.jsx +++ b/src/instructions/index.jsx @@ -70,7 +70,7 @@ const Instructions = ({ children }) => { case attempt.attempt_status === ExamStatus.READY_TO_START: return ; case attempt.attempt_status === ExamStatus.READY_TO_SUBMIT: - return ; + return ; case attempt.attempt_status === ExamStatus.SUBMITTED: return ; case attempt.attempt_status === ExamStatus.VERIFIED: diff --git a/src/instructions/proctored_exam/ProctoredExamInstructions.test.jsx b/src/instructions/proctored_exam/ProctoredExamInstructions.test.jsx index 1e8cc83c..1a5e328c 100644 --- a/src/instructions/proctored_exam/ProctoredExamInstructions.test.jsx +++ b/src/instructions/proctored_exam/ProctoredExamInstructions.test.jsx @@ -3,6 +3,7 @@ import React from 'react'; import { fireEvent } from '@testing-library/dom'; import Instructions from '../index'; import { store, getExamAttemptsData } from '../../data'; +import { submitExam } from '../../data/thunks'; import { render } from '../../setupTest'; import { ExamStateProvider } from '../../index'; import { @@ -16,6 +17,11 @@ jest.mock('../../data', () => ({ store: {}, getExamAttemptsData: jest.fn(), })); +jest.mock('../../data/thunks', () => ({ + getExamReviewPolicy: jest.fn(), + submitExam: jest.fn(), +})); +submitExam.mockReturnValue(jest.fn()); getExamAttemptsData.mockReturnValue(jest.fn()); store.subscribe = jest.fn(); store.dispatch = jest.fn(); @@ -170,7 +176,7 @@ describe('SequenceExamWrapper', () => { expect(getByTestId('proctored-exam-instructions-title')).toHaveTextContent('You have submitted this proctored exam for review'); }); - it('Instructions are shown when attempt status is ready_to_submit', () => { + it('Shows correct instructions when attempt status is ready_to_submit ', () => { store.getState = () => ({ examState: { isLoading: false, @@ -194,7 +200,7 @@ describe('SequenceExamWrapper', () => { }, }); - const { getByTestId } = render( + const { queryByTestId } = render(
Sequence
@@ -202,7 +208,10 @@ describe('SequenceExamWrapper', () => {
, { store }, ); - expect(getByTestId('proctored-exam-instructions-title')).toHaveTextContent('Are you sure you want to end your proctored exam?'); + + expect(queryByTestId('proctored-exam-instructions-title')).toHaveTextContent('Are you sure you want to end your proctored exam?'); + fireEvent.click(queryByTestId('end-exam-button')); + expect(submitExam).toHaveBeenCalled(); }); it('Instructions are shown when attempt status is verified', () => { diff --git a/src/instructions/proctored_exam/SubmitProctoredExamInstructions.jsx b/src/instructions/proctored_exam/SubmitProctoredExamInstructions.jsx index 94b144a9..ca99f02a 100644 --- a/src/instructions/proctored_exam/SubmitProctoredExamInstructions.jsx +++ b/src/instructions/proctored_exam/SubmitProctoredExamInstructions.jsx @@ -8,7 +8,6 @@ const SubmitProctoredExamInstructions = () => { const state = useContext(ExamStateContext); const { submitExam, - continueExam, exam, activeAttempt, } = state; @@ -49,19 +48,12 @@ const SubmitProctoredExamInstructions = () => { />

)} - -   - ); }; diff --git a/src/instructions/timed_exam/SubmitTimedExamInstructions.jsx b/src/instructions/timed_exam/SubmitTimedExamInstructions.jsx index 7108d196..ee6cfe7b 100644 --- a/src/instructions/timed_exam/SubmitTimedExamInstructions.jsx +++ b/src/instructions/timed_exam/SubmitTimedExamInstructions.jsx @@ -5,7 +5,7 @@ import ExamStateContext from '../../context'; const SubmitTimedExamInstructions = () => { const state = useContext(ExamStateContext); - const { submitExam, continueExam } = state; + const { submitExam } = state; return ( <> @@ -27,19 +27,12 @@ const SubmitTimedExamInstructions = () => { defaultMessage="After you submit your exam, your exam will be graded." />

- -   - ); }; diff --git a/src/timer/TimerProvider.jsx b/src/timer/TimerProvider.jsx index 5458885c..781a32c8 100644 --- a/src/timer/TimerProvider.jsx +++ b/src/timer/TimerProvider.jsx @@ -6,6 +6,7 @@ import { TIMER_IS_CRITICALLY_LOW, TIMER_IS_LOW, TIMER_LIMIT_REACHED, + TIMER_REACHED_NULL, } from './events'; import { withExamStore } from '../hocs'; @@ -62,6 +63,12 @@ const TimerServiceProvider = ({ } else if (secondsLeft <= lowTime) { Emitter.emit(TIMER_IS_LOW); } + // Used to hide continue exam button on submit exam pages. + // Since TIME_LIMIT_REACHED is fired after the grace period we + // need to emit separate event when timer reaches 00:00 + if (secondsLeft <= 0) { + Emitter.emit(TIMER_REACHED_NULL); + } if (!limitReached && secondsLeft < LIMIT) { clearInterval(timer); setLimitReached(); diff --git a/src/timer/events.js b/src/timer/events.js index 9d246641..37a4e0ea 100644 --- a/src/timer/events.js +++ b/src/timer/events.js @@ -1,3 +1,4 @@ export const TIMER_IS_LOW = 'timer_is_low'; export const TIMER_IS_CRITICALLY_LOW = 'timer_is_critical'; export const TIMER_LIMIT_REACHED = 'timer_time_is_over'; +export const TIMER_REACHED_NULL = 'timer_reached_null';