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:
84
package-lock.json
generated
84
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
55
src/courseware/course/chat/Chat.jsx
Normal file
55
src/courseware/course/chat/Chat.jsx
Normal 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);
|
||||
151
src/courseware/course/chat/Chat.test.jsx
Normal file
151
src/courseware/course/chat/Chat.test.jsx
Normal 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();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
1
src/courseware/course/chat/index.js
Normal file
1
src/courseware/course/chat/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Chat';
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './ChatTrigger';
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user