Skip to content

Commit

Permalink
feat: start lti proctoring software (#101)
Browse files Browse the repository at this point in the history
* feat: start lti proctoring software
  • Loading branch information
zacharis278 authored Apr 19, 2023
1 parent 431ab82 commit 34b88df
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 27 deletions.
167 changes: 163 additions & 4 deletions src/instructions/Instructions.test.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import '@testing-library/jest-dom';
import { Factory } from 'rosie';
import React from 'react';
import { fireEvent } from '@testing-library/dom';
import { fireEvent, waitFor } from '@testing-library/dom';
import Instructions from './index';
import { store, getExamAttemptsData, startTimedExam } from '../data';
import { pollExamAttempt } from '../data/api';
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 {
render, screen, act, initializeMockApp,
} from '../setupTest';
import ExamStateProvider from '../core/ExamStateProvider';
import {
ExamStatus, ExamType, INCOMPLETE_STATUSES,
Expand All @@ -23,14 +26,23 @@ jest.mock('../data/thunks', () => ({
getExamReviewPolicy: jest.fn(),
submitExam: jest.fn(),
}));
jest.mock('../data/api', () => ({
pollExamAttempt: jest.fn(),
softwareDownloadAttempt: jest.fn(),
}));
continueExam.mockReturnValue(jest.fn());
submitExam.mockReturnValue(jest.fn());
getExamAttemptsData.mockReturnValue(jest.fn());
startTimedExam.mockReturnValue(jest.fn());
pollExamAttempt.mockReturnValue(Promise.resolve({}));
store.subscribe = jest.fn();
store.dispatch = jest.fn();

describe('SequenceExamWrapper', () => {
beforeEach(() => {
initializeMockApp();
});

it('Start exam instructions can be successfully rendered', () => {
store.getState = () => ({ examState: Factory.build('examState') });

Expand Down Expand Up @@ -709,7 +721,116 @@ describe('SequenceExamWrapper', () => {
expect(screen.getByText('You have submitted this proctored exam for review')).toBeInTheDocument();
});

it('Shows download software proctored exam instructions if attempt status is created', () => {
it('Shows correct download instructions for LTI provider if attempt status is created', () => {
store.getState = () => ({
examState: Factory.build('examState', {
activeAttempt: {},
proctoringSettings: Factory.build('proctoringSettings', {
provider_name: 'LTI Provider',
provider_tech_support_email: '[email protected]',
provider_tech_support_phone: '+123456789',
}),
exam: Factory.build('exam', {
is_proctored: true,
type: ExamType.PROCTORED,
attempt: Factory.build('attempt', {
attempt_status: ExamStatus.CREATED,
}),
}),
}),
});

render(
<ExamStateProvider>
<Instructions>
<div>Sequence</div>
</Instructions>
</ExamStateProvider>,
{ store },
);

expect(screen.getByText(
'If you have issues relating to proctoring, you can contact '
+ 'LTI Provider technical support by emailing [email protected] or by calling +123456789.',
)).toBeInTheDocument();
expect(screen.getByText('Set up and start your proctored exam.')).toBeInTheDocument();
expect(screen.getByText('Start System Check')).toBeInTheDocument();
expect(screen.getByText('Start Exam')).toBeInTheDocument();
});

it('Hides support contact info on download instructions for LTI provider if not provided', () => {
store.getState = () => ({
examState: Factory.build('examState', {
activeAttempt: {},
proctoringSettings: Factory.build('proctoringSettings', {
provider_name: 'LTI Provider',
}),
exam: Factory.build('exam', {
is_proctored: true,
type: ExamType.PROCTORED,
attempt: Factory.build('attempt', {
attempt_status: ExamStatus.CREATED,
}),
}),
}),
});

render(
<ExamStateProvider>
<Instructions>
<div>Sequence</div>
</Instructions>
</ExamStateProvider>,
{ store },
);

expect(screen.queryByText('If you have issues relating to proctoring, you can contact LTI Provider')).toBeNull();
expect(screen.getByText('Set up and start your proctored exam.')).toBeInTheDocument();
expect(screen.getByText('Start System Check')).toBeInTheDocument();
expect(screen.getByText('Start Exam')).toBeInTheDocument();
});

it('Initiates an LTI launch in a new window when the user clicks the System Check button', async () => {
const windowSpy = jest.spyOn(window, 'open');
windowSpy.mockImplementation(() => ({}));
store.getState = () => ({
examState: Factory.build('examState', {
activeAttempt: {},
proctoringSettings: Factory.build('proctoringSettings', {
provider_name: 'LTI Provider',
provider_tech_support_email: '[email protected]',
provider_tech_support_phone: '+123456789',
}),
exam: Factory.build('exam', {
is_proctored: true,
type: ExamType.PROCTORED,
attempt: Factory.build('attempt', {
attempt_id: 4321,
attempt_status: ExamStatus.CREATED,
}),
}),
getExamAttemptsData,
}),
});

render(
<ExamStateProvider>
<Instructions>
<div>Sequence</div>
</Instructions>
</ExamStateProvider>,
{ store },
);
fireEvent.click(screen.getByText('Start System Check'));
await waitFor(() => { expect(windowSpy).toHaveBeenCalledWith('http://localhost:18740/lti/start_proctoring/4321', '_blank'); });

// also validate start button works
pollExamAttempt.mockReturnValue(Promise.resolve({ status: ExamStatus.READY_TO_START }));
fireEvent.click(screen.getByText('Start Exam'));
await waitFor(() => { expect(getExamAttemptsData).toHaveBeenCalled(); });
});

it('Shows correct download instructions for legacy rest provider if attempt status is created', () => {
const instructions = [
'instruction 1',
'instruction 2',
Expand All @@ -729,8 +850,10 @@ describe('SequenceExamWrapper', () => {
exam: Factory.build('exam', {
is_proctored: true,
type: ExamType.PROCTORED,
use_legacy_attempt_api: true,
attempt: Factory.build('attempt', {
attempt_status: ExamStatus.CREATED,
use_legacy_attempt_api: true,
}),
}),
}),
Expand All @@ -755,6 +878,41 @@ describe('SequenceExamWrapper', () => {
});
});

it('Shows correct download instructions for legacy rpnow provider if attempt status is created', () => {
store.getState = () => ({
examState: Factory.build('examState', {
activeAttempt: {},
proctoringSettings: Factory.build('proctoringSettings', {
provider_name: 'Provider Name',
exam_proctoring_backend: {},
}),
exam: Factory.build('exam', {
is_proctored: true,
type: ExamType.PROCTORED,
use_legacy_attempt_api: true,
attempt: Factory.build('attempt', {
attempt_status: ExamStatus.CREATED,
attempt_code: '1234-5678-9012-3456',
use_legacy_attempt_api: true,
}),
}),
}),
});

render(
<ExamStateProvider>
<Instructions>
<div>Sequence</div>
</Instructions>
</ExamStateProvider>,
{ store },
);
expect(screen.getByDisplayValue('1234-5678-9012-3456')).toBeInTheDocument();
expect(screen.getByText('For security and exam integrity reasons, '
+ 'we ask you to sign in to your edX account. Then we will '
+ 'direct you to the RPNow proctoring experience.')).toBeInTheDocument();
});

it('Shows error message if receives unknown attempt status', () => {
store.getState = () => ({
examState: Factory.build('examState', {
Expand Down Expand Up @@ -806,7 +964,7 @@ describe('SequenceExamWrapper', () => {
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', () => {
it('Shows loading spinner while waiting to start exam', async () => {
store.getState = () => ({
examState: Factory.build('examState', {
activeAttempt: {},
Expand All @@ -831,6 +989,7 @@ describe('SequenceExamWrapper', () => {
);

fireEvent.click(screen.getByTestId('start-exam-button'));
waitFor(() => expect(getExamAttemptsData).toHaveBeenCalled());
expect(screen.getByTestId('exam-loading-spinner')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

const LtiProviderExamInstructions = ({
providerName, supportEmail, supportPhone,
}) => (
<>
<p>
<FormattedMessage
id="exam.DownloadSoftwareProctoredExamInstructions.text1"
defaultMessage={'Note: As part of the proctored exam setup, you '
+ 'will be asked to verify your identity. Before you begin, make '
+ 'sure you are on a computer with a webcam, and that you have a '
+ 'valid form of photo identification such as a driver’s license or passport.'}
/>
</p>
{supportEmail && supportPhone && (
<p>
<FormattedMessage
id="exam.DownloadSoftwareProctoredExamInstructions.supportText"
defaultMessage={'If you have issues relating to proctoring, you can contact '
+ '{providerName} technical support by emailing {supportEmail} or by calling {supportPhone}.'}
values={{
providerName,
supportEmail,
supportPhone,
}}
/>
</p>
)}
</>
);

LtiProviderExamInstructions.propTypes = {
providerName: PropTypes.string,
supportEmail: PropTypes.string,
supportPhone: PropTypes.string,
};

LtiProviderExamInstructions.defaultProps = {
providerName: '',
supportEmail: '',
supportPhone: '',
};

export default LtiProviderExamInstructions;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import ExamCode from './ExamCode';

const DefaultInstructions = ({ code }) => (
const RPNowInstructions = ({ code }) => (
<>
<div className="h4">
<FormattedMessage
Expand Down Expand Up @@ -55,8 +55,8 @@ const DefaultInstructions = ({ code }) => (
</>
);

DefaultInstructions.propTypes = {
RPNowInstructions.propTypes = {
code: PropTypes.string.isRequired,
};

export default DefaultInstructions;
export default RPNowInstructions;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

const ProviderProctoredExamInstructions = ({
const RestProviderInstructions = ({
providerName, supportEmail, supportPhone, instructions,
}) => (
<>
Expand Down Expand Up @@ -39,17 +39,17 @@ const ProviderProctoredExamInstructions = ({
</>
);

ProviderProctoredExamInstructions.propTypes = {
RestProviderInstructions.propTypes = {
providerName: PropTypes.string,
supportEmail: PropTypes.string,
supportPhone: PropTypes.string,
instructions: PropTypes.arrayOf(PropTypes.string).isRequired,
};

ProviderProctoredExamInstructions.defaultProps = {
RestProviderInstructions.defaultProps = {
providerName: '',
supportEmail: '',
supportPhone: '',
};

export default ProviderProctoredExamInstructions;
export default RestProviderInstructions;
Loading

0 comments on commit 34b88df

Please sign in to comment.