feat: add escalation email field for LTI-based proctoring providers (#736)

This commit adds an escalation email field for LTI-based proctoring providers to the Proctoring modal on the Pages & Resources page. This field behaves identically to the Proctortrack escalation email.
This commit is contained in:
Michael Roytman
2023-12-12 14:28:23 -05:00
committed by GitHub
parent c5abd21569
commit 0f483dc4e1
4 changed files with 256 additions and 211 deletions

View File

@@ -1,5 +1,6 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { convertObjectToSnakeCase } from '../../utils';
class ExamsApiService { class ExamsApiService {
static isAvailable() { static isAvailable() {
@@ -26,8 +27,9 @@ class ExamsApiService {
} }
static saveCourseExamConfiguration(courseId, dataToSave) { static saveCourseExamConfiguration(courseId, dataToSave) {
const snakecaseDataToSave = convertObjectToSnakeCase(dataToSave, true);
const apiClient = getAuthenticatedHttpClient(); const apiClient = getAuthenticatedHttpClient();
return apiClient.patch(this.getExamConfigurationUrl(courseId), dataToSave); return apiClient.patch(this.getExamConfigurationUrl(courseId), snakecaseDataToSave);
} }
} }

View File

@@ -28,7 +28,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
const initialFormValues = { const initialFormValues = {
enableProctoredExams: false, enableProctoredExams: false,
proctoringProvider: false, proctoringProvider: false,
proctortrackEscalationEmail: '', escalationEmail: '',
allowOptingOut: false, allowOptingOut: false,
createZendeskTickets: false, createZendeskTickets: false,
}; };
@@ -44,7 +44,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
const [saveSuccess, setSaveSuccess] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false);
const [saveError, setSaveError] = useState(false); const [saveError, setSaveError] = useState(false);
const [submissionInProgress, setSubmissionInProgress] = useState(false); const [submissionInProgress, setSubmissionInProgress] = useState(false);
const [showProctortrackEscalationEmail, setShowProctortrackEscalationEmail] = useState(false); const [showEscalationEmail, setShowEscalationEmail] = useState(false);
const isEdxStaff = getAuthenticatedUser().administrator; const isEdxStaff = getAuthenticatedUser().administrator;
const [formStatus, setFormStatus] = useState({ const [formStatus, setFormStatus] = useState({
isValid: true, isValid: true,
@@ -53,6 +53,15 @@ const ProctoringSettings = ({ intl, onClose }) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const modalVariant = isMobile ? 'dark' : 'default'; const modalVariant = isMobile ? 'dark' : 'default';
const isLtiProvider = (provider) => (
ltiProctoringProviders.some(p => p.name === provider)
);
function getProviderDisplayLabel(provider) {
// if a display label exists for this provider return it
return ltiProctoringProviders.find(p => p.name === provider)?.verbose_name || provider;
}
const { courseId } = useContext(PagesAndResourcesContext); const { courseId } = useContext(PagesAndResourcesContext);
const appInfo = useModel('courseApps', 'proctoring'); const appInfo = useModel('courseApps', 'proctoring');
const alertRef = React.createRef(); const alertRef = React.createRef();
@@ -73,38 +82,36 @@ const ProctoringSettings = ({ intl, onClose }) => {
if (value === 'proctortrack') { if (value === 'proctortrack') {
setFormValues({ ...newFormValues, createZendeskTickets: false }); setFormValues({ ...newFormValues, createZendeskTickets: false });
setShowProctortrackEscalationEmail(true); setShowEscalationEmail(true);
} else if (value === 'software_secure') {
setFormValues({ ...newFormValues, createZendeskTickets: true });
setShowEscalationEmail(false);
} else if (isLtiProvider(value)) {
setFormValues(newFormValues);
setShowEscalationEmail(true);
} else { } else {
if (value === 'software_secure') { setFormValues(newFormValues);
setFormValues({ ...newFormValues, createZendeskTickets: true }); setShowEscalationEmail(false);
} else {
setFormValues(newFormValues);
}
setShowProctortrackEscalationEmail(false);
} }
} else { } else {
setFormValues({ ...formValues, [name]: value }); setFormValues({ ...formValues, [name]: value });
} }
}; };
function isLtiProvider(provider) { const setFocusToEscalationEmailInput = () => {
return ltiProctoringProviders.some(p => p.name === provider);
}
const setFocusToProctortrackEscalationEmailInput = () => {
if (proctoringEscalationEmailInputRef && proctoringEscalationEmailInputRef.current) { if (proctoringEscalationEmailInputRef && proctoringEscalationEmailInputRef.current) {
proctoringEscalationEmailInputRef.current.focus(); proctoringEscalationEmailInputRef.current.focus();
} }
}; };
function postSettingsBackToServer() { function postSettingsBackToServer() {
const providerIsLti = isLtiProvider(formValues.proctoringProvider); const selectedProvider = formValues.proctoringProvider;
const isLtiProviderSelected = isLtiProvider(selectedProvider);
const studioDataToPostBack = { const studioDataToPostBack = {
proctored_exam_settings: { proctored_exam_settings: {
enable_proctored_exams: formValues.enableProctoredExams, enable_proctored_exams: formValues.enableProctoredExams,
// lti providers are managed outside edx-platform, lti_external indicates this // lti providers are managed outside edx-platform, lti_external indicates this
proctoring_provider: providerIsLti ? 'lti_external' : formValues.proctoringProvider, proctoring_provider: isLtiProviderSelected ? 'lti_external' : selectedProvider,
create_zendesk_tickets: formValues.createZendeskTickets, create_zendesk_tickets: formValues.createZendeskTickets,
}, },
}; };
@@ -113,17 +120,23 @@ const ProctoringSettings = ({ intl, onClose }) => {
} }
if (formValues.proctoringProvider === 'proctortrack') { if (formValues.proctoringProvider === 'proctortrack') {
studioDataToPostBack.proctored_exam_settings.proctoring_escalation_email = formValues.proctortrackEscalationEmail === '' ? null : formValues.proctortrackEscalationEmail; studioDataToPostBack.proctored_exam_settings.proctoring_escalation_email = formValues.escalationEmail === '' ? null : formValues.escalationEmail;
} }
// only save back to exam service if necessary // only save back to exam service if necessary
setSubmissionInProgress(true); setSubmissionInProgress(true);
const saveOperations = [StudioApiService.saveProctoredExamSettingsData(courseId, studioDataToPostBack)]; const saveOperations = [StudioApiService.saveProctoredExamSettingsData(courseId, studioDataToPostBack)];
if (allowLtiProviders && ExamsApiService.isAvailable()) { if (allowLtiProviders && ExamsApiService.isAvailable()) {
const selectedEscalationEmail = formValues.escalationEmail;
saveOperations.push( saveOperations.push(
ExamsApiService.saveCourseExamConfiguration( ExamsApiService.saveCourseExamConfiguration(
courseId, courseId,
{ provider: providerIsLti ? formValues.proctoringProvider : null }, {
provider: isLtiProviderSelected ? formValues.proctoringProvider : null,
escalationEmail: (isLtiProviderSelected && selectedEscalationEmail !== '') ? selectedEscalationEmail : null,
},
), ),
); );
} }
@@ -141,20 +154,21 @@ const ProctoringSettings = ({ intl, onClose }) => {
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
const isLtiProviderSelected = isLtiProvider(formValues.proctoringProvider);
if ( if (
formValues.proctoringProvider === 'proctortrack' (formValues.proctoringProvider === 'proctortrack' || isLtiProviderSelected)
&& !EmailValidator.validate(formValues.proctortrackEscalationEmail) && !EmailValidator.validate(formValues.escalationEmail)
&& !(formValues.proctortrackEscalationEmail === '' && !formValues.enableProctoredExams) && !(formValues.escalationEmail === '' && !formValues.enableProctoredExams)
) { ) {
if (formValues.proctortrackEscalationEmail === '') { if (formValues.escalationEmail === '') {
const errorMessage = intl.formatMessage(messages['authoring.proctoring.escalationemail.error.blank']); const errorMessage = intl.formatMessage(messages['authoring.proctoring.escalationemail.error.blank'], { proctoringProviderName: getProviderDisplayLabel(formValues.proctoringProvider) });
setFormStatus({ setFormStatus({
isValid: false, isValid: false,
errors: { errors: {
formProctortrackEscalationEmail: { formEscalationEmail: {
dialogErrorMessage: ( dialogErrorMessage: (
<Alert.Link onClick={setFocusToProctortrackEscalationEmailInput} href="#formProctortrackEscalationEmail" data-testid="proctorTrackEscalationEmailErrorLink"> <Alert.Link onClick={setFocusToEscalationEmailInput} href="#formEscalationEmail" data-testid="escalationEmailErrorLink">
{errorMessage} {errorMessage}
</Alert.Link> </Alert.Link>
), ),
@@ -168,8 +182,8 @@ const ProctoringSettings = ({ intl, onClose }) => {
setFormStatus({ setFormStatus({
isValid: false, isValid: false,
errors: { errors: {
formProctortrackEscalationEmail: { formEscalationEmail: {
dialogErrorMessage: (<Alert.Link onClick={setFocusToProctortrackEscalationEmailInput} href="#formProctortrackEscalationEmail" data-testid="proctorTrackEscalationEmailErrorLink">{errorMessage}</Alert.Link>), dialogErrorMessage: (<Alert.Link onClick={setFocusToEscalationEmailInput} href="#formEscalationEmail" data-testid="escalationEmailErrorLink">{errorMessage}</Alert.Link>),
inputErrorMessage: errorMessage, inputErrorMessage: errorMessage,
}, },
}, },
@@ -178,7 +192,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
} else { } else {
postSettingsBackToServer(); postSettingsBackToServer();
const errors = { ...formStatus.errors }; const errors = { ...formStatus.errors };
delete errors.formProctortrackEscalationEmail; delete errors.formEscalationEmail;
setFormStatus({ setFormStatus({
isValid: true, isValid: true,
errors, errors,
@@ -202,11 +216,6 @@ const ProctoringSettings = ({ intl, onClose }) => {
return markDisabled; return markDisabled;
} }
function getProviderDisplayLabel(provider) {
// if a display label exists for this provider return it
return ltiProctoringProviders.find(p => p.name === provider)?.verbose_name || provider;
}
function getProctoringProviderOptions(providers) { function getProctoringProviderOptions(providers) {
return providers.map(provider => ( return providers.map(provider => (
<option <option
@@ -247,16 +256,18 @@ const ProctoringSettings = ({ intl, onClose }) => {
); );
function renderContent() { function renderContent() {
const isLtiProviderSelected = isLtiProvider(formValues.proctoringProvider);
return ( return (
<> <>
{!formStatus.isValid && formStatus.errors.formProctortrackEscalationEmail {!formStatus.isValid && formStatus.errors.formEscalationEmail
&& ( && (
// tabIndex="-1" to make non-focusable element focusable // tabIndex="-1" to make non-focusable element focusable
<Alert <Alert
id="proctortrackEscalationEmailError" id="escalationEmailError"
variant="danger" variant="danger"
tabIndex="-1" tabIndex="-1"
data-testid="proctortrackEscalationEmailError" data-testid="escalationEmailError"
ref={alertRef} ref={alertRef}
> >
{getFormErrorMessage()} {getFormErrorMessage()}
@@ -319,30 +330,30 @@ const ProctoringSettings = ({ intl, onClose }) => {
</> </>
)} )}
{/* PROCTORTRACK ESCALATION EMAIL */} {/* ESCALATION EMAIL */}
{showProctortrackEscalationEmail && formValues.enableProctoredExams && ( {showEscalationEmail && formValues.enableProctoredExams && (
<Form.Group controlId="formProctortrackEscalationEmail"> <Form.Group controlId="formEscalationEmail">
<Form.Label className="font-weight-bold"> <Form.Label className="font-weight-bold">
{intl.formatMessage(messages['authoring.proctoring.escalationemail.label'])} {intl.formatMessage(messages['authoring.proctoring.escalationemail.label'])}
</Form.Label> </Form.Label>
<Form.Control <Form.Control
ref={proctoringEscalationEmailInputRef} ref={proctoringEscalationEmailInputRef}
type="email" type="email"
name="proctortrackEscalationEmail" name="escalationEmail"
data-testid="escalationEmail" data-testid="escalationEmail"
onChange={handleChange} onChange={handleChange}
value={formValues.proctortrackEscalationEmail} value={formValues.escalationEmail}
isInvalid={Object.prototype.hasOwnProperty.call(formStatus.errors, 'formProctortrackEscalationEmail')} isInvalid={Object.prototype.hasOwnProperty.call(formStatus.errors, 'formEscalationEmail')}
aria-describedby="proctortrackEscalationEmailHelpText" aria-describedby="escalationEmailHelpText"
/> />
<Form.Text id="proctortrackEscalationEmailHelpText"> <Form.Text id="escalationEmailHelpText">
{intl.formatMessage(messages['authoring.proctoring.escalationemail.help'])} {intl.formatMessage(messages['authoring.proctoring.escalationemail.help'])}
</Form.Text> </Form.Text>
{Object.prototype.hasOwnProperty.call(formStatus.errors, 'formProctortrackEscalationEmail') && ( {Object.prototype.hasOwnProperty.call(formStatus.errors, 'formEscalationEmail') && (
<Form.Control.Feedback type="invalid"> <Form.Control.Feedback type="invalid">
{ {
formStatus.errors.formProctortrackEscalationEmail formStatus.errors.formEscalationEmail
&& formStatus.errors.formProctortrackEscalationEmail.inputErrorMessage && formStatus.errors.formEscalationEmail.inputErrorMessage
} }
</Form.Control.Feedback> </Form.Control.Feedback>
)} )}
@@ -350,7 +361,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
)} )}
{/* ALLOW OPTING OUT OF PROCTORED EXAMS */} {/* ALLOW OPTING OUT OF PROCTORED EXAMS */}
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProvider(formValues.proctoringProvider) && ( { isEdxStaff && formValues.enableProctoredExams && !isLtiProviderSelected && (
<fieldset aria-describedby="allowOptingOutHelpText"> <fieldset aria-describedby="allowOptingOutHelpText">
<Form.Group controlId="formAllowingOptingOut"> <Form.Group controlId="formAllowingOptingOut">
<Form.Label as="legend" className="font-weight-bold"> <Form.Label as="legend" className="font-weight-bold">
@@ -374,7 +385,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
)} )}
{/* CREATE ZENDESK TICKETS */} {/* CREATE ZENDESK TICKETS */}
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProvider(formValues.proctoringProvider) && ( { isEdxStaff && formValues.enableProctoredExams && !isLtiProviderSelected && (
<fieldset aria-describedby="createZendeskTicketsText"> <fieldset aria-describedby="createZendeskTicketsText">
<Form.Group controlId="formCreateZendeskTickets"> <Form.Group controlId="formCreateZendeskTickets">
<Form.Label as="legend" className="font-weight-bold"> <Form.Label as="legend" className="font-weight-bold">
@@ -487,10 +498,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
setLoading(false); setLoading(false);
setSubmissionInProgress(false); setSubmissionInProgress(false);
setCourseStartDate(settingsResponse.data.course_start_date); setCourseStartDate(settingsResponse.data.course_start_date);
const isProctortrack = proctoredExamSettings.proctoring_provider === 'proctortrack';
setShowProctortrackEscalationEmail(isProctortrack);
setAvailableProctoringProviders(settingsResponse.data.available_proctoring_providers); setAvailableProctoringProviders(settingsResponse.data.available_proctoring_providers);
const proctoringEscalationEmail = proctoredExamSettings.proctoring_escalation_email;
// The list of providers returned by studio settings are the default behavior. If lti_external // The list of providers returned by studio settings are the default behavior. If lti_external
// is available as an option display the list of LTI providers returned by the exam service. // is available as an option display the list of LTI providers returned by the exam service.
@@ -517,6 +525,18 @@ const ProctoringSettings = ({ intl, onClose }) => {
} else { } else {
selectedProvider = proctoredExamSettings.proctoring_provider; selectedProvider = proctoredExamSettings.proctoring_provider;
} }
const isProctortrack = selectedProvider === 'proctortrack';
const ltiProviderSelected = proctoringProvidersLti.some(p => p.name === selectedProvider);
if (isProctortrack || ltiProviderSelected) {
setShowEscalationEmail(true);
}
const proctoringEscalationEmail = ltiProviderSelected
? examConfigResponse.data.escalation_email
: proctoredExamSettings.proctoring_escalation_email;
setFormValues({ setFormValues({
...formValues, ...formValues,
proctoringProvider: selectedProvider, proctoringProvider: selectedProvider,
@@ -526,7 +546,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
// The backend API may return null for the proctoringEscalationEmail value, which is the default. // The backend API may return null for the proctoringEscalationEmail value, which is the default.
// In order to keep our email input component controlled, we use the empty string as the default // In order to keep our email input component controlled, we use the empty string as the default
// and perform this conversion during GETs and POSTs. // and perform this conversion during GETs and POSTs.
proctortrackEscalationEmail: proctoringEscalationEmail === null ? '' : proctoringEscalationEmail, escalationEmail: proctoringEscalationEmail === null ? '' : proctoringEscalationEmail,
}); });
}, },
).catch( ).catch(

View File

@@ -196,7 +196,6 @@ describe('ProctoredExamSettings', () => {
await act(async () => { await act(async () => {
fireEvent.change(selectElement, { target: { value: 'test_lti' } }); fireEvent.change(selectElement, { target: { value: 'test_lti' } });
}); });
expect(screen.queryByTestId('escalationEmail')).toBeNull();
expect(screen.queryByTestId('allowOptingOutRadio')).toBeNull(); expect(screen.queryByTestId('allowOptingOutRadio')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull(); expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull(); expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull();
@@ -204,6 +203,8 @@ describe('ProctoredExamSettings', () => {
}); });
describe('Validation with invalid escalation email', () => { describe('Validation with invalid escalation email', () => {
const proctoringProvidersRequiringEscalationEmail = ['proctortrack', 'test_lti'];
beforeEach(async () => { beforeEach(async () => {
axiosMock.onGet( axiosMock.onGet(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
@@ -215,10 +216,14 @@ describe('ProctoredExamSettings', () => {
proctoring_escalation_email: 'test@example.com', proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true, create_zendesk_tickets: true,
}, },
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'], available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc', 'lti_external'],
course_start_date: '2070-01-01T00:00:00Z', course_start_date: '2070-01-01T00:00:00Z',
}); });
axiosMock.onPatch(
ExamsApiService.getExamConfigurationUrl(defaultProps.courseId),
).reply(204, {});
axiosMock.onPost( axiosMock.onPost(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, {}); ).reply(200, {});
@@ -226,175 +231,183 @@ describe('ProctoredExamSettings', () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />))); await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
}); });
it('Creates an alert when no proctoring escalation email is provided with proctortrack selected', async () => { proctoringProvidersRequiringEscalationEmail.forEach(provider => {
await waitFor(() => { it(`Creates an alert when no proctoring escalation email is provided with ${provider} selected`, async () => {
screen.getByDisplayValue('proctortrack'); await waitFor(() => {
}); screen.getByDisplayValue('proctortrack');
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com'); });
await act(async () => { const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } }); await act(async () => {
}); fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
const selectButton = screen.getByTestId('submissionButton'); });
await act(async () => { const selectButton = screen.getByTestId('submissionButton');
fireEvent.click(selectButton); await act(async () => {
fireEvent.click(selectButton);
});
// verify alert content and focus management
const escalationEmailError = screen.getByTestId('escalationEmailError');
expect(escalationEmailError.textContent).not.toBeNull();
expect(document.activeElement).toEqual(escalationEmailError);
// verify alert link links to offending input
const errorLink = screen.getByTestId('escalationEmailErrorLink');
await act(async () => {
fireEvent.click(errorLink);
});
const escalationEmailInput = screen.getByTestId('escalationEmail');
expect(document.activeElement).toEqual(escalationEmailInput);
}); });
// verify alert content and focus management it(`Creates an alert when invalid proctoring escalation email is provided with ${provider} selected`, async () => {
const escalationEmailError = screen.getByTestId('proctortrackEscalationEmailError'); await waitFor(() => {
expect(escalationEmailError.textContent).not.toBeNull(); screen.getByDisplayValue('proctortrack');
expect(document.activeElement).toEqual(escalationEmailError); });
// verify alert link links to offending input const selectElement = screen.getByDisplayValue('proctortrack');
const errorLink = screen.getByTestId('proctorTrackEscalationEmailErrorLink'); await act(async () => {
await act(async () => { fireEvent.change(selectElement, { target: { value: provider } });
fireEvent.click(errorLink); });
});
const escalationEmailInput = screen.getByTestId('escalationEmail');
expect(document.activeElement).toEqual(escalationEmailInput);
});
it('Creates an alert when invalid proctoring escalation email is provided with proctortrack selected', async () => { const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await waitFor(() => { await act(async () => {
screen.getByDisplayValue('proctortrack'); fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
}); });
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com'); const selectButton = screen.getByTestId('submissionButton');
await act(async () => { await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } }); fireEvent.click(selectButton);
}); });
const selectButton = screen.getByTestId('submissionButton');
await act(async () => { // verify alert content and focus management
fireEvent.click(selectButton); const escalationEmailError = screen.getByTestId('escalationEmailError');
expect(document.activeElement).toEqual(escalationEmailError);
expect(escalationEmailError.textContent).not.toBeNull();
expect(document.activeElement).toEqual(escalationEmailError);
// verify alert link links to offending input
const errorLink = screen.getByTestId('escalationEmailErrorLink');
await act(async () => {
fireEvent.click(errorLink);
});
const escalationEmailInput = screen.getByTestId('escalationEmail');
expect(document.activeElement).toEqual(escalationEmailInput);
}); });
// verify alert content and focus management it('Creates an alert when invalid proctoring escalation email is provided with proctoring disabled', async () => {
const escalationEmailError = screen.getByTestId('proctortrackEscalationEmailError'); await waitFor(() => {
expect(document.activeElement).toEqual(escalationEmailError); screen.getByDisplayValue('proctortrack');
expect(escalationEmailError.textContent).not.toBeNull(); });
expect(document.activeElement).toEqual(escalationEmailError); const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
});
const enableProctoringElement = screen.getByText('Proctored exams');
await act(async () => fireEvent.click(enableProctoringElement));
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
});
// verify alert link links to offending input // verify alert content and focus management
const errorLink = screen.getByTestId('proctorTrackEscalationEmailErrorLink'); const escalationEmailError = screen.getByTestId('escalationEmailError');
await act(async () => { expect(document.activeElement).toEqual(escalationEmailError);
fireEvent.click(errorLink); expect(escalationEmailError.textContent).not.toBeNull();
}); expect(document.activeElement).toEqual(escalationEmailError);
const escalationEmailInput = screen.getByTestId('escalationEmail');
expect(document.activeElement).toEqual(escalationEmailInput);
});
it('Creates an alert when invalid proctoring escalation email is provided with proctoring disabled', async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
});
const enableProctoringElement = screen.getByText('Proctored exams');
await act(async () => fireEvent.click(enableProctoringElement));
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
}); });
// verify alert content and focus management it('Has no error when empty proctoring escalation email is provided with proctoring disabled', async () => {
const escalationEmailError = screen.getByTestId('proctortrackEscalationEmailError'); await waitFor(() => {
expect(document.activeElement).toEqual(escalationEmailError); screen.getByDisplayValue('proctortrack');
expect(escalationEmailError.textContent).not.toBeNull(); });
expect(document.activeElement).toEqual(escalationEmailError); const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
}); await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
});
const enableProctoringElement = screen.getByText('Proctored exams');
await act(async () => fireEvent.click(enableProctoringElement));
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
});
it('Has no error when invalid proctoring escalation email is provided with proctoring disabled', async () => { // verify there is no escalation email alert, and focus has been set on save success alert
await waitFor(() => { expect(screen.queryByTestId('escalationEmailError')).toBeNull();
screen.getByDisplayValue('proctortrack');
}); const errorAlert = screen.getByTestId('saveSuccess');
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com'); expect(errorAlert.textContent).toEqual(
await act(async () => { expect.stringContaining('Proctored exam settings saved successfully.'),
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } }); );
}); expect(document.activeElement).toEqual(errorAlert);
const enableProctoringElement = screen.getByText('Proctored exams');
await act(async () => fireEvent.click(enableProctoringElement));
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
}); });
// verify there is no escalation email alert, and focus has been set on save success alert it(`Has no error when valid proctoring escalation email is provided with ${provider} selected`, async () => {
expect(screen.queryByTestId('proctortrackEscalationEmailError')).toBeNull(); await waitFor(() => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo@bar.com' } });
});
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
});
const errorAlert = screen.getByTestId('saveSuccess'); // verify there is no escalation email alert, and focus has been set on save success alert
expect(errorAlert.textContent).toEqual( expect(screen.queryByTestId('escalationEmailError')).toBeNull();
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
it('Has no error when valid proctoring escalation email is provided with proctortrack selected', async () => { const errorAlert = screen.getByTestId('saveSuccess');
await waitFor(() => { expect(errorAlert.textContent).toEqual(
screen.getByDisplayValue('proctortrack'); expect.stringContaining('Proctored exam settings saved successfully.'),
}); );
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com'); expect(document.activeElement).toEqual(errorAlert);
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo@bar.com' } });
});
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
}); });
// verify there is no escalation email alert, and focus has been set on save success alert it(`Escalation email field hidden when proctoring backend is not ${provider}`, async () => {
expect(screen.queryByTestId('proctortrackEscalationEmailError')).toBeNull(); await waitFor(() => {
screen.getByDisplayValue('proctortrack');
});
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
const selectEscalationEmailElement = screen.getByTestId('escalationEmail');
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
await act(async () => {
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
});
expect(screen.queryByTestId('escalationEmail')).toBeNull();
});
const errorAlert = screen.getByTestId('saveSuccess'); it(`Escalation email Field Show when proctoring backend is switched back to ${provider}`, async () => {
expect(errorAlert.textContent).toEqual( await waitFor(() => {
expect.stringContaining('Proctored exam settings saved successfully.'), screen.getByDisplayValue('proctortrack');
); });
expect(document.activeElement).toEqual(errorAlert); const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
}); let selectEscalationEmailElement = screen.getByTestId('escalationEmail');
await act(async () => {
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
});
expect(screen.queryByTestId('escalationEmail')).toBeNull();
await act(async () => {
fireEvent.change(proctoringBackendSelect, { target: { value: 'proctortrack' } });
});
expect(screen.queryByTestId('escalationEmail')).toBeDefined();
selectEscalationEmailElement = screen.getByTestId('escalationEmail');
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
});
it('Escalation email field hidden when proctoring backend is not Proctortrack', async () => { it('Submits form when "Enter" key is hit in the escalation email field', async () => {
await waitFor(() => { await waitFor(() => {
screen.getByDisplayValue('proctortrack'); screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
});
await act(async () => {
fireEvent.submit(selectEscalationEmailElement);
});
// if the error appears, the form has been submitted
expect(screen.getByTestId('escalationEmailError')).toBeDefined();
}); });
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
const selectEscalationEmailElement = screen.getByTestId('escalationEmail');
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
await act(async () => {
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
});
expect(screen.queryByTestId('escalationEmail')).toBeNull();
});
it('Escalation email Field Show when proctoring backend is switched back to Proctortrack', async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
});
const proctoringBackendSelect = screen.getByDisplayValue('proctortrack');
let selectEscalationEmailElement = screen.getByTestId('escalationEmail');
await act(async () => {
fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } });
});
expect(screen.queryByTestId('escalationEmail')).toBeNull();
await act(async () => {
fireEvent.change(proctoringBackendSelect, { target: { value: 'proctortrack' } });
});
expect(screen.queryByTestId('escalationEmail')).toBeDefined();
selectEscalationEmailElement = screen.getByTestId('escalationEmail');
expect(selectEscalationEmailElement.value).toEqual('test@example.com');
});
it('Submits form when "Enter" key is hit in the escalation email field', async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
});
await act(async () => {
fireEvent.submit(selectEscalationEmailElement);
});
// if the error appears, the form has been submitted
expect(screen.getByTestId('proctortrackEscalationEmailError')).toBeDefined();
}); });
}); });
@@ -687,11 +700,19 @@ describe('ProctoredExamSettings', () => {
it('Successfully updates exam configuration and studio provider is set to "lti_external" for lti providers', async () => { it('Successfully updates exam configuration and studio provider is set to "lti_external" for lti providers', async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />))); await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
// Make a change to the provider to proctortrack and set the email // Make a change to the provider to test_lti and set the email
const selectElement = screen.getByDisplayValue('mockproc'); const selectElement = screen.getByDisplayValue('mockproc');
await act(async () => { await act(async () => {
fireEvent.change(selectElement, { target: { value: 'test_lti' } }); fireEvent.change(selectElement, { target: { value: 'test_lti' } });
}); });
const escalationEmail = screen.getByTestId('escalationEmail');
expect(escalationEmail.value).toEqual('test@example.com');
await act(async () => {
fireEvent.change(escalationEmail, { target: { value: 'test_lti@example.com' } });
});
expect(escalationEmail.value).toEqual('test_lti@example.com');
const submitButton = screen.getByTestId('submissionButton'); const submitButton = screen.getByTestId('submissionButton');
await act(async () => { await act(async () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
@@ -701,6 +722,7 @@ describe('ProctoredExamSettings', () => {
expect(axiosMock.history.patch.length).toBe(1); expect(axiosMock.history.patch.length).toBe(1);
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({ expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
provider: 'test_lti', provider: 'test_lti',
escalation_email: 'test_lti@example.com',
}); });
// update studio settings // update studio settings
@@ -731,6 +753,7 @@ describe('ProctoredExamSettings', () => {
expect(axiosMock.history.patch.length).toBe(1); expect(axiosMock.history.patch.length).toBe(1);
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({ expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
provider: null, provider: null,
escalation_email: null,
}); });
expect(axiosMock.history.patch.length).toBe(1); expect(axiosMock.history.patch.length).toBe(1);
expect(axiosMock.history.post.length).toBe(1); expect(axiosMock.history.post.length).toBe(1);

View File

@@ -53,7 +53,7 @@ const messages = defineMessages({
}, },
'authoring.proctoring.escalationemail.label': { 'authoring.proctoring.escalationemail.label': {
id: 'authoring.proctoring.escalationemail.label', id: 'authoring.proctoring.escalationemail.label',
defaultMessage: 'Proctortrack escalation email', defaultMessage: 'Escalation email',
description: 'Label for escalation email text field', description: 'Label for escalation email text field',
}, },
'authoring.proctoring.escalationemail.help': { 'authoring.proctoring.escalationemail.help': {
@@ -63,12 +63,12 @@ const messages = defineMessages({
}, },
'authoring.proctoring.escalationemail.error.blank': { 'authoring.proctoring.escalationemail.error.blank': {
id: 'authoring.proctoring.escalationemail.error.blank', id: 'authoring.proctoring.escalationemail.error.blank',
defaultMessage: 'The Proctortrack Escalation Email field cannot be empty if proctortrack is the selected provider.', defaultMessage: 'The Escalation Email field cannot be empty if {proctoringProviderName} is the selected provider.',
description: 'Error message for missing required email field.', description: 'Error message for missing required email field.',
}, },
'authoring.proctoring.escalationemail.error.invalid': { 'authoring.proctoring.escalationemail.error.invalid': {
id: 'authoring.proctoring.escalationemail.error.invalid', id: 'authoring.proctoring.escalationemail.error.invalid',
defaultMessage: 'The Proctortrack Escalation Email field is in the wrong format and is not valid.', defaultMessage: 'The Escalation Email field is in the wrong format and is not valid.',
description: 'Error message for a invalid email format.', description: 'Error message for a invalid email format.',
}, },
'authoring.proctoring.allowoptout.label': { 'authoring.proctoring.allowoptout.label': {