From ad2715ead80bce1f7ce5c3108833055e4406db6d Mon Sep 17 00:00:00 2001 From: michaelroytman Date: Tue, 30 Jan 2024 16:23:53 -0500 Subject: [PATCH] feat: use backend GET endpoint to determine whether to show Learning Assistant This commit uses a new GET endpoint published on the Learning Assistant backend to determine whether the Learning Assistant feature is enabled. If the features is not enabled, the Learning Assistant is not rendered, and vice-versa. This is an alternative to using the edx-platform Courseware API to determine whether the feature is enabled. The reason this decision was made was so as not to introduce 2U-specific code into the platform in the form of calls to the learning-assistant plugin and it's models and APIs. This allows the Learning Assistant frontend to encapsulate calls to the backend instead. Please see https://github.com/edx/learning-assistant/pull/63 for more details. --- src/data/api.js | 8 +++++ src/data/slice.js | 5 +++ src/data/thunks.js | 15 ++++++++- src/widgets/Xpert.jsx | 13 ++++++-- src/widgets/Xpert.test.jsx | 63 ++++++++++++++++++++++++++++++++++---- 5 files changed, 94 insertions(+), 10 deletions(-) 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();