Skip to content

Commit

Permalink
feat: use backend GET endpoint to determine whether to show Learning …
Browse files Browse the repository at this point in the history
…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 edx/learning-assistant#63 for more details.
  • Loading branch information
MichaelRoytman committed Jan 31, 2024
1 parent 7b940f4 commit ad2715e
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 10 deletions.
8 changes: 8 additions & 0 deletions src/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
5 changes: 5 additions & 0 deletions src/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const learningAssistantSlice = createSlice({
conversationId: uuidv4(),
disclosureAcknowledged: false,
sidebarIsOpen: false,
isEnabled: false,
},
reducers: {
setCurrentMessage: (state, { payload }) => {
Expand Down Expand Up @@ -43,6 +44,9 @@ export const learningAssistantSlice = createSlice({
setSidebarIsOpen: (state, { payload }) => {
state.sidebarIsOpen = payload;
},
setIsEnabled: (state, { payload }) => {
state.isEnabled = payload;
},
},
});

Expand All @@ -56,6 +60,7 @@ export const {
resetApiError,
setDisclosureAcknowledged,
setSidebarIsOpen,
setIsEnabled,
} = learningAssistantSlice.actions;

export const {
Expand Down
15 changes: 14 additions & 1 deletion src/data/thunks.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,6 +11,7 @@ import {
resetApiError,
setDisclosureAcknowledged,
setSidebarIsOpen,
setIsEnabled,
} from './slice';

export function addChatMessage(role, content, courseId) {
Expand Down Expand Up @@ -89,3 +90,15 @@ export function updateSidebarIsOpen(isOpen) {
dispatch(setSidebarIsOpen(isOpen));
};
}


Check failure on line 94 in src/data/thunks.js

View workflow job for this annotation

GitHub Actions / test

More than 1 blank line not allowed
export function getIsEnabled(courseId) {
return async (dispatch) => {
try {
const data = await fetchLearningAssistantEnabled(courseId);
dispatch(setIsEnabled(data.enabled));
} catch (error) {
dispatch(setApiError());
}
};
}
13 changes: 10 additions & 3 deletions src/widgets/Xpert.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
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';

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 ? (
<div>
<ToggleXpert
courseId={courseId}
Expand All @@ -29,7 +36,7 @@ const Xpert = ({ courseId, contentToolsEnabled, unitId }) => {
unitId={unitId}
/>
</div>
);
) : null;
};

Xpert.propTypes = {
Expand Down
63 changes: 57 additions & 6 deletions src/widgets/Xpert.test.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(<Xpert courseId={courseId} contentToolsEnabled={false} />, { 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(<Xpert courseId={courseId} contentToolsEnabled={false} />, { 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();
Expand All @@ -60,8 +72,8 @@ test('clicking the call to action dismiss button removes the message', async ()
render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { 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();
Expand All @@ -76,6 +88,9 @@ test('clicking the call to action opens the sidebar', async () => {

render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { 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
Expand All @@ -93,6 +108,9 @@ test('clicking the toggle button opens the sidebar', async () => {

render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { 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
Expand All @@ -111,6 +129,9 @@ test('submitted text appears as message in the sidebar', async () => {

render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { preloadedState: initialState });

// wait for button to appear
await screen.findByTestId('toggle-button');

await user.click(screen.queryByTestId('toggle-button'));

// type the user message
Expand Down Expand Up @@ -139,6 +160,9 @@ test('loading message appears in the sidebar while the response loads', async ()

render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { preloadedState: initialState });

// wait for button to appear
await screen.findByTestId('toggle-button');

await user.click(screen.queryByTestId('toggle-button'));

// type the user message
Expand All @@ -163,6 +187,9 @@ test('response text appears as message in the sidebar', async () => {

render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { preloadedState: initialState });

// wait for button to appear
await screen.findByTestId('toggle-button');

await user.click(screen.queryByTestId('toggle-button'));

// type the user message
Expand All @@ -184,6 +211,9 @@ test('clicking the clear button clears messages in the sidebar', async () => {

render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { preloadedState: initialState });

// wait for button to appear
await screen.findByTestId('toggle-button');

await user.click(screen.queryByTestId('toggle-button'));

// type the user message
Expand All @@ -202,6 +232,9 @@ test('clicking the close button closes the sidebar', async () => {
const user = userEvent.setup();
render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { 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'));

Expand All @@ -212,6 +245,9 @@ test('toggle elements do not appear when sidebar is open', async () => {
const user = userEvent.setup();
render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { 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();
Expand All @@ -235,6 +271,9 @@ test('error message should disappear upon succesful api call', async () => {
};
render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { preloadedState: errorState });

// wait for button to appear
await screen.findByTestId('toggle-button');

await user.click(screen.queryByTestId('toggle-button'));

// assert that error has focus
Expand Down Expand Up @@ -266,6 +305,9 @@ test('error message should disappear when dismissed', async () => {
};
render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { preloadedState: errorState });

// wait for button to appear
await screen.findByTestId('toggle-button');

await user.click(screen.queryByTestId('toggle-button'));

// assert that error message exists
Expand All @@ -292,6 +334,9 @@ test('error message should disappear when messages cleared', async () => {
};
render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { preloadedState: errorState });

// wait for button to appear
await screen.findByTestId('toggle-button');

await user.click(screen.queryByTestId('toggle-button'));

// assert that error message exists
Expand All @@ -307,6 +352,9 @@ test('popup modal should open chat', async () => {

render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { 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();
Expand All @@ -327,6 +375,9 @@ test('popup modal should close and display CTA', async () => {

render(<Xpert courseId={courseId} contentToolsEnabled={false} />, { 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();
Expand Down

0 comments on commit ad2715e

Please sign in to comment.