import { render, screen, cleanup, waitFor, fireEvent, act, initializeMocks, } from 'CourseAuthoring/testUtils'; import { mergeConfig } from '@edx/frontend-platform'; import StudioApiService from 'CourseAuthoring/data/services/StudioApiService'; import ExamsApiService from 'CourseAuthoring/data/services/ExamsApiService'; import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; import { CourseAuthoringProvider } from 'CourseAuthoring/CourseAuthoringContext'; import { getCourseDetailsUrl } from 'CourseAuthoring/data/api'; import ProctoredExamSettings from './Settings'; const courseId = 'course-v1%3AedX%2BDemoX%2BDemo_Course'; const defaultProps = { courseId, onClose: () => {}, }; const renderComponent = children => ( {children} ); let axiosMock; describe('ProctoredExamSettings', () => { /** * @param {boolean} isAdmin * @param {string | undefined} org */ function setupApp(isAdmin = true, org = undefined) { mergeConfig({ EXAMS_BASE_URL: 'http://exams.testing.co', }, 'CourseAuthoringConfig'); const user = { userId: 3, username: 'abc123', administrator: isAdmin, roles: [], }; const mocks = initializeMocks({ user, initialState: { models: { courseApps: { proctoring: {}, }, }, }, }); axiosMock = mocks.axiosMock; axiosMock .onGet(getCourseDetailsUrl(courseId, user.username)) .reply(200, { courseId, name: 'Course Test', start: Date(), ...(org ? { org } : {}), }); axiosMock.onGet(`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers`) .reply(200, [{ name: 'test_lti', verbose_name: 'LTI Provider' }]); if (org) { axiosMock.onGet(`${ExamsApiService.getExamsBaseUrl()}/api/v1/providers?org=${org}`) .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', }, available_proctoring_providers: ['software_secure', 'mockproc', 'lti_external'], requires_escalation_email_providers: ['test_lti'], course_start_date: '2070-01-01T00:00:00Z', }); } beforeEach(async () => { setupApp(); }); describe('Field dependencies', () => { beforeEach(async () => { await act(async () => render(renderComponent())); }); it('Hides all other fields when enabledProctorExam is false when first loaded', async () => { cleanup(); // Overrides the handler defined in beforeEach. axiosMock.onGet( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), ).reply(200, { proctored_exam_settings: { enable_proctored_exams: false, allow_proctoring_opt_out: false, proctoring_provider: 'mockproc', proctoring_escalation_email: 'test@example.com', }, available_proctoring_providers: ['software_secure', 'mockproc'], requires_escalation_email_providers: [], course_start_date: '2070-01-01T00:00:00Z', }); await act(async () => render(renderComponent())); await waitFor(() => { screen.getByText('Proctored exams'); }); const enabledProctoredExamCheck = screen.getByLabelText('Proctored exams'); expect(enabledProctoredExamCheck.checked).toEqual(false); expect(screen.queryByText('Allow Opting Out of Proctored Exams')).toBeNull(); expect(screen.queryByDisplayValue('mockproc')).toBeNull(); expect(screen.queryByTestId('escalationEmail')).toBeNull(); }); it('Hides all other fields when enableProctoredExams toggled to false', async () => { await waitFor(() => { screen.getByText('Proctored exams'); }); expect(screen.queryByText('Allow opting out of proctored exams')).toBeDefined(); expect(screen.queryByDisplayValue('mockproc')).toBeDefined(); expect(screen.queryByTestId('escalationEmail')).toBeDefined(); let enabledProctoredExamCheck = screen.getAllByLabelText('Proctored exams', { exact: false })[0]; expect(enabledProctoredExamCheck.checked).toEqual(true); fireEvent.click(enabledProctoredExamCheck, { target: { value: false } }); enabledProctoredExamCheck = screen.getByLabelText('Proctored exams'); expect(enabledProctoredExamCheck.checked).toEqual(false); expect(screen.queryByText('Allow opting out of proctored exams')).toBeNull(); expect(screen.queryByDisplayValue('mockproc')).toBeNull(); expect(screen.queryByTestId('escalationEmail')).toBeNull(); }); it('Hides unsupported fields when lti provider is selected', async () => { await waitFor(() => { screen.getByDisplayValue('mockproc'); }); const selectElement = screen.getByDisplayValue('mockproc'); fireEvent.change(selectElement, { target: { value: 'test_lti' } }); expect(screen.queryByTestId('allowOptingOutRadio')).toBeNull(); }); }); describe('Validation with invalid escalation email', () => { const proctoringProvidersRequiringEscalationEmail = ['test_lti']; beforeEach(async () => { axiosMock.onGet( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), ).reply(200, { proctored_exam_settings: { enable_proctored_exams: true, allow_proctoring_opt_out: false, proctoring_provider: 'lti_external', proctoring_escalation_email: 'test@example.com', }, available_proctoring_providers: ['software_secure', 'mockproc', 'lti_external'], requires_escalation_email_providers: ['test_lti'], course_start_date: '2070-01-01T00:00:00Z', }); axiosMock.onGet( `${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`, ).reply(200, { provider: 'test_lti', escalation_email: 'test@example.com', }); axiosMock.onPatch( ExamsApiService.getExamConfigurationUrl(defaultProps.courseId), ).reply(204, {}); axiosMock.onPost( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), ).reply(200, {}); await act(async () => render(renderComponent())); }); proctoringProvidersRequiringEscalationEmail.forEach(provider => { it(`Creates an alert when no proctoring escalation email is provided with ${provider} selected`, async () => { await waitFor(() => { screen.getByDisplayValue('LTI Provider'); }); const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com'); fireEvent.change(selectEscalationEmailElement, { target: { value: '' } }); const selectButton = screen.getByTestId('submissionButton'); 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'); 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 ${provider} selected`, async () => { await waitFor(() => { screen.getByDisplayValue('LTI Provider'); }); const selectElement = screen.getByDisplayValue('LTI Provider'); fireEvent.change(selectElement, { target: { value: provider } }); const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com'); fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } }); const proctoringForm = screen.getByTestId('proctoringForm'); fireEvent.submit(proctoringForm); // 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'); 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('LTI Provider'); }); const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com'); fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo.bar' } }); const enableProctoringElement = screen.getByText('Proctored exams'); fireEvent.click(enableProctoringElement); const selectButton = screen.getByTestId('submissionButton'); 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); }); it('Has no error when empty proctoring escalation email is provided with proctoring disabled', async () => { await waitFor(() => { screen.getByDisplayValue('LTI Provider'); }); const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com'); fireEvent.change(selectEscalationEmailElement, { target: { value: '' } }); const enableProctoringElement = screen.getByText('Proctored exams'); fireEvent.click(enableProctoringElement); const selectButton = screen.getByTestId('submissionButton'); fireEvent.click(selectButton); // verify there is no escalation email alert, and focus has been set on save success alert expect(screen.queryByTestId('escalationEmailError')).toBeNull(); await waitFor(() => { const errorAlert = screen.getByTestId('saveSuccess'); expect(errorAlert.textContent).toEqual( expect.stringContaining('Proctored exam settings saved successfully.'), ); expect(document.activeElement).toEqual(errorAlert); }); }); it(`Has no error when valid proctoring escalation email is provided with ${provider} selected`, async () => { await waitFor(() => { screen.getByDisplayValue('LTI Provider'); }); const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com'); fireEvent.change(selectEscalationEmailElement, { target: { value: 'foo@bar.com' } }); const selectButton = screen.getByTestId('submissionButton'); fireEvent.click(selectButton); // verify there is no escalation email alert, and focus has been set on save success alert expect(screen.queryByTestId('escalationEmailError')).toBeNull(); await waitFor(() => { 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 hidden when proctoring backend is not ${provider}`, async () => { await waitFor(() => { screen.getByDisplayValue('LTI Provider'); }); const proctoringBackendSelect = screen.getByDisplayValue('LTI Provider'); const selectEscalationEmailElement = screen.getByTestId('escalationEmail'); expect(selectEscalationEmailElement.value).toEqual('test@example.com'); fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } }); expect(screen.queryByTestId('escalationEmail')).toBeNull(); }); it(`Escalation email Field Show when proctoring backend is switched back to ${provider}`, async () => { await waitFor(() => { screen.getByDisplayValue('LTI Provider'); }); const proctoringBackendSelect = screen.getByDisplayValue('LTI Provider'); let selectEscalationEmailElement = screen.getByTestId('escalationEmail'); fireEvent.change(proctoringBackendSelect, { target: { value: 'software_secure' } }); expect(screen.queryByTestId('escalationEmail')).toBeNull(); fireEvent.change(proctoringBackendSelect, { target: { value: provider } }); 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('LTI Provider'); }); const selectEscalationEmailElement = screen.getByDisplayValue('test@example.com'); fireEvent.change(selectEscalationEmailElement, { target: { value: '' } }); fireEvent.submit(selectEscalationEmailElement); // if the error appears, the form has been submitted expect(screen.getByTestId('escalationEmailError')).toBeDefined(); }); }); }); describe('Proctoring provider options', () => { const mockGetFutureCourseData = { proctored_exam_settings: { enable_proctored_exams: true, allow_proctoring_opt_out: false, proctoring_provider: 'mockproc', proctoring_escalation_email: 'test@example.com', }, available_proctoring_providers: ['software_secure', 'mockproc'], requires_escalation_email_providers: [], course_start_date: '2099-01-01T00:00:00Z', }; const mockGetPastCourseData = { proctored_exam_settings: { enable_proctored_exams: true, allow_proctoring_opt_out: false, proctoring_provider: 'mockproc', proctoring_escalation_email: 'test@example.com', }, available_proctoring_providers: ['software_secure', 'mockproc'], requires_escalation_email_providers: [], course_start_date: '2013-01-01T00:00:00Z', }; 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 () => { const isAdmin = false; setupApp(isAdmin); mockCourseData(mockGetPastCourseData); await act(async () => render(renderComponent())); const providerOption = screen.getByTestId('software_secure'); 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 () => { const isAdmin = false; setupApp(isAdmin); mockCourseData(mockGetFutureCourseData); await act(async () => render(renderComponent())); const providerOption = screen.getByTestId('software_secure'); expect(providerOption.hasAttribute('disabled')).toEqual(false); }); it('Sends the org to the proctoring provider endpoint', async () => { const isAdmin = false; const org = 'test-org'; setupApp(isAdmin, org); mockCourseData(mockGetFutureCourseData); await act(async () => render(renderComponent())); const providerOption = screen.getByTestId('software_secure'); expect(providerOption.hasAttribute('disabled')).toEqual(false); }); it('Enables all proctoring provider options if user administrator and it is after start date', async () => { const isAdmin = true; setupApp(isAdmin); mockCourseData(mockGetPastCourseData); await act(async () => render(renderComponent())); const providerOption = screen.getByTestId('software_secure'); expect(providerOption.hasAttribute('disabled')).toEqual(false); }); it('Enables all proctoring provider options if user administrator and it is before start date', async () => { const isAdmin = true; setupApp(isAdmin); mockCourseData(mockGetFutureCourseData); await act(async () => render(renderComponent())); const providerOption = screen.getByTestId('software_secure'); expect(providerOption.hasAttribute('disabled')).toEqual(false); }); it('Does not include lti_external as a selectable option', async () => { const courseData = { ...mockGetFutureCourseData, available_proctoring_providers: ['lti_external', 'mockproc'], }; mockCourseData(courseData); await act(async () => render(renderComponent())); 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, available_proctoring_providers: ['lti_external', 'mockproc'], }; mockCourseData(courseData); await act(async () => render(renderComponent())); 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 include lti provider options when lti_external is not available in studio', async () => { const isAdmin = true; setupApp(isAdmin); mockCourseData(mockGetFutureCourseData); await act(async () => render(renderComponent())); await waitFor(() => { screen.getByDisplayValue('mockproc'); }); const providerOption = screen.queryByTestId('test_lti'); expect(providerOption).not.toBeInTheDocument(); }); 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(renderComponent())); await waitFor(() => { screen.getByDisplayValue('mockproc'); }); // (1) for studio settings // (2) waffle flags // (3) for course details expect(axiosMock.history.get.length).toBe(3); 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', '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(renderComponent())); 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', () => { it('Hides opting out for non edX staff', async () => { setupApp(false); await act(async () => render(renderComponent())); expect(screen.queryByTestId('allowOptingOutYes')).toBeNull(); }); it('Shows opting out for edX staff', async () => { setupApp(true); await act(async () => render(renderComponent())); expect(screen.queryByTestId('allowOptingOutYes')).not.toBeNull(); }); }); describe('Connection states', () => { it('Shows the spinner before the connection is complete', async () => { render(renderComponent()); const spinner = await screen.findByRole('status'); expect(spinner.textContent).toEqual('Loading...'); }); it('Show connection error message when we suffer studio server side error', async () => { axiosMock.onGet( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), ).reply(500); await act(async () => render(renderComponent())); const connectionError = screen.getByTestId('connectionErrorAlert'); expect(connectionError.textContent).toEqual( expect.stringContaining('We encountered a technical error when loading this page.'), ); }); 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(renderComponent())); 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), ).reply(403); await act(async () => render(renderComponent())); const permissionError = screen.getByTestId('permissionDeniedAlert'); expect(permissionError.textContent).toEqual( expect.stringContaining('You are not authorized to view this page'), ); }); }); describe('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(renderComponent())); let submitButton = screen.getByTestId('submissionButton'); expect(screen.queryByTestId('saveInProgress')).toBeFalsy(); fireEvent.click(submitButton); submitButton = screen.getByTestId('submissionButton'); expect(submitButton).toHaveAttribute('disabled'); }); it('Makes API call successfully with proctoring_escalation_email if test_lti', async () => { // Setup mock to include test_lti as available provider 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', }, available_proctoring_providers: ['software_secure', 'mockproc', 'lti_external'], requires_escalation_email_providers: ['test_lti'], course_start_date: '2070-01-01T00:00:00Z', }); await act(async () => render(renderComponent())); // Make a change to the provider to test_lti and set the email const selectElement = screen.getByDisplayValue('mockproc'); fireEvent.change(selectElement, { target: { value: 'test_lti' } }); const escalationEmail = screen.getByTestId('escalationEmail'); expect(escalationEmail.value).toEqual('test@example.com'); fireEvent.change(escalationEmail, { target: { value: 'test_lti@example.com' } }); expect(escalationEmail.value).toEqual('test_lti@example.com'); const submitButton = screen.getByTestId('submissionButton'); fireEvent.click(submitButton); 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', proctoring_escalation_email: 'test_lti@example.com', }, }); 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', }); await waitFor(() => { const errorAlert = screen.getByTestId('saveSuccess'); expect(errorAlert.textContent).toEqual( expect.stringContaining('Proctored exam settings saved successfully.'), ); expect(document.activeElement).toEqual(errorAlert); }); }); it('Makes API call successfully without proctoring_escalation_email if not requiring escalation email', async () => { await act(async () => render(renderComponent())); // make sure we have not selected a provider requiring escalation email expect(screen.getByDisplayValue('mockproc')).toBeDefined(); const submitButton = screen.getByTestId('submissionButton'); fireEvent.click(submitButton); 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', }, }); await waitFor(() => { const errorAlert = screen.getByTestId('saveSuccess'); expect(errorAlert.textContent).toEqual( expect.stringContaining('Proctored exam settings saved successfully.'), ); expect(document.activeElement).toEqual(errorAlert); }); }); it('Successfully updates exam configuration and studio provider is set to "lti_external" for lti providers', async () => { await act(async () => render(renderComponent())); // Make a change to the provider to test_lti and set the email const selectElement = screen.getByDisplayValue('mockproc'); fireEvent.change(selectElement, { target: { value: 'test_lti' } }); const escalationEmail = screen.getByTestId('escalationEmail'); expect(escalationEmail.value).toEqual('test@example.com'); fireEvent.change(escalationEmail, { target: { value: 'test_lti@example.com' } }); expect(escalationEmail.value).toEqual('test_lti@example.com'); const submitButton = screen.getByTestId('submissionButton'); 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', escalation_email: 'test_lti@example.com', }); // 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', proctoring_escalation_email: 'test_lti@example.com', }, }); await waitFor(() => { 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(renderComponent())); const submitButton = screen.getByTestId('submissionButton'); 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, escalation_email: 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', }, }); await waitFor(() => { 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', }, available_proctoring_providers: ['software_secure', 'mockproc'], requires_escalation_email_providers: [], course_start_date: '2070-01-01T00:00:00Z', }); await act(async () => render(renderComponent())); const submitButton = screen.getByTestId('submissionButton'); 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', }, }); await waitFor(() => { 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); await act(async () => render(renderComponent())); const submitButton = screen.getByTestId('submissionButton'); fireEvent.click(submitButton); expect(axiosMock.history.post.length).toBe(1); await waitFor(() => { 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('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(renderComponent())); const submitButton = screen.getByTestId('submissionButton'); fireEvent.click(submitButton); expect(axiosMock.history.post.length).toBe(1); await waitFor(() => { 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); }); }); test('Exams API permission error', async () => { axiosMock.onPatch( `${ExamsApiService.getExamsBaseUrl()}/api/v1/configs/course_id/${defaultProps.courseId}`, ).reply(403, 'error'); await act(async () => render(renderComponent())); const submitButton = screen.getByTestId('submissionButton'); fireEvent.click(submitButton); expect(axiosMock.history.post.length).toBe(1); await waitFor(() => { const errorAlert = screen.getByTestId('saveError'); expect(errorAlert.textContent).toEqual( expect.stringContaining('You do not have permission to edit proctored exam settings for this course'), ); 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), ).reply(500); await act(async () => render(renderComponent())); const submitButton = screen.getByTestId('submissionButton'); fireEvent.click(submitButton); expect(axiosMock.history.post.length).toBe(1); await waitFor(() => { 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); }); // now make a call that will allow for a successful save axiosMock.onPost( StudioApiService.getProctoredExamSettingsUrl(defaultProps.courseId), ).reply(200, 'success'); fireEvent.click(submitButton); expect(axiosMock.history.post.length).toBe(2); await waitFor(() => { const successAlert = screen.getByTestId('saveSuccess'); expect(successAlert.textContent).toEqual( expect.stringContaining('Proctored exam settings saved successfully.'), ); expect(document.activeElement).toEqual(successAlert); }); }); }); });