Skip to content

Commit

Permalink
fix: do not show continue button on ready_to_submit pages if timer re…
Browse files Browse the repository at this point in the history
…ached 0 (#22)
  • Loading branch information
viktorrusakov authored Jun 15, 2021
1 parent 7e74a25 commit 586a81d
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 42 deletions.
37 changes: 32 additions & 5 deletions src/instructions/Instructions.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -329,7 +339,9 @@ describe('SequenceExamWrapper', () => {
can_verify: true,
},
proctoringSettings: {},
activeAttempt: {},
activeAttempt: {
time_remaining_seconds: secondsLeft,
},
exam: {
type: ExamType.TIMED,
time_limit_mins: 30,
Expand All @@ -341,15 +353,30 @@ describe('SequenceExamWrapper', () => {
},
});

const { getByTestId } = render(
const { queryByTestId } = render(
<ExamStateProvider>
<Instructions>
<div>Sequence</div>
</Instructions>
</ExamStateProvider>,
{ 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', () => {
Expand Down
55 changes: 40 additions & 15 deletions src/instructions/SubmitInstructions.jsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div>
<Container className="border py-5 mb-4">
{examType === ExamType.TIMED
? <SubmitTimedExamInstructions />
: <SubmitProctoredExamInstructions />}
</Container>
{examType !== ExamType.TIMED && <Footer />}
</div>
);
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 (
<div>
<Container className="border py-5 mb-4">
{examType === ExamType.TIMED
? <SubmitTimedExamInstructions />
: <SubmitProctoredExamInstructions />}
{canContinue && (
<Button variant="outline-primary" onClick={continueExam} data-testid="continue-exam-button">
<FormattedMessage
id="exam.SubmitExamInstructions.continueButton"
defaultMessage="No, I'd like to continue working"
/>
</Button>
)}
</Container>
{examType !== ExamType.TIMED && <Footer />}
</div>
);
};

export default SubmitExamInstructions;
2 changes: 1 addition & 1 deletion src/instructions/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const Instructions = ({ children }) => {
case attempt.attempt_status === ExamStatus.READY_TO_START:
return <ReadyToStartProctoredExamInstructions />;
case attempt.attempt_status === ExamStatus.READY_TO_SUBMIT:
return <SubmitExamInstructions examType={examType} />;
return <SubmitExamInstructions />;
case attempt.attempt_status === ExamStatus.SUBMITTED:
return <SubmittedExamInstructions examType={examType} />;
case attempt.attempt_status === ExamStatus.VERIFIED:
Expand Down
15 changes: 12 additions & 3 deletions src/instructions/proctored_exam/ProctoredExamInstructions.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -194,15 +200,18 @@ describe('SequenceExamWrapper', () => {
},
});

const { getByTestId } = render(
const { queryByTestId } = render(
<ExamStateProvider>
<Instructions>
<div>Sequence</div>
</Instructions>
</ExamStateProvider>,
{ 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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const SubmitProctoredExamInstructions = () => {
const state = useContext(ExamStateContext);
const {
submitExam,
continueExam,
exam,
activeAttempt,
} = state;
Expand Down Expand Up @@ -49,19 +48,12 @@ const SubmitProctoredExamInstructions = () => {
/>
</p>
)}
<Button variant="primary" onClick={submitExam}>
<Button variant="primary" onClick={submitExam} className="mr-2" data-testid="end-exam-button">
<FormattedMessage
id="exam.SubmitProctoredExamInstructions.submit"
defaultMessage="Yes, end my proctored exam"
/>
</Button>
&nbsp;
<Button variant="outline-primary" onClick={continueExam}>
<FormattedMessage
id="exam.SubmitProctoredExamInstructions.continue"
defaultMessage="No, I'd like to continue working"
/>
</Button>
</>
);
};
Expand Down
11 changes: 2 additions & 9 deletions src/instructions/timed_exam/SubmitTimedExamInstructions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ExamStateContext from '../../context';

const SubmitTimedExamInstructions = () => {
const state = useContext(ExamStateContext);
const { submitExam, continueExam } = state;
const { submitExam } = state;

return (
<>
Expand All @@ -27,19 +27,12 @@ const SubmitTimedExamInstructions = () => {
defaultMessage="After you submit your exam, your exam will be graded."
/>
</p>
<Button variant="primary" onClick={submitExam}>
<Button variant="primary" onClick={submitExam} className="mr-2" data-testid="end-exam-button">
<FormattedMessage
id="exam.submitExamInstructions.submit"
defaultMessage="Yes, submit my timed exam."
/>
</Button>
&nbsp;
<Button variant="outline-primary" onClick={continueExam}>
<FormattedMessage
id="exam.submitExamInstructions.continue"
defaultMessage="No, I want to continue working."
/>
</Button>
</>
);
};
Expand Down
7 changes: 7 additions & 0 deletions src/timer/TimerProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
TIMER_IS_CRITICALLY_LOW,
TIMER_IS_LOW,
TIMER_LIMIT_REACHED,
TIMER_REACHED_NULL,
} from './events';
import { withExamStore } from '../hocs';

Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/timer/events.js
Original file line number Diff line number Diff line change
@@ -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';

0 comments on commit 586a81d

Please sign in to comment.