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 { getConfig } from '@edx/frontend-platform';
import { convertObjectToSnakeCase } from '../../utils';
class ExamsApiService {
static isAvailable() {
@@ -26,8 +27,9 @@ class ExamsApiService {
}
static saveCourseExamConfiguration(courseId, dataToSave) {
const snakecaseDataToSave = convertObjectToSnakeCase(dataToSave, true);
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 = {
enableProctoredExams: false,
proctoringProvider: false,
proctortrackEscalationEmail: '',
escalationEmail: '',
allowOptingOut: false,
createZendeskTickets: false,
};
@@ -44,7 +44,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
const [saveSuccess, setSaveSuccess] = useState(false);
const [saveError, setSaveError] = useState(false);
const [submissionInProgress, setSubmissionInProgress] = useState(false);
const [showProctortrackEscalationEmail, setShowProctortrackEscalationEmail] = useState(false);
const [showEscalationEmail, setShowEscalationEmail] = useState(false);
const isEdxStaff = getAuthenticatedUser().administrator;
const [formStatus, setFormStatus] = useState({
isValid: true,
@@ -53,6 +53,15 @@ const ProctoringSettings = ({ intl, onClose }) => {
const isMobile = useIsMobile();
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 appInfo = useModel('courseApps', 'proctoring');
const alertRef = React.createRef();
@@ -73,38 +82,36 @@ const ProctoringSettings = ({ intl, onClose }) => {
if (value === 'proctortrack') {
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 {
if (value === 'software_secure') {
setFormValues({ ...newFormValues, createZendeskTickets: true });
} else {
setFormValues(newFormValues);
}
setShowProctortrackEscalationEmail(false);
setFormValues(newFormValues);
setShowEscalationEmail(false);
}
} else {
setFormValues({ ...formValues, [name]: value });
}
};
function isLtiProvider(provider) {
return ltiProctoringProviders.some(p => p.name === provider);
}
const setFocusToProctortrackEscalationEmailInput = () => {
const setFocusToEscalationEmailInput = () => {
if (proctoringEscalationEmailInputRef && proctoringEscalationEmailInputRef.current) {
proctoringEscalationEmailInputRef.current.focus();
}
};
function postSettingsBackToServer() {
const providerIsLti = isLtiProvider(formValues.proctoringProvider);
const selectedProvider = formValues.proctoringProvider;
const isLtiProviderSelected = isLtiProvider(selectedProvider);
const studioDataToPostBack = {
proctored_exam_settings: {
enable_proctored_exams: formValues.enableProctoredExams,
// 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,
},
};
@@ -113,17 +120,23 @@ const ProctoringSettings = ({ intl, onClose }) => {
}
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
setSubmissionInProgress(true);
const saveOperations = [StudioApiService.saveProctoredExamSettingsData(courseId, studioDataToPostBack)];
if (allowLtiProviders && ExamsApiService.isAvailable()) {
const selectedEscalationEmail = formValues.escalationEmail;
saveOperations.push(
ExamsApiService.saveCourseExamConfiguration(
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) => {
event.preventDefault();
const isLtiProviderSelected = isLtiProvider(formValues.proctoringProvider);
if (
formValues.proctoringProvider === 'proctortrack'
&& !EmailValidator.validate(formValues.proctortrackEscalationEmail)
&& !(formValues.proctortrackEscalationEmail === '' && !formValues.enableProctoredExams)
(formValues.proctoringProvider === 'proctortrack' || isLtiProviderSelected)
&& !EmailValidator.validate(formValues.escalationEmail)
&& !(formValues.escalationEmail === '' && !formValues.enableProctoredExams)
) {
if (formValues.proctortrackEscalationEmail === '') {
const errorMessage = intl.formatMessage(messages['authoring.proctoring.escalationemail.error.blank']);
if (formValues.escalationEmail === '') {
const errorMessage = intl.formatMessage(messages['authoring.proctoring.escalationemail.error.blank'], { proctoringProviderName: getProviderDisplayLabel(formValues.proctoringProvider) });
setFormStatus({
isValid: false,
errors: {
formProctortrackEscalationEmail: {
formEscalationEmail: {
dialogErrorMessage: (
<Alert.Link onClick={setFocusToProctortrackEscalationEmailInput} href="#formProctortrackEscalationEmail" data-testid="proctorTrackEscalationEmailErrorLink">
<Alert.Link onClick={setFocusToEscalationEmailInput} href="#formEscalationEmail" data-testid="escalationEmailErrorLink">
{errorMessage}
</Alert.Link>
),
@@ -168,8 +182,8 @@ const ProctoringSettings = ({ intl, onClose }) => {
setFormStatus({
isValid: false,
errors: {
formProctortrackEscalationEmail: {
dialogErrorMessage: (<Alert.Link onClick={setFocusToProctortrackEscalationEmailInput} href="#formProctortrackEscalationEmail" data-testid="proctorTrackEscalationEmailErrorLink">{errorMessage}</Alert.Link>),
formEscalationEmail: {
dialogErrorMessage: (<Alert.Link onClick={setFocusToEscalationEmailInput} href="#formEscalationEmail" data-testid="escalationEmailErrorLink">{errorMessage}</Alert.Link>),
inputErrorMessage: errorMessage,
},
},
@@ -178,7 +192,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
} else {
postSettingsBackToServer();
const errors = { ...formStatus.errors };
delete errors.formProctortrackEscalationEmail;
delete errors.formEscalationEmail;
setFormStatus({
isValid: true,
errors,
@@ -202,11 +216,6 @@ const ProctoringSettings = ({ intl, onClose }) => {
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) {
return providers.map(provider => (
<option
@@ -247,16 +256,18 @@ const ProctoringSettings = ({ intl, onClose }) => {
);
function renderContent() {
const isLtiProviderSelected = isLtiProvider(formValues.proctoringProvider);
return (
<>
{!formStatus.isValid && formStatus.errors.formProctortrackEscalationEmail
{!formStatus.isValid && formStatus.errors.formEscalationEmail
&& (
// tabIndex="-1" to make non-focusable element focusable
<Alert
id="proctortrackEscalationEmailError"
id="escalationEmailError"
variant="danger"
tabIndex="-1"
data-testid="proctortrackEscalationEmailError"
data-testid="escalationEmailError"
ref={alertRef}
>
{getFormErrorMessage()}
@@ -319,30 +330,30 @@ const ProctoringSettings = ({ intl, onClose }) => {
</>
)}
{/* PROCTORTRACK ESCALATION EMAIL */}
{showProctortrackEscalationEmail && formValues.enableProctoredExams && (
<Form.Group controlId="formProctortrackEscalationEmail">
{/* ESCALATION EMAIL */}
{showEscalationEmail && formValues.enableProctoredExams && (
<Form.Group controlId="formEscalationEmail">
<Form.Label className="font-weight-bold">
{intl.formatMessage(messages['authoring.proctoring.escalationemail.label'])}
</Form.Label>
<Form.Control
ref={proctoringEscalationEmailInputRef}
type="email"
name="proctortrackEscalationEmail"
name="escalationEmail"
data-testid="escalationEmail"
onChange={handleChange}
value={formValues.proctortrackEscalationEmail}
isInvalid={Object.prototype.hasOwnProperty.call(formStatus.errors, 'formProctortrackEscalationEmail')}
aria-describedby="proctortrackEscalationEmailHelpText"
value={formValues.escalationEmail}
isInvalid={Object.prototype.hasOwnProperty.call(formStatus.errors, 'formEscalationEmail')}
aria-describedby="escalationEmailHelpText"
/>
<Form.Text id="proctortrackEscalationEmailHelpText">
<Form.Text id="escalationEmailHelpText">
{intl.formatMessage(messages['authoring.proctoring.escalationemail.help'])}
</Form.Text>
{Object.prototype.hasOwnProperty.call(formStatus.errors, 'formProctortrackEscalationEmail') && (
{Object.prototype.hasOwnProperty.call(formStatus.errors, 'formEscalationEmail') && (
<Form.Control.Feedback type="invalid">
{
formStatus.errors.formProctortrackEscalationEmail
&& formStatus.errors.formProctortrackEscalationEmail.inputErrorMessage
formStatus.errors.formEscalationEmail
&& formStatus.errors.formEscalationEmail.inputErrorMessage
}
</Form.Control.Feedback>
)}
@@ -350,7 +361,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
)}
{/* ALLOW OPTING OUT OF PROCTORED EXAMS */}
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProvider(formValues.proctoringProvider) && (
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProviderSelected && (
<fieldset aria-describedby="allowOptingOutHelpText">
<Form.Group controlId="formAllowingOptingOut">
<Form.Label as="legend" className="font-weight-bold">
@@ -374,7 +385,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
)}
{/* CREATE ZENDESK TICKETS */}
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProvider(formValues.proctoringProvider) && (
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProviderSelected && (
<fieldset aria-describedby="createZendeskTicketsText">
<Form.Group controlId="formCreateZendeskTickets">
<Form.Label as="legend" className="font-weight-bold">
@@ -487,10 +498,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
setLoading(false);
setSubmissionInProgress(false);
setCourseStartDate(settingsResponse.data.course_start_date);
const isProctortrack = proctoredExamSettings.proctoring_provider === 'proctortrack';
setShowProctortrackEscalationEmail(isProctortrack);
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
// 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 {
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({
...formValues,
proctoringProvider: selectedProvider,
@@ -526,7 +546,7 @@ const ProctoringSettings = ({ intl, onClose }) => {
// 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
// and perform this conversion during GETs and POSTs.
proctortrackEscalationEmail: proctoringEscalationEmail === null ? '' : proctoringEscalationEmail,
escalationEmail: proctoringEscalationEmail === null ? '' : proctoringEscalationEmail,
});
},
).catch(

View File

@@ -196,7 +196,6 @@ describe('ProctoredExamSettings', () => {
await act(async () => {
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
});
expect(screen.queryByTestId('escalationEmail')).toBeNull();
expect(screen.queryByTestId('allowOptingOutRadio')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull();
@@ -204,6 +203,8 @@ describe('ProctoredExamSettings', () => {
});
describe('Validation with invalid escalation email', () => {
const proctoringProvidersRequiringEscalationEmail = ['proctortrack', 'test_lti'];
beforeEach(async () => {
axiosMock.onGet(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
@@ -215,10 +216,14 @@ describe('ProctoredExamSettings', () => {
proctoring_escalation_email: 'test@example.com',
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',
});
axiosMock.onPatch(
ExamsApiService.getExamConfigurationUrl(defaultProps.courseId),
).reply(204, {});
axiosMock.onPost(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, {});
@@ -226,175 +231,183 @@ describe('ProctoredExamSettings', () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
});
it('Creates an alert when no proctoring escalation email is provided with proctortrack selected', async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
});
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
proctoringProvidersRequiringEscalationEmail.forEach(provider => {
it(`Creates an alert when no proctoring escalation email is provided with ${provider} selected`, async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: '' } });
});
const selectButton = screen.getByTestId('submissionButton');
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
const escalationEmailError = screen.getByTestId('proctortrackEscalationEmailError');
expect(escalationEmailError.textContent).not.toBeNull();
expect(document.activeElement).toEqual(escalationEmailError);
it(`Creates an alert when invalid proctoring escalation email is provided with ${provider} selected`, async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
});
// verify alert link links to offending input
const errorLink = screen.getByTestId('proctorTrackEscalationEmailErrorLink');
await act(async () => {
fireEvent.click(errorLink);
});
const escalationEmailInput = screen.getByTestId('escalationEmail');
expect(document.activeElement).toEqual(escalationEmailInput);
});
const selectElement = screen.getByDisplayValue('proctortrack');
await act(async () => {
fireEvent.change(selectElement, { target: { value: provider } });
});
it('Creates an alert when invalid proctoring escalation email is provided with proctortrack selected', async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
});
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
});
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com');
await act(async () => {
fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } });
});
const selectButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(selectButton);
});
// verify alert content and focus management
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
const escalationEmailError = screen.getByTestId('proctortrackEscalationEmailError');
expect(document.activeElement).toEqual(escalationEmailError);
expect(escalationEmailError.textContent).not.toBeNull();
expect(document.activeElement).toEqual(escalationEmailError);
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 link links to offending input
const errorLink = screen.getByTestId('proctorTrackEscalationEmailErrorLink');
await act(async () => {
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 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
const escalationEmailError = screen.getByTestId('escalationEmailError');
expect(document.activeElement).toEqual(escalationEmailError);
expect(escalationEmailError.textContent).not.toBeNull();
expect(document.activeElement).toEqual(escalationEmailError);
});
// verify alert content and focus management
const escalationEmailError = screen.getByTestId('proctortrackEscalationEmailError');
expect(document.activeElement).toEqual(escalationEmailError);
expect(escalationEmailError.textContent).not.toBeNull();
expect(document.activeElement).toEqual(escalationEmailError);
});
it('Has no error when empty 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: '' } });
});
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 () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
});
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);
// verify there is no escalation email alert, and focus has been set on save success alert
expect(screen.queryByTestId('escalationEmailError')).toBeNull();
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
// verify there is no escalation email alert, and focus has been set on save success alert
expect(screen.queryByTestId('proctortrackEscalationEmailError')).toBeNull();
it(`Has no error when valid proctoring escalation email is provided with ${provider} selected`, async () => {
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');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
// verify there is no escalation email alert, and focus has been set on save success alert
expect(screen.queryByTestId('escalationEmailError')).toBeNull();
it('Has no error when valid proctoring escalation email is provided with proctortrack selected', async () => {
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');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
// verify there is no escalation email alert, and focus has been set on save success alert
expect(screen.queryByTestId('proctortrackEscalationEmailError')).toBeNull();
it(`Escalation email field hidden when proctoring backend is not ${provider}`, async () => {
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');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
it(`Escalation email Field Show when proctoring backend is switched back to ${provider}`, 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('Escalation email field hidden when proctoring backend is not Proctortrack', async () => {
await waitFor(() => {
screen.getByDisplayValue('proctortrack');
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('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 () => {
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');
await act(async () => {
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');
await act(async () => {
fireEvent.click(submitButton);
@@ -701,6 +722,7 @@ describe('ProctoredExamSettings', () => {
expect(axiosMock.history.patch.length).toBe(1);
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
provider: 'test_lti',
escalation_email: 'test_lti@example.com',
});
// update studio settings
@@ -731,6 +753,7 @@ describe('ProctoredExamSettings', () => {
expect(axiosMock.history.patch.length).toBe(1);
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
provider: null,
escalation_email: null,
});
expect(axiosMock.history.patch.length).toBe(1);
expect(axiosMock.history.post.length).toBe(1);

View File

@@ -53,7 +53,7 @@ const messages = defineMessages({
},
'authoring.proctoring.escalationemail.label': {
id: 'authoring.proctoring.escalationemail.label',
defaultMessage: 'Proctortrack escalation email',
defaultMessage: 'Escalation email',
description: 'Label for escalation email text field',
},
'authoring.proctoring.escalationemail.help': {
@@ -63,12 +63,12 @@ const messages = defineMessages({
},
'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.',
},
'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.',
},
'authoring.proctoring.allowoptout.label': {