feat: configure lti providers on exam settings modal (#363)

This commit is contained in:
Zachary Hancock
2022-10-13 09:22:36 -04:00
committed by GitHub
parent 6f41a14012
commit d1d04d5585
2 changed files with 345 additions and 165 deletions

View File

@@ -13,6 +13,7 @@ import {
ActionRow, Alert, Badge, Form, Hyperlink, ModalDialog, StatefulButton,
} from '@edx/paragon';
import ExamsApiService from '../../data/services/ExamsApiService';
import StudioApiService from '../../data/services/StudioApiService';
import Loading from '../../generic/Loading';
import ConnectionErrorAlert from '../../generic/ConnectionErrorAlert';
@@ -36,7 +37,9 @@ function ProctoringSettings({ intl, onClose }) {
const [loaded, setLoaded] = useState(false);
const [loadingConnectionError, setLoadingConnectionError] = useState(false);
const [loadingPermissionError, setLoadingPermissionError] = useState(false);
const [allowLtiProviders, setAllowLtiProviders] = useState(false);
const [availableProctoringProviders, setAvailableProctoringProviders] = useState([]);
const [ltiProctoringProviders, setLtiProctoringProviders] = useState([]);
const [courseStartDate, setCourseStartDate] = useState('');
const [saveSuccess, setSaveSuccess] = useState(false);
const [saveError, setSaveError] = useState(false);
@@ -85,6 +88,10 @@ function ProctoringSettings({ intl, onClose }) {
}
}
function isLtiProvider(provider) {
return ltiProctoringProviders.some(p => p.name === provider);
}
function setFocusToProctortrackEscalationEmailInput() {
if (proctoringEscalationEmailInputRef && proctoringEscalationEmailInputRef.current) {
proctoringEscalationEmailInputRef.current.focus();
@@ -92,23 +99,35 @@ function ProctoringSettings({ intl, onClose }) {
}
function postSettingsBackToServer() {
const dataToPostBack = {
const providerIsLti = isLtiProvider(formValues.proctoringProvider);
const studioDataToPostBack = {
proctored_exam_settings: {
enable_proctored_exams: formValues.enableProctoredExams,
proctoring_provider: formValues.proctoringProvider,
// lti providers are managed outside edx-platform, lti_external indicates this
proctoring_provider: providerIsLti ? 'lti_external' : formValues.proctoringProvider,
create_zendesk_tickets: formValues.createZendeskTickets,
},
};
if (isEdxStaff) {
dataToPostBack.proctored_exam_settings.allow_proctoring_opt_out = formValues.allowOptingOut;
studioDataToPostBack.proctored_exam_settings.allow_proctoring_opt_out = formValues.allowOptingOut;
}
if (formValues.proctoringProvider === 'proctortrack') {
dataToPostBack.proctored_exam_settings.proctoring_escalation_email = formValues.proctortrackEscalationEmail === '' ? null : formValues.proctortrackEscalationEmail;
studioDataToPostBack.proctored_exam_settings.proctoring_escalation_email = formValues.proctortrackEscalationEmail === '' ? null : formValues.proctortrackEscalationEmail;
}
// only save back to exam service if necessary
setSubmissionInProgress(true);
StudioApiService.saveProctoredExamSettingsData(courseId, dataToPostBack).then(() => {
const saveOperations = [StudioApiService.saveProctoredExamSettingsData(courseId, studioDataToPostBack)];
if (allowLtiProviders && ExamsApiService.isAvailable()) {
saveOperations.push(
ExamsApiService.saveCourseExamConfiguration(
courseId, { provider: providerIsLti ? formValues.proctoringProvider : null },
),
);
}
Promise.all(saveOperations)
.then(() => {
setSaveSuccess(true);
setSaveError(false);
setSubmissionInProgress(false);
@@ -178,6 +197,11 @@ function 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
@@ -186,7 +210,7 @@ function ProctoringSettings({ intl, onClose }) {
disabled={isDisabledOption(provider)}
data-testid={provider}
>
{provider}
{getProviderDisplayLabel(provider)}
</option>
));
}
@@ -344,7 +368,7 @@ function ProctoringSettings({ intl, onClose }) {
)}
{/* CREATE ZENDESK TICKETS */}
{ isEdxStaff && formValues.enableProctoredExams && (
{ isEdxStaff && formValues.enableProctoredExams && !isLtiProvider(formValues.proctoringProvider) && (
<fieldset aria-describedby="createZendeskTicketsText">
<Form.Group controlId="formCreateZendeskTickets">
<Form.Label as="legend" className="font-weight-bold">
@@ -446,23 +470,48 @@ function ProctoringSettings({ intl, onClose }) {
useEffect(
() => {
StudioApiService.getProctoredExamSettingsData(courseId)
Promise.all([
StudioApiService.getProctoredExamSettingsData(courseId),
ExamsApiService.isAvailable() ? ExamsApiService.getCourseExamConfiguration(courseId) : Promise.resolve(),
ExamsApiService.isAvailable() ? ExamsApiService.getAvailableProviders() : Promise.resolve(),
])
.then(
response => {
const proctoredExamSettings = response.data.proctored_exam_settings;
([settingsResponse, examConfigResponse, ltiProvidersResponse]) => {
const proctoredExamSettings = settingsResponse.data.proctored_exam_settings;
setLoaded(true);
setLoading(false);
setSubmissionInProgress(false);
setCourseStartDate(response.data.course_start_date);
setCourseStartDate(settingsResponse.data.course_start_date);
const isProctortrack = proctoredExamSettings.proctoring_provider === 'proctortrack';
setShowProctortrackEscalationEmail(isProctortrack);
setAvailableProctoringProviders(response.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
// is available as an option display the list of LTI providers returned by the exam service.
// Setting 'lti_external' in studio indicates an LTI provider configured outside of edx-platform.
// This option is not directly selectable.
const proctoringProvidersStudio = settingsResponse.data.available_proctoring_providers;
const proctoringProvidersLti = ltiProvidersResponse?.data || [];
setAllowLtiProviders(proctoringProvidersStudio.includes('lti_external'));
setLtiProctoringProviders(proctoringProvidersLti);
// flatten provider objects and coalesce values to just the provider key
setAvailableProctoringProviders(
proctoringProvidersLti.reduce((result, provider) => [...result, provider.name], []).concat(
proctoringProvidersStudio.filter(value => value !== 'lti_external'),
),
);
let selectedProvider;
if (proctoredExamSettings.proctoring_provider === 'lti_external') {
selectedProvider = examConfigResponse.data.provider;
} else {
selectedProvider = proctoredExamSettings.proctoring_provider;
}
setFormValues({
...formValues,
proctoringProvider: selectedProvider,
enableProctoredExams: proctoredExamSettings.enable_proctored_exams,
proctoringProvider: proctoredExamSettings.proctoring_provider,
allowOptingOut: proctoredExamSettings.allow_proctoring_opt_out,
createZendeskTickets: proctoredExamSettings.create_zendesk_tickets,
// The backend API may return null for the proctoringEscalationEmail value, which is the default.
@@ -473,7 +522,7 @@ function ProctoringSettings({ intl, onClose }) {
},
).catch(
error => {
if (error.response.status === 403) {
if (error.response?.status === 403) {
setLoadingPermissionError(true);
} else {
setLoadingConnectionError(true);

View File

@@ -4,12 +4,13 @@ import {
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { initializeMockApp } from '@edx/frontend-platform';
import { initializeMockApp, mergeConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import StudioApiService from '../../data/services/StudioApiService';
import ExamsApiService from '../../data/services/ExamsApiService';
import initializeStore from '../../store';
import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
import ProctoredExamSettings from './Settings';
@@ -33,7 +34,19 @@ const intlWrapper = children => (
let axiosMock;
describe('ProctoredExamSettings', () => {
beforeEach(() => {
function setupApp(isAdmin = true) {
mergeConfig({
EXAMS_BASE_URL: 'http://exams.testing.co',
}, 'CourseAuthoringConfig');
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: isAdmin,
roles: [],
},
});
store = initializeStore({
models: {
courseApps: {
@@ -41,39 +54,47 @@ describe('ProctoredExamSettings', () => {
},
},
});
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers`,
).reply(200, [
{
name: 'test_lti',
verbose_name: 'LTI Provider',
},
]);
axiosMock.onGet(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
).reply(200, {
provider: null,
});
axiosMock.onGet(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, {
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc', 'lti_external'],
course_start_date: '2070-01-01T00:00:00Z',
});
}
afterEach(() => {
cleanup();
axiosMock.reset();
});
beforeEach(async () => {
setupApp();
});
describe('Field dependencies', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, {
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'],
course_start_date: '2070-01-01T00:00:00Z',
});
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
});
@@ -166,21 +187,23 @@ describe('ProctoredExamSettings', () => {
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull();
});
it('Hides unsupported fields when lti provider is selected', async () => {
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
const selectElement = screen.getByDisplayValue('mockproc');
await act(async () => {
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
});
expect(screen.queryByTestId('escalationEmail')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsNo')).toBeNull();
});
});
describe('Validation with invalid escalation email', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: false,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, {
@@ -399,83 +422,114 @@ describe('ProctoredExamSettings', () => {
course_start_date: '2013-01-01T00:00:00Z',
};
function setup(data, isAdmin) {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: isAdmin,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
function mockCourseData(data) {
axiosMock.onGet(StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId)).reply(200, data);
}
it('Disables irrelevant proctoring provider fields when user is not an administrator and it is after start date', async () => {
setup(mockGetPastCourseData, false);
const isAdmin = false;
setupApp(isAdmin);
mockCourseData(mockGetPastCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('proctortrack');
expect(providerOption.hasAttribute('disabled')).toEqual(true);
});
it('Enables all proctoring provider options if user is not an administrator and it is before start date', async () => {
setup(mockGetFutureCourseData, false);
const isAdmin = false;
setupApp(isAdmin);
mockCourseData(mockGetFutureCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('proctortrack');
expect(providerOption.hasAttribute('disabled')).toEqual(false);
});
it('Enables all proctoring provider options if user administrator and it is after start date', async () => {
setup(mockGetPastCourseData, true);
const isAdmin = true;
setupApp(isAdmin);
mockCourseData(mockGetPastCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('proctortrack');
expect(providerOption.hasAttribute('disabled')).toEqual(false);
});
it('Enables all proctoring provider options if user administrator and it is before start date', async () => {
setup(mockGetFutureCourseData, true);
const isAdmin = true;
setupApp(isAdmin);
mockCourseData(mockGetFutureCourseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const providerOption = screen.getByTestId('proctortrack');
expect(providerOption.hasAttribute('disabled')).toEqual(false);
});
it('Does not include lti_external as a selectable option', async () => {
const courseData = mockGetFutureCourseData;
courseData.available_proctoring_providers = ['lti_external', 'proctortrack', 'mockproc'];
mockCourseData(courseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
expect(screen.queryByTestId('lti_external')).toBeNull();
});
it('Includes lti proctoring provider options when lti_external is allowed by studio', async () => {
const courseData = mockGetFutureCourseData;
courseData.available_proctoring_providers = ['lti_external', 'proctortrack', 'mockproc'];
mockCourseData(courseData);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
const providerOption = screen.getByTestId('test_lti');
// as as admin the provider should not be disabled
expect(providerOption.hasAttribute('disabled')).toEqual(false);
});
it('Does not request lti provider options if there is no exam service url configuration', async () => {
mergeConfig({
EXAMS_BASE_URL: null,
}, 'CourseAuthoringConfig');
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await waitFor(() => {
screen.getByDisplayValue('mockproc');
});
// only outgoing request should be for studio settings
expect(axiosMock.history.get.length).toBe(1);
expect(axiosMock.history.get[0].url.includes('proctored_exam_settings')).toEqual(true);
});
it('Selected LTI proctoring provider is shown on page load', async () => {
const courseData = { ...mockGetFutureCourseData };
courseData.available_proctoring_providers = ['lti_external', 'proctortrack', 'mockproc'];
courseData.proctored_exam_settings.proctoring_provider = 'lti_external';
mockCourseData(courseData);
axiosMock.onGet(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
).reply(200, {
provider: 'test_lti',
});
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
await waitFor(() => {
screen.getByText('Proctoring provider');
});
// make sure test_lti is the selected provider
expect(screen.getByDisplayValue('LTI Provider')).toBeInTheDocument();
});
});
describe('Toggles field visibility based on user permissions', () => {
function setup(isAdmin) {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: isAdmin,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId)).reply(200, {
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'],
course_start_date: '2070-01-01T00:00:00Z',
});
}
it('Hides opting out and zendesk tickets for non edX staff', async () => {
setup(false);
setupApp(false);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
expect(screen.queryByTestId('allowOptingOutYes')).toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).toBeNull();
});
it('Shows opting out and zendesk tickets for edX staff', async () => {
setup(true);
setupApp(true);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
expect(screen.queryByTestId('allowOptingOutYes')).not.toBeNull();
expect(screen.queryByTestId('createZendeskTicketsYes')).not.toBeNull();
@@ -483,18 +537,6 @@ describe('ProctoredExamSettings', () => {
});
describe('Connection states', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('Shows the spinner before the connection is complete', async () => {
await act(async () => {
render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />));
@@ -504,7 +546,7 @@ describe('ProctoredExamSettings', () => {
});
});
it('Show connection error message when we suffer server side error', async () => {
it('Show connection error message when we suffer studio server side error', async () => {
axiosMock.onGet(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(500);
@@ -516,6 +558,18 @@ describe('ProctoredExamSettings', () => {
);
});
it('Show connection error message when we suffer edx-exams server side error', async () => {
axiosMock.onGet(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers`,
).reply(500);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const connectionError = screen.getByTestId('connectionErrorAlert');
expect(connectionError.textContent).toEqual(
expect.stringContaining('We encountered a technical error when loading this page.'),
);
});
it('Show permission error message when user do not have enough permission', async () => {
axiosMock.onGet(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
@@ -529,35 +583,17 @@ describe('ProctoredExamSettings', () => {
});
});
describe('Save settings', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient(), { onNoMatch: 'throwException' });
axiosMock.onGet(StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId)).reply(200, {
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'],
});
});
it('Disable button while submitting', async () => {
describe.only('Save settings', () => {
beforeEach(async () => {
axiosMock.onPost(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, 'success');
axiosMock.onPatch(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
).reply(200, 'success');
});
it('Disable button while submitting', async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
let submitButton = screen.getByTestId('submissionButton');
expect(screen.queryByTestId('saveInProgress')).toBeFalsy();
@@ -570,10 +606,6 @@ describe('ProctoredExamSettings', () => {
});
it('Makes API call successfully with proctoring_escalation_email if proctortrack', async () => {
axiosMock.onPost(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, 'success');
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
// Make a change to the provider to proctortrack and set the email
const selectElement = screen.getByDisplayValue('mockproc');
@@ -609,10 +641,6 @@ describe('ProctoredExamSettings', () => {
});
it('Makes API call successfully without proctoring_escalation_email if not proctortrack', async () => {
axiosMock.onPost(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, 'success');
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
// make sure we have not selected proctortrack as the proctoring provider
@@ -639,7 +667,112 @@ describe('ProctoredExamSettings', () => {
expect(document.activeElement).toEqual(errorAlert);
});
it('Makes API call generated error', 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} />)));
// Make a change to the provider to proctortrack and set the email
const selectElement = screen.getByDisplayValue('mockproc');
await act(async () => {
fireEvent.change(selectElement, { target: { value: 'test_lti' } });
});
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
// update exam service config
expect(axiosMock.history.patch.length).toBe(1);
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
provider: 'test_lti',
});
// update studio settings
expect(axiosMock.history.post.length).toBe(1);
expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'lti_external',
create_zendesk_tickets: true,
},
});
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
it('Sets exam service provider to null if a non-lti provider is selected', async () => {
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
// update exam service config
expect(axiosMock.history.patch.length).toBe(1);
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
provider: null,
});
expect(axiosMock.history.patch.length).toBe(1);
expect(axiosMock.history.post.length).toBe(1);
expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
create_zendesk_tickets: true,
},
});
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
it('Does not update exam service if lti is not enabled in studio', async () => {
axiosMock.onGet(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, {
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'],
course_start_date: '2070-01-01T00:00:00Z',
});
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
// does not update exam service config
expect(axiosMock.history.patch.length).toBe(0);
// does update studio
expect(axiosMock.history.post.length).toBe(1);
expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
create_zendesk_tickets: true,
},
});
const errorAlert = screen.getByTestId('saveSuccess');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('Proctored exam settings saved successfully.'),
);
expect(document.activeElement).toEqual(errorAlert);
});
it('Makes studio API call generated error', async () => {
axiosMock.onPost(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(500);
@@ -657,11 +790,29 @@ describe('ProctoredExamSettings', () => {
expect(document.activeElement).toEqual(errorAlert);
});
it('Makes exams API call generated error', async () => {
axiosMock.onPatch(
`${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`,
).reply(500, 'error');
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
await act(async () => {
fireEvent.click(submitButton);
});
expect(axiosMock.history.post.length).toBe(1);
const errorAlert = screen.getByTestId('saveError');
expect(errorAlert.textContent).toEqual(
expect.stringContaining('We encountered a technical error while trying to save proctored exam settings'),
);
expect(document.activeElement).toEqual(errorAlert);
});
it('Manages focus correctly after different save statuses', async () => {
// first make a call that will cause a save error
axiosMock.onPost(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).replyOnce(500);
).reply(500);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
const submitButton = screen.getByTestId('submissionButton');
@@ -678,7 +829,7 @@ describe('ProctoredExamSettings', () => {
// now make a call that will allow for a successful save
axiosMock.onPost(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).replyOnce(200, 'success');
).reply(200, 'success');
await act(async () => {
fireEvent.click(submitButton);
});
@@ -693,28 +844,8 @@ describe('ProctoredExamSettings', () => {
it('Include Zendesk ticket in post request if user is not an admin', async () => {
// use non-admin user for test
initializeMockApp({
authenticatedUser: {
userId: 4,
username: 'abc1234',
administrator: false,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient(), { onNoMatch: 'throwException' });
axiosMock.onGet(StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId)).reply(200, {
proctored_exam_settings: {
enable_proctored_exams: true,
allow_proctoring_opt_out: false,
proctoring_provider: 'mockproc',
proctoring_escalation_email: 'test@example.com',
create_zendesk_tickets: true,
},
available_proctoring_providers: ['software_secure', 'proctortrack', 'mockproc'],
});
axiosMock.onPost(
StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId),
).reply(200, 'success');
const isAdmin = false;
setupApp(isAdmin);
await act(async () => render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)));
// Make a change to the proctoring provider