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();