-
Notifications
You must be signed in to change notification settings - Fork 211
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: install Xpert chatbot from frontend-lib-learning-assistant
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.
- Loading branch information
1 parent
2e90e21
commit c88cd44
Showing
17 changed files
with
332 additions
and
257 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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/[email protected]", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
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 }, | ||
); | ||
|
||
// This is rather brittle and depends on the implementation of the Xpert component, but there | ||
// isn't another way to validate that the Xpert is visible. | ||
const chat = screen.queryByRole('button', { name: 'Have a question?' }); | ||
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 }, | ||
); | ||
|
||
// This is rather brittle and depends on the implementation of the Xpert component, but there | ||
// isn't another way to validate that the Xpert is visible. | ||
const chat = screen.queryByRole('button', { name: 'Have a question?' }); | ||
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 }, | ||
); | ||
|
||
// This is rather brittle and depends on the implementation of the Xpert component, but there | ||
// isn't another way to validate that the Xpert is visible. | ||
const chat = screen.queryByRole('button', { name: 'Have a question?' }); | ||
if (test.isVisible) { | ||
expect(chat).toBeInTheDocument(); | ||
} else { | ||
expect(chat).not.toBeInTheDocument(); | ||
} | ||
}, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './Chat'; |
Oops, something went wrong.