From 816d7f7f97b7112f95efcb2d025b077f140c87d7 Mon Sep 17 00:00:00 2001 From: Muhammad Adeel Tajamul <77053848+muhammadadeeltajamul@users.noreply.github.com> Date: Wed, 6 Apr 2022 01:58:03 +0500 Subject: [PATCH] test: live settings test cases (#274) * test: live settings test cases --- .../app-settings-modal/AppSettingsModal.jsx | 8 +- src/pages-and-resources/live/Settings.jsx | 4 +- .../live/Settings.test.jsx | 232 ++++++++++++++++++ src/pages-and-resources/live/data/api.js | 4 +- .../live/factories/mockApiResponses.jsx | 85 +++++++ 5 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 src/pages-and-resources/live/Settings.test.jsx create mode 100644 src/pages-and-resources/live/factories/mockApiResponses.jsx diff --git a/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx b/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx index aaedc3c64..710580511 100644 --- a/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx +++ b/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx @@ -75,7 +75,7 @@ function AppSettingsModalBase({ isFullscreenOnMobile > - + {title} @@ -159,7 +159,7 @@ function AppSettingsModal({ await handleSubmit(event); if (Object.keys(errors).length > 0) { await setSaveError(true); - alertRef?.current.scrollIntoView(); // eslint-disable-line no-unused-expressions + alertRef?.current.scrollIntoView?.(); // eslint-disable-line no-unused-expressions } }; @@ -216,9 +216,9 @@ function AppSettingsModal({ {saveError && ( - {formikProps.errors.enabled?.title ?? intl.formatMessage(messages.errorSavingTitle)} + {formikProps.errors.enabled?.title || intl.formatMessage(messages.errorSavingTitle)} - {formikProps.errors.enabled?.message ?? intl.formatMessage(messages.errorSavingMessage)} + {formikProps.errors.enabled?.message || intl.formatMessage(messages.errorSavingMessage)} )} ))} -

{intl.formatMessage(messages.providerHelperText, { providerName: 'Zoom' })}

+

{intl.formatMessage(messages.providerHelperText, { providerName: 'Zoom' })}

{values.piiSharingEnable ? ( <>

{intl.formatMessage(messages.formInstructions)}

@@ -107,7 +107,7 @@ function LiveSettings({ /> ) : ( -

{intl.formatMessage(messages.requestPiiSharingEnable)}

+

{intl.formatMessage(messages.requestPiiSharingEnable)}

)} ) diff --git a/src/pages-and-resources/live/Settings.test.jsx b/src/pages-and-resources/live/Settings.test.jsx new file mode 100644 index 000000000..c2c6d3698 --- /dev/null +++ b/src/pages-and-resources/live/Settings.test.jsx @@ -0,0 +1,232 @@ +import { + render, + act, + fireEvent, + waitFor, + queryByRole, + queryByTestId, + queryByText, +} from '@testing-library/react'; + +import { Switch } from 'react-router-dom'; +import { initializeMockApp, history } from '@edx/frontend-platform'; +import MockAdapter from 'axios-mock-adapter'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { AppProvider, PageRoute } from '@edx/frontend-platform/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import initializeStore from '../../store'; +import { executeThunk } from '../../utils'; +import LiveSettings from './Settings'; +import { + generateLiveConfigurationApiResponse, + courseId, + initialState, +} from './factories/mockApiResponses'; + +import { fetchLiveConfiguration } from './data/thunks'; +import { providerConfigurationApiUrl } from './data/api'; +import messages from './messages'; +import PagesAndResourcesProvider from '../PagesAndResourcesProvider'; + +let axiosMock; +let container; +let store; +const liveSettingsUrl = `/course/${courseId}/pages-and-resources/live/settings`; + +const renderComponent = () => { + const wrapper = render( + + + + + + {}} /> + + + + + , + ); + container = wrapper.container; +}; + +describe('LiveSettings', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: false, + roles: [], + }, + }); + store = initializeStore(initialState); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + history.push(liveSettingsUrl); + }); + + test('Live Configuration modal is visible', async () => { + renderComponent(); + + expect(queryByRole(container, 'dialog')).toBeVisible(); + }); + + test('Displays "Configure Live" heading', async () => { + renderComponent(); + + const headingElement = queryByTestId(container, 'modal-title'); + + expect(headingElement).toHaveTextContent(messages.heading.defaultMessage); + }); + + test('Displays title, helper and badge when live configuration button is enabled', async () => { + renderComponent(); + + const label = container.querySelector('label[for="enable-live-toggle"]'); + const helperText = queryByTestId(container, 'helper-text'); + + expect(label).toHaveTextContent(messages.enableLiveLabel.defaultMessage); + expect(label.firstChild).toHaveTextContent('Enabled'); + expect(helperText).toHaveTextContent( + messages.providerHelperText.defaultMessage.replace('{providerName}', 'Zoom'), + ); + }); + + test('Displays title, helper and hides badge when live configuration button is disabled', async () => { + const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`; + axiosMock.onGet(fetchProviderConfigUrl).reply( + 200, + generateLiveConfigurationApiResponse(false, false), + ); + await executeThunk(fetchLiveConfiguration(courseId), store.dispatch); + renderComponent(); + + const label = container.querySelector('label[for="enable-live-toggle"]'); + const helperText = queryByTestId(container, 'helper-text'); + + expect(label).toHaveTextContent('Live'); + expect(label.firstChild).not.toHaveTextContent('Enabled'); + expect(helperText).toHaveTextContent( + messages.providerHelperText.defaultMessage.replace('{providerName}', 'Zoom'), + ); + }); + + test('Displays provider heading, helper and all providers', async () => { + const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`; + axiosMock.onGet(fetchProviderConfigUrl).reply( + 200, + generateLiveConfigurationApiResponse(false, false), + ); + await executeThunk(fetchLiveConfiguration(courseId), store.dispatch); + renderComponent(); + + const providers = queryByRole(container, 'group'); + const helperText = queryByTestId(container, 'helper-text'); + + expect(providers.childElementCount).toBe(1); + expect(providers).toHaveTextContent('Zoom'); + expect(helperText).toHaveTextContent( + messages.providerHelperText.defaultMessage.replace('{providerName}', 'Zoom'), + ); + }); + + test('Only helper text and lti fields are visible when pii sharing is enabled', async () => { + const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`; + axiosMock.onGet(fetchProviderConfigUrl).reply( + 200, + generateLiveConfigurationApiResponse(), + ); + await executeThunk(fetchLiveConfiguration(courseId), store.dispatch); + renderComponent(); + + const consumerKey = container.querySelector('input[name="consumerKey"]').parentElement; + const consumerSecret = container.querySelector('input[name="consumerSecret"]').parentElement; + const launchUrl = container.querySelector('input[name="launchUrl"]').parentElement; + const launchEmail = container.querySelector('input[name="launchEmail"]').parentElement; + + expect(consumerKey.firstChild).toBeVisible(); + expect(consumerKey.lastChild).toHaveTextContent(messages.consumerKey.defaultMessage); + expect(consumerSecret.firstChild).toBeVisible(); + expect(consumerSecret.lastChild).toHaveTextContent(messages.consumerSecret.defaultMessage); + expect(launchUrl.firstChild).toBeVisible(); + expect(launchUrl.lastChild).toHaveTextContent(messages.launchUrl.defaultMessage); + expect(launchEmail.firstChild).toBeVisible(); + expect(launchEmail.lastChild).toHaveTextContent(messages.launchEmail.defaultMessage); + }); + + test('Only connect to support is visible when pii sharing is disabled', async () => { + const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`; + axiosMock.onGet(fetchProviderConfigUrl).reply( + 200, + generateLiveConfigurationApiResponse(false, false), + ); + await executeThunk(fetchLiveConfiguration(courseId), store.dispatch); + renderComponent(); + + const requestPiiText = queryByTestId(container, 'request-pii-sharing'); + const consumerKey = container.querySelector('input[name="consumerKey"]'); + const consumerSecret = container.querySelector('input[name="consumerSecret"]'); + const launchUrl = container.querySelector('input[name="launchUrl"]'); + const launchEmail = container.querySelector('input[name="launchEmail"]'); + + expect(requestPiiText).toHaveTextContent(messages.requestPiiSharingEnable.defaultMessage); + expect(consumerKey).not.toBeInTheDocument(); + expect(consumerSecret).not.toBeInTheDocument(); + expect(launchUrl).not.toBeInTheDocument(); + expect(launchEmail).not.toBeInTheDocument(); + }); + + test('Form should be submitted and closed when valid data is provided', async () => { + const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`; + const apiDefaultResponse = generateLiveConfigurationApiResponse(); + axiosMock.onPost(fetchProviderConfigUrl, apiDefaultResponse).reply(200, apiDefaultResponse); + axiosMock.onGet(fetchProviderConfigUrl).reply(200, apiDefaultResponse); + await executeThunk(fetchLiveConfiguration(courseId), store.dispatch); + renderComponent(); + + const saveButton = queryByText(container, 'Save'); + + await waitFor(async () => { + await act(async () => fireEvent.click(saveButton)); + expect(queryByRole(container, 'dialog')).not.toBeInTheDocument(); + }); + }); + + test('Provider Configuration should be displayed correctly', async () => { + const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`; + const apiDefaultResponse = generateLiveConfigurationApiResponse(); + axiosMock.onGet(fetchProviderConfigUrl).reply(200, apiDefaultResponse); + await executeThunk(fetchLiveConfiguration(courseId), store.dispatch); + renderComponent(); + + const consumerKey = container.querySelector('input[name="consumerKey"]'); + const consumerSecret = container.querySelector('input[name="consumerSecret"]'); + const launchUrl = container.querySelector('input[name="launchUrl"]'); + const launchEmail = container.querySelector('input[name="launchEmail"]'); + + expect(consumerKey.value).toBe(apiDefaultResponse.lti_configuration.lti_1p1_client_key); + expect(consumerSecret.value).toBe(apiDefaultResponse.lti_configuration.lti_1p1_client_secret); + expect(launchUrl.value).toBe(apiDefaultResponse.lti_configuration.lti_1p1_launch_url); + expect(launchEmail.value).toBe( + apiDefaultResponse.lti_configuration.lti_config.additional_parameters.custom_instructor_email, + ); + }); + + test('Unable to save error should be shown on submission if a field is empty', async () => { + const fetchProviderConfigUrl = `${providerConfigurationApiUrl}/${courseId}/`; + const apiDefaultResponse = generateLiveConfigurationApiResponse(); + apiDefaultResponse.lti_configuration.lti_1p1_client_key = ''; + + axiosMock.onGet(fetchProviderConfigUrl).reply(200, apiDefaultResponse); + await executeThunk(fetchLiveConfiguration(courseId), store.dispatch); + renderComponent(); + + const saveButton = queryByText(container, 'Save'); + + await waitFor(async () => { + await act(async () => fireEvent.click(saveButton)); + expect(queryByRole(container, 'alert')).toBeVisible(); + }); + }); +}); diff --git a/src/pages-and-resources/live/data/api.js b/src/pages-and-resources/live/data/api.js index 143ddddaa..f73c55926 100644 --- a/src/pages-and-resources/live/data/api.js +++ b/src/pages-and-resources/live/data/api.js @@ -8,8 +8,8 @@ ensureConfig([ const apiBaseUrl = getConfig().STUDIO_BASE_URL; -const providersApiUrl = `${apiBaseUrl}/api/course_live/providers`; -const providerConfigurationApiUrl = `${apiBaseUrl}/api/course_live/course`; +export const providersApiUrl = `${apiBaseUrl}/api/course_live/providers`; +export const providerConfigurationApiUrl = `${apiBaseUrl}/api/course_live/course`; /** * Fetches providers for provided course diff --git a/src/pages-and-resources/live/factories/mockApiResponses.jsx b/src/pages-and-resources/live/factories/mockApiResponses.jsx new file mode 100644 index 000000000..ab7f2c2fd --- /dev/null +++ b/src/pages-and-resources/live/factories/mockApiResponses.jsx @@ -0,0 +1,85 @@ +export const courseId = 'course-v1:edX+DemoX+Demo_Course'; + +export const initialState = { + courseDetail: { + courseId, + status: 'LOADED', + }, + pagesAndResources: { + courseAppIds: [ + 'live', + ], + loadingStatus: 'successful', + savingStatus: '', + courseAppsApiStatus: {}, + courseAppSettings: {}, + }, + models: { + courseApps: { + live: { + id: 'live', + enabled: true, + name: 'Live', + description: 'Enable in-platform video conferencing by configuring live', + allowedOperations: { + enable: true, + configure: true, + }, + documentationLinks: { + learnMoreConfiguration: '', + }, + }, + }, + }, + live: { + providers: { + available: { + zoom: { + name: 'Zoom LTI PRO', + features: [], + }, + }, + selectedProvider: {}, + active: 'zoom', + }, + appIds: [ + { + id: 'zoom', + }, + ], + status: 'successful', + configuration: { + courseKey: '', + enabled: true, + consumerKey: '', + consumerSecret: '', + launchUrl: '', + launchEmail: '', + provider: 'zoom', + piiSharingEnable: true, + }, + saveStatus: 'successful', + configuredProvider: 'zoom', + }, +}; + +export const generateLiveConfigurationApiResponse = ( + enabled = true, + piiSharingAllowed = true, +) => ({ + course_key: courseId, + provider_type: 'zoom', + enabled, + lti_configuration: { + lti_1p1_client_key: 'consumer_key', + lti_1p1_client_secret: 'secret_key', + lti_1p1_launch_url: 'https://launch-url.com', + version: 'lti_1p1', + lti_config: { + additional_parameters: { + custom_instructor_email: 'launch@email.com', + }, + }, + }, + pii_sharing_allowed: piiSharingAllowed, +});