committed by
GitHub
parent
397f6c5d2e
commit
816d7f7f97
@@ -75,7 +75,7 @@ function AppSettingsModalBase({
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
<ModalDialog.Title data-testid="modal-title">
|
||||
{title}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
@@ -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 && (
|
||||
<Alert variant="danger" icon={Info} ref={alertRef}>
|
||||
<Alert.Heading>
|
||||
{formikProps.errors.enabled?.title ?? intl.formatMessage(messages.errorSavingTitle)}
|
||||
{formikProps.errors.enabled?.title || intl.formatMessage(messages.errorSavingTitle)}
|
||||
</Alert.Heading>
|
||||
{formikProps.errors.enabled?.message ?? intl.formatMessage(messages.errorSavingMessage)}
|
||||
{formikProps.errors.enabled?.message || intl.formatMessage(messages.errorSavingMessage)}
|
||||
</Alert>
|
||||
)}
|
||||
<FormSwitchGroup
|
||||
|
||||
@@ -74,7 +74,7 @@ function LiveSettings({
|
||||
</SelectableBox>
|
||||
))}
|
||||
</SelectableBox.Set>
|
||||
<p>{intl.formatMessage(messages.providerHelperText, { providerName: 'Zoom' })}</p>
|
||||
<p data-testid="helper-text">{intl.formatMessage(messages.providerHelperText, { providerName: 'Zoom' })}</p>
|
||||
{values.piiSharingEnable ? (
|
||||
<>
|
||||
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
|
||||
@@ -107,7 +107,7 @@ function LiveSettings({
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<p>{intl.formatMessage(messages.requestPiiSharingEnable)}</p>
|
||||
<p data-testid="request-pii-sharing">{intl.formatMessage(messages.requestPiiSharingEnable)}</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
232
src/pages-and-resources/live/Settings.test.jsx
Normal file
232
src/pages-and-resources/live/Settings.test.jsx
Normal file
@@ -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(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<Switch>
|
||||
<PageRoute path={liveSettingsUrl}>
|
||||
<LiveSettings onClose={() => {}} />
|
||||
</PageRoute>
|
||||
</Switch>
|
||||
</PagesAndResourcesProvider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
85
src/pages-and-resources/live/factories/mockApiResponses.jsx
Normal file
85
src/pages-and-resources/live/factories/mockApiResponses.jsx
Normal file
@@ -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,
|
||||
});
|
||||
Reference in New Issue
Block a user