feat: install Xpert chatbot from frontend-lib-learning-assistant (#1166)

This commit installs the Xpert chatbot feature from the frontend-lib-learning-assistant repository into the frontend-lib-learning application.

This component is rendered by the Course component. The component is only rendered when a few conditions are satisfied.
This commit is contained in:
Michael Roytman
2023-08-23 09:14:14 -04:00
committed by GitHub
parent 2b9b3db5d3
commit 2d29827e6b
18 changed files with 338 additions and 260 deletions

84
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-footer": "12.0.0",
"@edx/frontend-component-header": "4.0.0",
"@edx/frontend-lib-learning-assistant": "^1.4.0",
"@edx/frontend-lib-special-exams": "2.20.1",
"@edx/frontend-platform": "4.3.0",
"@edx/paragon": "20.46.0",
@@ -3431,6 +3432,78 @@
"node": ">=10"
}
},
"node_modules/@edx/frontend-lib-learning-assistant": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-1.4.0.tgz",
"integrity": "sha512-yTwN4Qqqx2atIX941IFEJ2HeH/nGj4pPdkpUhluce31IHjIB2oUyk1Vi/iBKASZvN/22qvrtpjCQClhFvfopEg==",
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"core-js": "3.31.1",
"prop-types": "15.8.1",
"react-feather": "^2.0.10"
},
"peerDependencies": {
"@edx/frontend-platform": "4.3.0",
"@edx/paragon": "20.46.0",
"@reduxjs/toolkit": "1.8.1",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-redux": "7.2.9",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"redux": "4.1.2",
"regenerator-runtime": "0.13.11",
"uuid": "9.0.0"
}
},
"node_modules/@edx/frontend-lib-learning-assistant/node_modules/@fortawesome/fontawesome-common-types": {
"version": "0.2.36",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz",
"integrity": "sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-lib-learning-assistant/node_modules/@fortawesome/fontawesome-svg-core": {
"version": "1.2.36",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
"integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "^0.2.36"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-lib-learning-assistant/node_modules/@fortawesome/react-fontawesome": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
"integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
"react": ">=16.3"
}
},
"node_modules/@edx/frontend-lib-learning-assistant/node_modules/core-js": {
"version": "3.31.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.1.tgz",
"integrity": "sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/@edx/frontend-lib-special-exams": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-special-exams/-/frontend-lib-special-exams-2.20.1.tgz",
@@ -19385,6 +19458,17 @@
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
"node_modules/react-feather": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/react-feather/-/react-feather-2.0.10.tgz",
"integrity": "sha512-BLhukwJ+Z92Nmdcs+EMw6dy1Z/VLiJTzEQACDUEnWMClhYnFykJCGWQx+NmwP/qQHGX/5CzQ+TGi8ofg2+HzVQ==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": ">=16.8.6"
}
},
"node_modules/react-focus-lock": {
"version": "2.9.5",
"resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.5.tgz",

View File

@@ -33,6 +33,7 @@
"@edx/frontend-component-footer": "12.0.0",
"@edx/frontend-component-header": "4.0.0",
"@edx/frontend-lib-special-exams": "2.20.1",
"@edx/frontend-lib-learning-assistant": "^1.4.0",
"@edx/frontend-platform": "4.3.0",
"@edx/paragon": "20.46.0",
"@edx/react-unit-test-utils": "npm:@edx/react-unit-test-utils@1.5.3",

View File

@@ -18,6 +18,9 @@ Object {
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
},
"learningAssistant": ObjectContaining {
"conversationId": Any<String>,
},
"models": Object {
"courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
@@ -336,6 +339,9 @@ Object {
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
},
"learningAssistant": ObjectContaining {
"conversationId": Any<String>,
},
"models": Object {
"courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": Object {
@@ -532,6 +538,9 @@ Object {
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
},
"learningAssistant": ObjectContaining {
"conversationId": Any<String>,
},
"models": Object {
"courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": Object {

View File

@@ -67,7 +67,14 @@ describe('Data layer integration tests', () => {
const state = store.getState();
expect(state.courseHome.courseStatus).toEqual('loaded');
expect(state).toMatchSnapshot();
expect(state).toMatchSnapshot({
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
// to keep track of conversations. This causes snapshots to fail, because this UUID
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
learningAssistant: expect.objectContaining({
conversationId: expect.any(String),
}),
});
});
it.each([401, 403, 404])(
@@ -111,7 +118,14 @@ describe('Data layer integration tests', () => {
const state = store.getState();
expect(state.courseHome.courseStatus).toEqual('loaded');
expect(state).toMatchSnapshot();
expect(state).toMatchSnapshot({
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
// to keep track of conversations. This causes snapshots to fail, because this UUID
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
learningAssistant: expect.objectContaining({
conversationId: expect.any(String),
}),
});
});
it.each([401, 403, 404])(
@@ -156,7 +170,14 @@ describe('Data layer integration tests', () => {
const state = store.getState();
expect(state.courseHome.courseStatus).toEqual('loaded');
expect(state).toMatchSnapshot();
expect(state).toMatchSnapshot({
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
// to keep track of conversations. This causes snapshots to fail, because this UUID
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
learningAssistant: expect.objectContaining({
conversationId: expect.any(String),
}),
});
});
it('Should handle the url including a targetUserId', async () => {

View File

@@ -10,11 +10,11 @@ import { AlertList } from '../../generic/user-messages';
import Sequence from './sequence';
import { CelebrationModal, shouldCelebrateOnSectionLoad, WeeklyGoalCelebrationModal } from './celebration';
import Chat from './chat/Chat';
import ContentTools from './content-tools';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import SidebarProvider from './sidebar/SidebarContextProvider';
import SidebarTriggers from './sidebar/SidebarTriggers';
import ChatTrigger from './lti-modal/ChatTrigger';
import { useModel } from '../../generic/model-store';
import { getSessionStorage, setSessionStorage } from '../../data/sessionStorage';
@@ -93,10 +93,10 @@ const Course = ({
/>
{shouldDisplayTriggers && (
<>
<ChatTrigger
<Chat
enabled={course.learningAssistantEnabled}
enrollmentMode={course.enrollmentMode}
isStaff={isStaff}
launchUrl={course.learningAssistantLaunchUrl}
courseId={courseId}
/>
<SidebarTriggers />

View File

@@ -0,0 +1,55 @@
import { createPortal } from 'react-dom';
import PropTypes from 'prop-types';
import { Xpert } from '@edx/frontend-lib-learning-assistant';
import { injectIntl } from '@edx/frontend-platform/i18n';
const Chat = ({
enabled,
enrollmentMode,
isStaff,
courseId,
}) => {
const VERIFIED_MODES = [
'professional',
'verified',
'no-id-professional',
'credit',
'masters',
'executive-education',
];
const isVerifiedEnrollmentMode = (
enrollmentMode !== null
&& enrollmentMode !== undefined
&& VERIFIED_MODES.some(mode => mode === enrollmentMode)
);
const shouldDisplayChat = (
enabled
&& (isVerifiedEnrollmentMode || isStaff) // display only to non-audit or staff
);
return (
<>
{/* Use a portal to ensure that component overlay does not compete with learning MFE styles. */}
{shouldDisplayChat && (createPortal(
<Xpert courseId={courseId} />,
document.body,
))}
</>
);
};
Chat.propTypes = {
isStaff: PropTypes.bool.isRequired,
enabled: PropTypes.bool.isRequired,
enrollmentMode: PropTypes.string,
courseId: PropTypes.string.isRequired,
};
Chat.defaultProps = {
enrollmentMode: null,
};
export default injectIntl(Chat);

View File

@@ -0,0 +1,151 @@
import { BrowserRouter } from 'react-router-dom';
import { configureStore } from '@reduxjs/toolkit';
import React from 'react';
import { reducer as learningAssistantReducer } from '@edx/frontend-lib-learning-assistant';
import { initializeMockApp, render, screen } from '../../../setupTest';
import Chat from './Chat';
jest.mock('@edx/frontend-platform/analytics');
initializeMockApp();
const courseId = 'course-v1:edX+DemoX+Demo_Course';
let testCases = [];
let enabledTestCases = [];
let disabledTestCases = [];
const enabledModes = ['professional', 'verified', 'no-id-professional', 'credit', 'masters', 'executive-education'];
const disabledModes = [null, undefined, 'xyz', 'audit'];
describe('Chat', () => {
// Generate test cases.
enabledTestCases = enabledModes.map((mode) => ({ enrollmentMode: mode, isVisible: true }));
disabledTestCases = disabledModes.map((mode) => ({ enrollmentMode: mode, isVisible: false }));
testCases = enabledTestCases.concat(disabledTestCases);
testCases.forEach(test => {
it(
`visibility determined by ${test.enrollmentMode} enrollment mode when enabled and not isStaff`,
async () => {
const store = configureStore({
reducer: {
learningAssistant: learningAssistantReducer,
},
});
render(
<BrowserRouter>
<Chat
enrollmentMode={test.enrollmentMode}
isStaff={false}
enabled
courseId={courseId}
/>
</BrowserRouter>,
{ store },
);
const chat = screen.queryByRole('button');
if (test.isVisible) {
expect(chat).toBeInTheDocument();
} else {
expect(chat).not.toBeInTheDocument();
}
},
);
});
// Generate test cases.
testCases = enabledModes.concat(disabledModes).map((mode) => ({ enrollmentMode: mode, isVisible: true }));
testCases.forEach(test => {
it('visibility determined by isStaff when enabled and any enrollment mode', async () => {
const store = configureStore({
reducer: {
learningAssistant: learningAssistantReducer,
},
});
render(
<BrowserRouter>
<Chat
enrollmentMode={test.enrollmentMode}
isStaff
enabled
courseId={courseId}
/>
</BrowserRouter>,
{ store },
);
const chat = screen.queryByRole('button');
if (test.isVisible) {
expect(chat).toBeInTheDocument();
} else {
expect(chat).not.toBeInTheDocument();
}
});
});
// Generate the map function used for generating test cases by currying the map function.
// In this test suite, visibility depends on whether the enrollment mode is a valid or invalid
// enrollment mode for enabling the Chat when the user is not a staff member and the Chat is enabled. Instead of
// defining two separate map functions that differ in only one case, curry the function.
const generateMapFunction = (areEnabledModes) => (
(mode) => (
[
{
enrollmentMode: mode, isStaff: true, enabled: true, isVisible: true,
},
{
enrollmentMode: mode, isStaff: true, enabled: false, isVisible: false,
},
{
enrollmentMode: mode, isStaff: false, enabled: true, isVisible: areEnabledModes,
},
{
enrollmentMode: mode, isStaff: false, enabled: false, isVisible: false,
},
]
)
);
// Generate test cases.
enabledTestCases = enabledModes.map(generateMapFunction(true));
disabledTestCases = disabledModes.map(generateMapFunction(false));
testCases = enabledTestCases.concat(disabledTestCases);
testCases = testCases.flat();
testCases.forEach(test => {
it(
`visibility determined by ${test.enabled} enabled when ${test.isStaff} isStaff
and ${test.enrollmentMode} enrollment mode`,
async () => {
const store = configureStore({
reducer: {
learningAssistant: learningAssistantReducer,
},
});
render(
<BrowserRouter>
<Chat
enrollmentMode={test.enrollmentMode}
isStaff={test.isStaff}
enabled={test.enabled}
courseId={courseId}
/>
</BrowserRouter>,
{ store },
);
const chat = screen.queryByRole('button');
if (test.isVisible) {
expect(chat).toBeInTheDocument();
} else {
expect(chat).not.toBeInTheDocument();
}
},
);
});
});

View File

@@ -0,0 +1 @@
export { default } from './Chat';

View File

@@ -1,133 +0,0 @@
import React, { useState } from 'react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
ModalDialog,
Icon,
useToggle,
OverlayTrigger,
Popover,
} from '@edx/paragon';
import { ChatBubbleOutline } from '@edx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import messages from './messages';
const ChatTrigger = ({
intl,
enrollmentMode,
isStaff,
launchUrl,
courseId,
}) => {
const [isOpen, open, close] = useToggle(false);
const [hasOpenedChat, setHasOpenedChat] = useState(false);
const { userId } = getAuthenticatedUser();
const VERIFIED_MODES = [
'professional',
'verified',
'no-id-professional',
'credit',
'masters',
'executive-education',
];
const isVerifiedEnrollmentMode = (
enrollmentMode !== null
&& enrollmentMode !== undefined
&& VERIFIED_MODES.some(mode => mode === enrollmentMode)
);
const shouldDisplayChat = (
launchUrl
&& (isVerifiedEnrollmentMode || isStaff) // display only to non-audit or staff
);
const handleOpen = () => {
if (!hasOpenedChat) {
setHasOpenedChat(true);
}
open();
sendTrackEvent('edx.ui.lms.lti_modal.opened', {
course_id: courseId,
user_id: userId,
is_staff: isStaff,
});
};
return (
<>
{shouldDisplayChat && (
<div
className={classNames('mt-3', 'd-flex', 'ml-auto')}
>
<OverlayTrigger
trigger="click"
key="top"
show={!hasOpenedChat}
overlay={(
<Popover id="popover-chat-information">
<Popover.Title as="h3">{intl.formatMessage(messages.popoverTitle)}</Popover.Title>
<Popover.Content>
{intl.formatMessage(messages.popoverContent)}
</Popover.Content>
</Popover>
)}
>
<button
className="border border-light-400 bg-transparent align-items-center align-content-center d-flex"
type="button"
onClick={handleOpen}
aria-label={intl.formatMessage(messages.openChatModalTrigger)}
>
<div className="icon-container d-flex position-relative align-items-center">
<Icon src={ChatBubbleOutline} className="m-0 m-auto" />
</div>
</button>
</OverlayTrigger>
<ModalDialog
onClose={close}
isOpen={isOpen}
title={intl.formatMessage(messages.modalTitle)}
size="xl"
hasCloseButton
>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages.modalTitle)}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<iframe
src={launchUrl}
allowFullScreen
style={{
width: '100%',
height: '60vh',
}}
title={intl.formatMessage(messages.modalTitle)}
/>
</ModalDialog.Body>
</ModalDialog>
</div>
)}
</>
);
};
ChatTrigger.propTypes = {
intl: intlShape.isRequired,
isStaff: PropTypes.bool.isRequired,
enrollmentMode: PropTypes.string,
launchUrl: PropTypes.string,
courseId: PropTypes.string.isRequired,
};
ChatTrigger.defaultProps = {
launchUrl: null,
enrollmentMode: null,
};
export default injectIntl(ChatTrigger);

View File

@@ -1,88 +0,0 @@
import { render } from '@testing-library/react';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { BrowserRouter } from 'react-router-dom';
import React from 'react';
import ChatTrigger from './ChatTrigger';
import { act, fireEvent, screen } from '../../../setupTest';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({ userId: 1 })),
}));
jest.mock('@edx/frontend-platform/analytics');
describe('ChatTrigger', () => {
it('handles click to open/close chat modal', async () => {
sendTrackEvent.mockClear();
render(
<IntlProvider>
<BrowserRouter>
<ChatTrigger
enrollmentMode={null}
isStaff
launchUrl="https://testurl.org"
courseId="course-edX"
/>
</BrowserRouter>,
</IntlProvider>,
);
const chatTrigger = screen.getByRole('button', { name: /Show chat modal/i });
expect(chatTrigger).toBeInTheDocument();
expect(screen.queryByText('Need help understanding course content?')).toBeInTheDocument();
await act(async () => {
fireEvent.click(chatTrigger);
});
const modalCloseButton = screen.getByRole('button', { name: /Close/i });
await expect(modalCloseButton).toBeInTheDocument();
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.lti_modal.opened', {
course_id: 'course-edX',
user_id: 1,
is_staff: true,
});
await act(async () => {
fireEvent.click(modalCloseButton);
});
await expect(modalCloseButton).not.toBeInTheDocument();
expect(screen.queryByText('Need help understanding course content?')).not.toBeInTheDocument();
});
const testCases = [
{ enrollmentMode: null, isVisible: false },
{ enrollmentMode: undefined, isVisible: false },
{ enrollmentMode: 'audit', isVisible: false },
{ enrollmentMode: 'xyz', isVisible: false },
{ enrollmentMode: 'professional', isVisible: true },
{ enrollmentMode: 'verified', isVisible: true },
{ enrollmentMode: 'no-id-professional', isVisible: true },
{ enrollmentMode: 'credit', isVisible: true },
{ enrollmentMode: 'masters', isVisible: true },
{ enrollmentMode: 'executive-education', isVisible: true },
];
testCases.forEach(test => {
it(`does chat to be visible based on enrollment mode of ${test.enrollmentMode}`, async () => {
render(
<IntlProvider>
<BrowserRouter>
<ChatTrigger
enrollmentMode={test.enrollmentMode}
isStaff={false}
launchUrl="https://testurl.org"
/>
</BrowserRouter>,
</IntlProvider>,
);
const chatTrigger = screen.queryByRole('button', { name: /Show chat modal/i });
if (test.isVisible) {
expect(chatTrigger).toBeInTheDocument();
} else {
expect(chatTrigger).not.toBeInTheDocument();
}
});
});
});

View File

@@ -1 +0,0 @@
export { default } from './ChatTrigger';

View File

@@ -1,26 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
popoverTitle: {
id: 'popover.title',
defaultMessage: 'Need help understanding course content?',
description: 'Title for popover alerting user of chat modal',
},
popoverContent: {
id: 'popover.content',
defaultMessage: 'Click here for your Xpert Learning Assistant.',
description: 'Content of the popover message',
},
openChatModalTrigger: {
id: 'chat.model.trigger',
defaultMessage: 'Show chat modal',
description: 'Alt text for button that opens the chat modal',
},
modalTitle: {
id: 'chat.model.title',
defaultMessage: 'Xpert Learning Assistant',
description: 'Title for chat modal header',
},
});
export default messages;

View File

@@ -57,5 +57,5 @@ Factory.define('courseMetadata')
related_programs: null,
user_needs_integrity_signature: false,
recommendations: null,
learning_assistant_launch_url: null,
learning_assistant_enabled: null,
});

View File

@@ -122,7 +122,7 @@ function normalizeMetadata(metadata) {
relatedPrograms: camelCaseObject(data.related_programs),
userNeedsIntegritySignature: data.user_needs_integrity_signature,
canAccessProctoredExams: data.can_access_proctored_exams,
learningAssistantLaunchUrl: data.learning_assistant_launch_url,
learningAssistantEnabled: data.learning_assistant_enabled,
};
}

View File

@@ -228,7 +228,7 @@ describe('Courseware Service', () => {
linkedinAddToProfileUrl: null,
relatedPrograms: null,
userNeedsIntegritySignature: false,
learningAssistantLaunchUrl: null,
learningAssistantEnabled: false,
};
setTimeout(() => {
provider.addInteraction({
@@ -334,7 +334,7 @@ describe('Courseware Service', () => {
verification_status: string('none'),
linkedin_add_to_profile_url: null,
user_needs_integrity_signature: boolean(false),
learning_assistant_launch_url: null,
learning_assistant_enabled: boolean(false),
},
},
});

View File

@@ -306,7 +306,7 @@
"verification_status": "none",
"linkedin_add_to_profile_url": null,
"user_needs_integrity_signature": false,
"learning_assistant_launch_url": null
"learning_assistant_enabled": false
},
"matchingRules": {
"$.body.access_expiration.expiration_date": {
@@ -442,7 +442,7 @@
"$.body.user_needs_integrity_signature": {
"match": "type"
},
"$.body.learning_assistant_launch_url": {
"$.body.learning_assistant_enabled": {
"match": "type"
}
}

View File

@@ -13,6 +13,7 @@ import PropTypes from 'prop-types';
import { render as rtlRender } from '@testing-library/react';
import { configureStore } from '@reduxjs/toolkit';
import MockAdapter from 'axios-mock-adapter';
import { reducer as learningAssistantReducer } from '@edx/frontend-lib-learning-assistant';
import AppProvider from '@edx/frontend-platform/react/AppProvider';
import { reducer as courseHomeReducer } from './course-home/data';
import { reducer as coursewareReducer } from './courseware/data/slice';
@@ -116,6 +117,7 @@ export async function initializeTestStore(options = {}, overrideStore = true) {
models: modelsReducer,
courseware: coursewareReducer,
courseHome: courseHomeReducer,
learningAssistant: learningAssistantReducer,
},
});
if (overrideStore) {

View File

@@ -1,3 +1,4 @@
import { reducer as learningAssistantReducer } from '@edx/frontend-lib-learning-assistant';
import { configureStore } from '@reduxjs/toolkit';
import { reducer as courseHomeReducer } from './course-home/data';
import { reducer as coursewareReducer } from './courseware/data/slice';
@@ -11,6 +12,7 @@ export default function initializeStore() {
models: modelsReducer,
courseware: coursewareReducer,
courseHome: courseHomeReducer,
learningAssistant: learningAssistantReducer,
recommendations: recommendationsReducer,
tours: toursReducer,
},