diff --git a/src/data/api.js b/src/data/api.js
index 2d49d3b1..ec10b41e 100644
--- a/src/data/api.js
+++ b/src/data/api.js
@@ -19,4 +19,12 @@ async function fetchChatResponse(courseId, messageList, unitId) {
return data;
}
+async function fetchLearningAssistantEnabled(courseId) {
+ const url = new URL(`${getConfig().CHAT_RESPONSE_URL}/${courseId}/enabled`);
+
+ const { data } = await getAuthenticatedHttpClient().get(url.href);
+ return data;
+}
+
export default fetchChatResponse;
+export { fetchLearningAssistantEnabled };
diff --git a/src/data/slice.js b/src/data/slice.js
index 21dd36d1..cf2be001 100644
--- a/src/data/slice.js
+++ b/src/data/slice.js
@@ -12,6 +12,7 @@ export const learningAssistantSlice = createSlice({
conversationId: uuidv4(),
disclosureAcknowledged: false,
sidebarIsOpen: false,
+ isEnabled: false,
},
reducers: {
setCurrentMessage: (state, { payload }) => {
@@ -43,6 +44,9 @@ export const learningAssistantSlice = createSlice({
setSidebarIsOpen: (state, { payload }) => {
state.sidebarIsOpen = payload;
},
+ setIsEnabled: (state, { payload }) => {
+ state.isEnabled = payload;
+ },
},
});
@@ -56,6 +60,7 @@ export const {
resetApiError,
setDisclosureAcknowledged,
setSidebarIsOpen,
+ setIsEnabled,
} = learningAssistantSlice.actions;
export const {
diff --git a/src/data/thunks.js b/src/data/thunks.js
index 742d0f28..b14359e7 100644
--- a/src/data/thunks.js
+++ b/src/data/thunks.js
@@ -1,6 +1,6 @@
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
-import fetchChatResponse from './api';
+import fetchChatResponse, { fetchLearningAssistantEnabled } from './api';
import {
setCurrentMessage,
clearCurrentMessage,
@@ -11,6 +11,7 @@ import {
resetApiError,
setDisclosureAcknowledged,
setSidebarIsOpen,
+ setIsEnabled,
} from './slice';
export function addChatMessage(role, content, courseId) {
@@ -89,3 +90,15 @@ export function updateSidebarIsOpen(isOpen) {
dispatch(setSidebarIsOpen(isOpen));
};
}
+
+
+export function getIsEnabled(courseId) {
+ return async (dispatch) => {
+ try {
+ const data = await fetchLearningAssistantEnabled(courseId);
+ dispatch(setIsEnabled(data.enabled));
+ } catch (error) {
+ dispatch(setApiError());
+ }
+ };
+}
diff --git a/src/widgets/Xpert.jsx b/src/widgets/Xpert.jsx
index 505fdef2..2634bc45 100644
--- a/src/widgets/Xpert.jsx
+++ b/src/widgets/Xpert.jsx
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
+import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import { updateSidebarIsOpen } from '../data/thunks';
+import { updateSidebarIsOpen, getIsEnabled } from '../data/thunks';
import ToggleXpert from '../components/ToggleXpertButton';
import Sidebar from '../components/Sidebar';
@@ -8,13 +9,19 @@ const Xpert = ({ courseId, contentToolsEnabled, unitId }) => {
const dispatch = useDispatch();
const {
+ isEnabled,
sidebarIsOpen,
} = useSelector(state => state.learningAssistant);
const setSidebarIsOpen = (isOpen) => {
dispatch(updateSidebarIsOpen(isOpen));
};
- return (
+
+ useEffect(() => {
+ dispatch(getIsEnabled(courseId));
+ }, [dispatch, courseId]);
+
+ return isEnabled ? (
{
unitId={unitId}
/>
- );
+ ) : null;
};
Xpert.propTypes = {
diff --git a/src/widgets/Xpert.test.jsx b/src/widgets/Xpert.test.jsx
index f1c730de..dba03af8 100644
--- a/src/widgets/Xpert.test.jsx
+++ b/src/widgets/Xpert.test.jsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { screen, fireEvent } from '@testing-library/react';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as api from '../data/api';
@@ -39,18 +39,30 @@ const assertSidebarElementsNotInDOM = () => {
beforeEach(() => {
const responseMessage = createRandomResponseForTesting();
jest.spyOn(api, 'default').mockResolvedValue(responseMessage);
+ jest.spyOn(api, 'fetchLearningAssistantEnabled').mockResolvedValue({ enabled: true });
+
window.localStorage.clear();
// Popup modal should be ignored for all tests unless explicitly enabled. This is because
// it makes all other elements non-clickable, so it is easier to test most test cases without the popup.
window.localStorage.setItem('completedLearningAssistantTour', 'true');
});
-test('initial load displays correct elements', () => {
+test('doesn\'t load if not enabled', async () => {
+ jest.spyOn(api, 'fetchLearningAssistantEnabled').mockResolvedValue({ enabled: false });
+
+ render(, { preloadedState: initialState });
+
+ // button to open chat should not be in the DOM
+ await waitFor(() => expect(screen.queryByTestId('toggle-button')).not.toBeInTheDocument());
+ // expect(screen.queryByTestId('toggle-button')).not.toBeVisible();
+ await waitFor(() => (expect(screen.queryByTestId('action-message')).not.toBeInTheDocument()));
+});
+test('initial load displays correct elements', async () => {
render(, { preloadedState: initialState });
// button to open chat should be in the DOM
- expect(screen.queryByTestId('toggle-button')).toBeVisible();
- expect(screen.queryByTestId('action-message')).toBeVisible();
+ await waitFor(() => expect(screen.queryByTestId('toggle-button')).toBeVisible());
+ await waitFor(() => expect(screen.queryByTestId('action-message')).toBeVisible());
// assert that UI elements in the sidebar are not in the DOM
assertSidebarElementsNotInDOM();
@@ -60,8 +72,8 @@ test('clicking the call to action dismiss button removes the message', async ()
render(, { preloadedState: initialState });
// button to open chat should be in the DOM
- expect(screen.queryByTestId('toggle-button')).toBeVisible();
- expect(screen.queryByTestId('action-message')).toBeVisible();
+ await waitFor(() => expect(screen.queryByTestId('toggle-button')).toBeVisible());
+ await waitFor(() => expect(screen.queryByTestId('action-message')).toBeVisible());
await user.click(screen.getByRole('button', { name: 'dismiss' }));
expect(screen.queryByTestId('toggle-button')).toBeVisible();
@@ -76,6 +88,9 @@ test('clicking the call to action opens the sidebar', async () => {
render(, { preloadedState: initialState });
+ // wait for button to appear
+ await screen.findByTestId('message-button');
+
await user.click(screen.queryByTestId('message-button'));
// assert that UI elements present in the sidebar are visible
@@ -93,6 +108,9 @@ test('clicking the toggle button opens the sidebar', async () => {
render(, { preloadedState: initialState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
await user.click(screen.queryByTestId('toggle-button'));
// assert that UI elements present in the sidebar are visible
@@ -111,6 +129,9 @@ test('submitted text appears as message in the sidebar', async () => {
render(, { preloadedState: initialState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
await user.click(screen.queryByTestId('toggle-button'));
// type the user message
@@ -139,6 +160,9 @@ test('loading message appears in the sidebar while the response loads', async ()
render(, { preloadedState: initialState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
await user.click(screen.queryByTestId('toggle-button'));
// type the user message
@@ -163,6 +187,9 @@ test('response text appears as message in the sidebar', async () => {
render(, { preloadedState: initialState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
await user.click(screen.queryByTestId('toggle-button'));
// type the user message
@@ -184,6 +211,9 @@ test('clicking the clear button clears messages in the sidebar', async () => {
render(, { preloadedState: initialState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
await user.click(screen.queryByTestId('toggle-button'));
// type the user message
@@ -202,6 +232,9 @@ test('clicking the close button closes the sidebar', async () => {
const user = userEvent.setup();
render(, { preloadedState: initialState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
await user.click(screen.queryByTestId('toggle-button'));
await user.click(screen.getByTestId('close-button'));
@@ -212,6 +245,9 @@ test('toggle elements do not appear when sidebar is open', async () => {
const user = userEvent.setup();
render(, { preloadedState: initialState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
await user.click(screen.queryByTestId('toggle-button'));
expect(screen.queryByTestId('toggle-button')).not.toBeInTheDocument();
@@ -235,6 +271,9 @@ test('error message should disappear upon succesful api call', async () => {
};
render(, { preloadedState: errorState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
await user.click(screen.queryByTestId('toggle-button'));
// assert that error has focus
@@ -266,6 +305,9 @@ test('error message should disappear when dismissed', async () => {
};
render(, { preloadedState: errorState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
await user.click(screen.queryByTestId('toggle-button'));
// assert that error message exists
@@ -292,6 +334,9 @@ test('error message should disappear when messages cleared', async () => {
};
render(, { preloadedState: errorState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
await user.click(screen.queryByTestId('toggle-button'));
// assert that error message exists
@@ -307,6 +352,9 @@ test('popup modal should open chat', async () => {
render(, { preloadedState: initialState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
// button to open chat should be in the DOM
expect(screen.queryByTestId('toggle-button')).toBeVisible();
expect(screen.queryByTestId('modal-message')).toBeVisible();
@@ -327,6 +375,9 @@ test('popup modal should close and display CTA', async () => {
render(, { preloadedState: initialState });
+ // wait for button to appear
+ await screen.findByTestId('toggle-button');
+
// button to open chat should be in the DOM
expect(screen.queryByTestId('toggle-button')).toBeVisible();
expect(screen.queryByTestId('modal-message')).toBeVisible();