feat: configure lti providers on exam settings modal (#363)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user