From a6d9becd726a0542aa41477d4a99a8679da4b8fc Mon Sep 17 00:00:00 2001 From: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:47:16 -0400 Subject: [PATCH] Leangseu edx/masquerade user (#21) --- .../CertificateBanner.test.jsx | 4 +- .../CertificateBanner.test.jsx.snap | 52 ++++---- .../__snapshots__/index.test.jsx.snap | 1 + .../LearnerDashboardHeader/GreetingBanner.jsx | 2 +- .../LearnerDashboardHeader/index.jsx | 3 + .../__snapshots__/index.test.jsx.snap | 115 ++++++++++++++++++ src/containers/MasqueradeBar/hooks.js | 52 ++++++++ src/containers/MasqueradeBar/hooks.test.js | 70 +++++++++++ src/containers/MasqueradeBar/index.jsx | 80 ++++++++++++ src/containers/MasqueradeBar/index.scss | 22 ++++ src/containers/MasqueradeBar/index.test.jsx | 61 ++++++++++ src/containers/MasqueradeBar/messages.js | 26 ++++ .../__snapshots__/ProgramCard.test.jsx.snap | 1 + src/data/constants/requests.js | 3 + src/data/redux/hooks.js | 3 + src/data/redux/requests/reducer.js | 4 + src/data/redux/requests/selectors.js | 12 +- src/data/redux/requests/selectors.test.js | 22 +++- src/data/redux/thunkActions/app.js | 16 +++ src/data/redux/thunkActions/requests.js | 15 +++ src/data/services/lms/api.js | 6 +- src/setupTest.jsx | 11 ++ 22 files changed, 552 insertions(+), 29 deletions(-) create mode 100644 src/containers/MasqueradeBar/__snapshots__/index.test.jsx.snap create mode 100644 src/containers/MasqueradeBar/hooks.js create mode 100644 src/containers/MasqueradeBar/hooks.test.js create mode 100644 src/containers/MasqueradeBar/index.jsx create mode 100644 src/containers/MasqueradeBar/index.scss create mode 100644 src/containers/MasqueradeBar/index.test.jsx create mode 100644 src/containers/MasqueradeBar/messages.js diff --git a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx index b224fce..e17b401 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx @@ -104,8 +104,8 @@ describe('CourseBanner', () => { }); test('no display if audit access not expired and (course is not active or can upgrade)', () => { render(); - expect(el.isEmptyRender()).toEqual(true); + expect(el.html()).toEqual(''); render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } }); - expect(el.isEmptyRender()).toEqual(true); + expect(el.html()).toEqual(''); }); }); diff --git a/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap b/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap index 5c3160a..e4e6433 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap +++ b/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/CertificateBanner.test.jsx.snap @@ -1,33 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CourseBanner audit access expired, can upgrade snapshot: (auditAccessExpired, upgradeToAccess) 1`] = ` - - Your audit access to this course has expired. - - Upgrade now to access your course again. - + + + Your audit access to this course has expired. + + Upgrade now to access your course again. + + `; exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = ` - - Your audit access to this course has expired. - - - Find another course - - + + + Your audit access to this course has expired. + + + Find another course + + + `; exports[`CourseBanner course run active and cannot upgrade snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink) 1`] = ` - - Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future. - - - Explore course details. - - + + + Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future. + + + Explore course details. + + + `; diff --git a/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap index 6a589a2..dbc3ae1 100644 --- a/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap +++ b/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap @@ -5,6 +5,7 @@ exports[`RelatedProgramsBadge component snapshot: 3 programs 1`] = ` { ); }; GreetingBanner.propTypes = { - size: PropTypes.oneOf('small', 'large').isRequired, + size: PropTypes.oneOf(['small', 'large']).isRequired, }; export default GreetingBanner; diff --git a/src/containers/LearnerDashboardHeader/index.jsx b/src/containers/LearnerDashboardHeader/index.jsx index 8b8ff22..096125d 100644 --- a/src/containers/LearnerDashboardHeader/index.jsx +++ b/src/containers/LearnerDashboardHeader/index.jsx @@ -5,6 +5,7 @@ import { AppContext } from '@edx/frontend-platform/react'; import { Program } from '@edx/paragon/icons'; import { Button } from '@edx/paragon'; +import MasqueradeBar from 'containers/MasqueradeBar'; import AuthenticatedUserDropdown from './AuthenticatedUserDropdown'; import GreetingBanner from './GreetingBanner'; @@ -43,6 +44,8 @@ export const LearnerDashboardHeader = () => { {!isCollapsed && } + + > ); }; diff --git a/src/containers/MasqueradeBar/__snapshots__/index.test.jsx.snap b/src/containers/MasqueradeBar/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..6434cb7 --- /dev/null +++ b/src/containers/MasqueradeBar/__snapshots__/index.test.jsx.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MasqueradeBar snapshot can masquerade 1`] = ` + + + View as: + + + + + + Submit + + +`; + +exports[`MasqueradeBar snapshot can masquerade with input 1`] = ` + + + View as: + + + + + + Submit + + +`; + +exports[`MasqueradeBar snapshot cannot masquerade 1`] = `""`; + +exports[`MasqueradeBar snapshot is masquerading failed with error 1`] = ` + + + View as: + + + + + test-error + + + + Submit + + +`; + +exports[`MasqueradeBar snapshot is masquerading with input 1`] = ` + + + Viewing as: + + + test + + +`; diff --git a/src/containers/MasqueradeBar/hooks.js b/src/containers/MasqueradeBar/hooks.js new file mode 100644 index 0000000..54196cf --- /dev/null +++ b/src/containers/MasqueradeBar/hooks.js @@ -0,0 +1,52 @@ +import React from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { useDispatch, useSelector } from 'react-redux'; + +import { thunkActions, selectors } from 'data/redux'; +import { StrictDict } from 'utils'; +import * as module from './hooks'; + +export const state = StrictDict({ + masqueradeInput: (val) => React.useState(val), // eslint-disable-line +}); + +export const useMasqueradeInput = () => { + const [masqueradeInput, setMasqueradeInput] = module.state.masqueradeInput(''); + const handleMasqueradeInputChange = (e) => setMasqueradeInput(e.target.value); + return { + handleMasqueradeInputChange, + masqueradeInput, + }; +}; + +export const useMasqueradeBarData = ({ + authenticatedUser, +}) => { + const dispatch = useDispatch(); + const { formatMessage } = useIntl(); + const canMasquerade = authenticatedUser?.administrator; + + const handleMasqueradeSubmit = (user) => () => dispatch(thunkActions.app.masqueradeAs(user)); + const handleClearMasquerade = () => dispatch(thunkActions.app.clearMasquerade()); + + const { + isMasquerading, + isMasqueradingFailed, + masqueradeError, + } = useSelector(selectors.requests.masquerade); + const { masqueradeInput, handleMasqueradeInputChange } = module.useMasqueradeInput(); + + return { + canMasquerade, + isMasquerading, + isMasqueradingFailed, + masqueradeError, + masqueradeInput, + handleMasqueradeSubmit, + handleClearMasquerade, + handleMasqueradeInputChange, + formatMessage, + }; +}; + +export default useMasqueradeBarData; diff --git a/src/containers/MasqueradeBar/hooks.test.js b/src/containers/MasqueradeBar/hooks.test.js new file mode 100644 index 0000000..ab0773a --- /dev/null +++ b/src/containers/MasqueradeBar/hooks.test.js @@ -0,0 +1,70 @@ +import { MockUseState } from 'testUtils'; +import { thunkActions } from 'data/redux'; + +import { useSelector } from 'react-redux'; +import * as hooks from './hooks'; + +jest.mock('data/redux', () => ({ + thunkActions: { + app: { + masqueradeAs: jest.fn(), + clearMasquerade: jest.fn(), + }, + }, + selectors: { + requests: { + masquerade: jest.fn(), + }, + }, +})); + +const state = new MockUseState(hooks); + +describe('MasqueradeBar hooks', () => { + let out; + const authenticatedUser = { + administrator: true, + }; + describe('state values', () => { + state.testGetter(state.keys.masqueradeInput); + }); + describe('useMasqueradeBarData', () => { + beforeEach(() => { + state.mock(); + out = hooks.useMasqueradeBarData({ authenticatedUser }); + }); + afterEach(state.restore); + test('canMasquerade', () => { + expect(out.canMasquerade).toEqual(true); + }); + test('cannotMasquerade', () => { + out = hooks.useMasqueradeBarData({ + authenticatedUser: { + administrator: false, + }, + }); + expect(out.canMasquerade).toEqual(false); + }); + test('masqueradeError', () => { + expect(out.masqueradeError).toBeUndefined(); + useSelector.mockReturnValueOnce({ + masqueradeError: 'test error', + }); + out = hooks.useMasqueradeBarData({ authenticatedUser }); + expect(out.masqueradeError).toEqual('test error'); + }); + test('handleMasqueradeInputChange', () => { + expect(state.stateVals.masqueradeInput).toEqual(''); + out.handleMasqueradeInputChange({ target: { value: 'test' } }); + expect(state.setState.masqueradeInput).toHaveBeenCalledWith('test'); + }); + test('handleMasqueradeSubmit', () => { + out.handleMasqueradeSubmit('test')(); + expect(thunkActions.app.masqueradeAs).toHaveBeenCalledWith('test'); + }); + test('handleClearMasquerade', () => { + out.handleClearMasquerade(); + expect(thunkActions.app.clearMasquerade).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/containers/MasqueradeBar/index.jsx b/src/containers/MasqueradeBar/index.jsx new file mode 100644 index 0000000..20eb2ab --- /dev/null +++ b/src/containers/MasqueradeBar/index.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { AppContext } from '@edx/frontend-platform/react'; + +import { + Chip, + Button, + FormControl, + FormControlFeedback, + FormLabel, + FormGroup, +} from '@edx/paragon'; +import { Close } from '@edx/paragon/icons'; + +import messages from './messages'; +import { useMasqueradeBarData } from './hooks'; +import './index.scss'; + +export const MasqueradeBar = () => { + const { authenticatedUser } = React.useContext(AppContext); + + const { + canMasquerade, + isMasquerading, + isMasqueradingFailed, + masqueradeInput, + masqueradeError, + handleMasqueradeInputChange, + handleClearMasquerade, + handleMasqueradeSubmit, + formatMessage, + } = useMasqueradeBarData({ authenticatedUser }); + + if (!canMasquerade) { return null; } + + return ( + + {isMasquerading ? ( + <> + + {formatMessage(messages.ViewingAs)} + + + {masqueradeInput} + + > + ) : ( + <> + + {formatMessage(messages.ViewAs)} + + + + {isMasqueradingFailed && ( + + {masqueradeError} + + )} + + + {formatMessage(messages.SubmitButton)} + + > + )} + + ); +}; + +export default MasqueradeBar; diff --git a/src/containers/MasqueradeBar/index.scss b/src/containers/MasqueradeBar/index.scss new file mode 100644 index 0000000..7a9014b --- /dev/null +++ b/src/containers/MasqueradeBar/index.scss @@ -0,0 +1,22 @@ +@import "@edx/paragon/scss/core/core"; + +.masquerade-bar { + display: flex; + align-items: flex-start; + padding: map-get($spacers, 4) map-get($spacers, 3) 0 map-get($spacers, 3); + + .masquerade-form-label { + padding: map-get($spacers, 2) map-get($spacers, 1); + } + + .masquerade-form-input { + margin-bottom: 0; + flex-grow: 1; + max-width: map-get($grid-breakpoints, 'md'); + } + + .masquerade-chip { + padding: map-get($spacers, 2) map-get($spacers, 3); + font-size: $font-size-base; + } +} \ No newline at end of file diff --git a/src/containers/MasqueradeBar/index.test.jsx b/src/containers/MasqueradeBar/index.test.jsx new file mode 100644 index 0000000..f3704e7 --- /dev/null +++ b/src/containers/MasqueradeBar/index.test.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { formatMessage } from 'testUtils'; + +import MasqueradeBar from '.'; +import hooks from './hooks'; + +jest.mock('./hooks', () => ({ + useMasqueradeBarData: jest.fn(), +})); + +describe('MasqueradeBar', () => { + const masqueradeMockData = { + canMasquerade: true, + isMasquerading: false, + isMasqueradingFailed: false, + masqueradeInput: '', + masqueradeError: '', + handleMasqueradeInputChange: jest.fn().mockName('handleMasqueradeInputChange'), + handleClearMasquerade: jest.fn().mockName('handleClearMasquerade'), + handleMasqueradeSubmit: jest.fn().mockName('handleMasqueradeSubmit'), + formatMessage, + }; + + describe('snapshot', () => { + test('can masquerade', () => { + hooks.useMasqueradeBarData.mockReturnValueOnce(masqueradeMockData); + expect(shallow()).toMatchSnapshot(); + }); + test('can masquerade with input', () => { + hooks.useMasqueradeBarData.mockReturnValueOnce({ + ...masqueradeMockData, + masqueradeInput: 'test', + }); + expect(shallow()).toMatchSnapshot(); + }); + test('cannot masquerade', () => { + hooks.useMasqueradeBarData.mockReturnValueOnce({ + ...masqueradeMockData, + canMasquerade: false, + }); + expect(shallow()).toMatchSnapshot(); + }); + test('is masquerading with input', () => { + hooks.useMasqueradeBarData.mockReturnValueOnce({ + ...masqueradeMockData, + isMasquerading: true, + masqueradeInput: 'test', + }); + expect(shallow()).toMatchSnapshot(); + }); + test('is masquerading failed with error', () => { + hooks.useMasqueradeBarData.mockReturnValueOnce({ + ...masqueradeMockData, + isMasqueradingFailed: true, + masqueradeError: 'test-error', + }); + expect(shallow()).toMatchSnapshot(); + }); + }); +}); diff --git a/src/containers/MasqueradeBar/messages.js b/src/containers/MasqueradeBar/messages.js new file mode 100644 index 0000000..fe9bfe7 --- /dev/null +++ b/src/containers/MasqueradeBar/messages.js @@ -0,0 +1,26 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + ViewAs: { + id: 'MasqueradeBar.ViewAs', + defaultMessage: 'View as: ', + description: 'Label for the View as', + }, + ViewingAs: { + id: 'MasqueradeBar.ViewingAs', + defaultMessage: 'Viewing as: ', + description: 'Label for the Viewing as', + }, + SubmitButton: { + id: 'MasqueradeBar.SubmitButton', + defaultMessage: 'Submit', + description: 'Label for the Submit button', + }, + StudentNameInput: { + id: 'MasqueradeBar.StudentNameInput', + defaultMessage: 'Student username or email', + description: 'Label for the Student Name or email input', + }, +}); + +export default messages; diff --git a/src/containers/RelatedProgramsModal/components/__snapshots__/ProgramCard.test.jsx.snap b/src/containers/RelatedProgramsModal/components/__snapshots__/ProgramCard.test.jsx.snap index 53c3fcf..6a98133 100644 --- a/src/containers/RelatedProgramsModal/components/__snapshots__/ProgramCard.test.jsx.snap +++ b/src/containers/RelatedProgramsModal/components/__snapshots__/ProgramCard.test.jsx.snap @@ -42,6 +42,7 @@ exports[`RelatedProgramsModal ProgramCard snapshot 1`] = ` > props.data.programType diff --git a/src/data/constants/requests.js b/src/data/constants/requests.js index d02ba56..53a30c7 100644 --- a/src/data/constants/requests.js +++ b/src/data/constants/requests.js @@ -15,6 +15,9 @@ export const RequestKeys = StrictDict({ switchEntitlementSession: 'switchEntitlementSession', unenrollFromCourse: 'unenrollFromCourse', updateEmailSettings: 'updateEmailSettings', + enrollEntitlementSession: 'enrollEntitlementSession', + leaveEntitlementSession: 'leaveEntitlementSession', + masquerade: 'masquerade', }); export const ErrorCodes = StrictDict({ diff --git a/src/data/redux/hooks.js b/src/data/redux/hooks.js index 7172d80..70ff3ea 100644 --- a/src/data/redux/hooks.js +++ b/src/data/redux/hooks.js @@ -2,6 +2,7 @@ import { useSelector } from 'react-redux'; import { actions as appActions } from './app/reducer'; import appSelectors from './app/selectors'; +import requestSelectors from './requests/selectors'; const { courseCard } = appSelectors; @@ -36,3 +37,5 @@ export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPr export const useUpdateSelectSessionModalCallback = (dispatch, cardId) => () => dispatch( appActions.updateSelectSessionModal(cardId), ); + +export const useMasqueradeData = () => useSelector(requestSelectors.masquerade); diff --git a/src/data/redux/requests/reducer.js b/src/data/redux/requests/reducer.js index b9dea91..58ac64c 100644 --- a/src/data/redux/requests/reducer.js +++ b/src/data/redux/requests/reducer.js @@ -6,6 +6,10 @@ import { RequestStates, RequestKeys } from 'data/constants/requests'; const initialState = { [RequestKeys.initialize]: { status: RequestStates.inactive }, + [RequestKeys.refreshList]: { status: RequestStates.inactive }, + [RequestKeys.enrollEntitlementSession]: { status: RequestStates.inactive }, + [RequestKeys.leaveEntitlementSession]: { status: RequestStates.inactive }, + [RequestKeys.masquerade]: { status: RequestStates.inactive }, }; // eslint-disable-next-line no-unused-vars diff --git a/src/data/redux/requests/selectors.js b/src/data/redux/requests/selectors.js index 52c983a..4c6278a 100644 --- a/src/data/redux/requests/selectors.js +++ b/src/data/redux/requests/selectors.js @@ -1,5 +1,5 @@ import { StrictDict } from 'utils'; -import { RequestStates } from 'data/constants/requests'; +import { RequestStates, RequestKeys } from 'data/constants/requests'; // import * as module from './selectors'; export const requestStatus = (state, { requestKey }) => state.requests[requestKey]; @@ -16,6 +16,15 @@ export const errorCode = (request) => request.error?.response?.data; export const data = (request) => request.data; +export const masquerade = (state) => { + const request = requestStatus(state, { requestKey: RequestKeys.masquerade }); + return { + isMasquerading: isCompleted(request), + isMasqueradingFailed: isFailed(request), + masqueradeError: error(request), + }; +}; + export default StrictDict({ requestStatus, isInactive: statusSelector(isInactive), @@ -26,4 +35,5 @@ export default StrictDict({ errorCode: statusSelector(errorCode), errorStatus: statusSelector(errorStatus), data: statusSelector(data), + masquerade, }); diff --git a/src/data/redux/requests/selectors.test.js b/src/data/redux/requests/selectors.test.js index 089dc71..9ee715f 100644 --- a/src/data/redux/requests/selectors.test.js +++ b/src/data/redux/requests/selectors.test.js @@ -1,4 +1,4 @@ -import { RequestStates } from 'data/constants/requests'; +import { RequestStates, RequestKeys } from 'data/constants/requests'; import selectors from './selectors'; @@ -69,4 +69,24 @@ describe('requests selectors unit tests', () => { test('data reurns the request data', () => { expect(select(selectors.data, { data: testValue })).toEqual(testValue); }); + test('masquerade returns the masquerade data', () => { + const mockResponse = (response) => ({ + requests: { + [RequestKeys.masquerade]: response, + }, + }); + expect(selectors.masquerade(mockResponse(completedRequest))).toEqual({ + isMasquerading: true, + isMasqueradingFailed: false, + masqueradeError: undefined, + }); + expect(selectors.masquerade(mockResponse({ + ...failedRequest, + error: testValue, + }))).toEqual({ + isMasquerading: false, + isMasqueradingFailed: true, + masqueradeError: testValue, + }); + }); }); diff --git a/src/data/redux/thunkActions/app.js b/src/data/redux/thunkActions/app.js index b916025..5655239 100644 --- a/src/data/redux/thunkActions/app.js +++ b/src/data/redux/thunkActions/app.js @@ -80,6 +80,20 @@ export const unenrollFromCourse = (courseId, reason) => (dispatch) => { })); }; +export const masqueradeAs = (user) => (dispatch) => ( + dispatch(requests.masqueradeAs({ + user, + onSuccess: (({ courses }) => { + dispatch(actions.app.loadCourses({ courses })); + }), + })) +); + +export const clearMasquerade = () => (dispatch) => { + dispatch(requests.clearMasquerade()); + dispatch(module.refreshList()); +}; + export default StrictDict({ initialize, refreshList, @@ -88,4 +102,6 @@ export default StrictDict({ switchEntitlementEnrollment, leaveEntitlementSession, unenrollFromCourse, + masqueradeAs, + clearMasquerade, }); diff --git a/src/data/redux/thunkActions/requests.js b/src/data/redux/thunkActions/requests.js index 92ba438..ecb4a81 100644 --- a/src/data/redux/thunkActions/requests.js +++ b/src/data/redux/thunkActions/requests.js @@ -84,8 +84,23 @@ export const updateEmailSettings = ({ courseId, enable, ...options }) => module. options, ); +export const masqueradeAs = ({ user, onSuccess, onFailure }) => (dispatch) => { + dispatch(networkRequest({ + requestKey: RequestKeys.masquerade, + onFailure, + onSuccess, + promise: api.initializeList({ user }), + })); +}; + +export const clearMasquerade = () => (dispatch) => dispatch( + actions.requests.clearRequest({ requestKey: RequestKeys.masquerade }), +); + export default StrictDict({ initializeList, + masqueradeAs, + clearMasquerade, leaveEntitlementSession, newEntitlementEnrollment, switchEntitlementEnrollment, diff --git a/src/data/services/lms/api.js b/src/data/services/lms/api.js index a21b386..65bb4bc 100644 --- a/src/data/services/lms/api.js +++ b/src/data/services/lms/api.js @@ -9,7 +9,11 @@ import urls from './urls'; /********************************************************************************* * GET Actions *********************************************************************************/ -const initializeList = () => get(urls.init).then(({ data }) => data); +const initializeList = ({ user } = {}) => new Promise((resolve, reject) => { + get(stringifyUrl(urls.init, { user })) + .then(({ data }) => resolve(data)) + .catch(({ response }) => reject(response ? response.statusText : 'Unknown Error')); +}); const updateEntitlementEnrollment = ({ uuid, courseId }) => post(stringifyUrl( urls.entitlementEnrollment(uuid), diff --git a/src/setupTest.jsx b/src/setupTest.jsx index 8db2aca..f426f93 100755 --- a/src/setupTest.jsx +++ b/src/setupTest.jsx @@ -52,6 +52,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon Section: 'Card.Section', }, CardGrid: 'CardGrid', + Chip: 'Chip', Col: 'Col', Collapsible: { Advanced: 'Collapsible.Advanced', @@ -82,7 +83,10 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon RadioSet: 'Form.RadioSet', Switch: 'Form.Switch', }, + FormControl: 'FormControl', FormControlFeedback: 'FormControlFeedback', + FormGroup: 'FormGroup', + FormLabel: 'FormLabel', FullscreenModal: 'FullscreenModal', Hyperlink: 'Hyperlink', Icon: 'Icon', @@ -118,11 +122,18 @@ jest.mock('@edx/paragon/icons', () => ({ ArrowDropDown: jest.fn().mockName('icons.ArrowDropDown'), ArrowDropUp: jest.fn().mockName('icons.ArrowDropUp'), Cancel: jest.fn().mockName('icons.Cancel'), + Close: jest.fn().mockName('icons.Close'), + CheckCircle: jest.fn().mockName('icons.CheckCircle'), ChevronLeft: jest.fn().mockName('icons.ChevronLeft'), ChevronRight: jest.fn().mockName('icons.ChevronRight'), Highlight: jest.fn().mockName('icons.Highlight'), + Info: jest.fn().mockName('icons.Info'), InfoOutline: jest.fn().mockName('icons.InfoOutline'), Launch: jest.fn().mockName('icons.Launch'), + Locked: jest.fn().mockName('icons.Locked'), + MoreVert: jest.fn().mockName('icons.MoreVert'), + Tune: jest.fn().mockName('icons.Tune'), + Program: jest.fn().mockName('icons.Program'), })); jest.mock('data/constants/app', () => ({