feat: add recommendations modal (#189)

This commit is contained in:
Blue
2023-08-16 21:23:44 +05:00
committed by GitHub
22 changed files with 533 additions and 183 deletions

View File

@@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ModalView snapshot should renders default ModalView 1`] = `
<div
className="containers"
>
<ModalDialog
hasCloseButton={false}
isFullscreenScroll={true}
isOpen={true}
onClose={[MockFunction]}
title=""
>
<ModalDialog.Header>
<Component>
<h3
className="mt-2"
>
Thank you for your interest!
</h3>
</Component>
</ModalDialog.Header>
<ModalDialog.Body>
<div>
<p
className="mt-2"
>
Personalized recommendations feature is not yet available. We are working hard on bringing it to your learner home in the near future.
</p>
<p>
Would you like to be alerted when it becomes available?
</p>
</div>
</ModalDialog.Body>
<Component>
<ActionRow>
<Component
variant="tertiary"
>
Skip for now
</Component>
<Button
variant="primary"
>
Count me in!
</Button>
</ActionRow>
</Component>
</ModalDialog>
</div>
`;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { StrictDict } from 'utils';
import * as module from './hooks';
export const state = StrictDict({
isRecommendationsModalOpen: (val) => React.useState(val), // eslint-disable-line
});
export const useRecommendationsModal = () => {
const [isRecommendationsModalOpen, setIsRecommendationsModalOpen] = module.state.isRecommendationsModalOpen(false);
const toggleRecommendationsModal = () => setIsRecommendationsModalOpen(!isRecommendationsModalOpen);
return {
isRecommendationsModalOpen,
toggleRecommendationsModal,
};
};

View File

@@ -0,0 +1,21 @@
import { MockUseState } from 'testUtils';
import * as hooks from './hooks';
const state = new MockUseState(hooks);
const {
useRecommendationsModal,
} = hooks;
describe('LearnerDashboardHeader hooks', () => {
describe('useRecommendationsModal', () => {
test('default state', () => {
state.mock();
const out = useRecommendationsModal();
state.expectInitializedWith(state.keys.isRecommendationsModalOpen, false);
out.toggleRecommendationsModal();
expect(state.values.isRecommendationsModalOpen).toEqual(true);
});
});
});

View File

@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ModalDialog, ActionRow, Button } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
export const ModalView = ({
isOpen,
onClose,
}) => {
const { formatMessage } = useIntl();
return (
<div className="containers">
<ModalDialog
title=""
isOpen={isOpen}
onClose={onClose}
hasCloseButton={false}
isFullscreenScroll
>
<ModalDialog.Header>
<ModalDialog.Title>
<h3 className="mt-2">{formatMessage(messages.recommendationsModalHeading)}</h3>
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<div>
<p className="mt-2">{formatMessage(messages.recommendationsFeatureText)}</p>
<p>{formatMessage(messages.recommendationsAlertText)}</p>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{formatMessage(messages.modalSkipButton)}
</ModalDialog.CloseButton>
<Button variant="primary">{formatMessage(messages.modalCountMeButton)}</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
</div>
);
};
ModalView.defaultProps = {
isOpen: false,
};
ModalView.propTypes = {
onClose: PropTypes.func.isRequired,
isOpen: PropTypes.bool,
};
export default ModalView;

View File

@@ -0,0 +1,16 @@
import { shallow } from 'enzyme';
import ModalView from '.';
describe('ModalView', () => {
const props = {
isOpen: true,
onClose: jest.fn(),
};
describe('snapshot', () => {
test('should renders default ModalView', () => {
const wrapper = shallow(<ModalView {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,31 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
recommendationsFeatureText: {
id: 'RecommendationsPanel.recommendationsFeatureText',
defaultMessage: 'Personalized recommendations feature is not yet available. We are working hard on bringing it to your learner home in the near future.',
description: 'recommendations feature text',
},
recommendationsAlertText: {
id: 'RecommendationsPanel.recommendationsAlertText',
defaultMessage: 'Would you like to be alerted when it becomes available?',
description: 'recommendations alerted text',
},
recommendationsModalHeading: {
id: 'RecommendationsPanel.recommendationsModalHeading',
defaultMessage: 'Thank you for your interest!',
description: 'Heading of modal',
},
modalSkipButton: {
id: 'RecommendationsPanel.modalSkipButton',
defaultMessage: 'Skip for now',
description: 'button for Skip for now',
},
modalCountMeButton: {
id: 'RecommendationsPanel.modalCountMeButton',
defaultMessage: 'Count me in!',
description: 'button for Count me in!',
},
});
export default messages;

View File

@@ -12,9 +12,10 @@ import { reduxHooks } from 'hooks';
import { findCoursesNavDropdownClicked } from '../hooks';
import ModalView from '../../../components/ModalView';
import messages from '../messages';
export const CollapseMenuBody = ({ isOpen }) => {
export const CollapseMenuBody = ({ isOpen, isRecommendationsModalOpen, setIsRecommendationsModalOpen }) => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
@@ -40,6 +41,12 @@ export const CollapseMenuBody = ({ isOpen }) => {
>
{formatMessage(messages.discoverNew)}
</Button>
<Button
variant="inverse-primary"
onClick={setIsRecommendationsModalOpen}
>
{formatMessage(messages.recommendedForYou)}
</Button>
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
{formatMessage(messages.help)}
</Button>
@@ -92,6 +99,7 @@ export const CollapseMenuBody = ({ isOpen }) => {
</Button>
</>
)}
<ModalView isOpen={isRecommendationsModalOpen} onClose={setIsRecommendationsModalOpen} />
</div>
)
);
@@ -99,6 +107,13 @@ export const CollapseMenuBody = ({ isOpen }) => {
CollapseMenuBody.propTypes = {
isOpen: PropTypes.bool.isRequired,
isRecommendationsModalOpen: PropTypes.bool,
setIsRecommendationsModalOpen: PropTypes.func,
};
CollapseMenuBody.defaultProps = {
isRecommendationsModalOpen: false,
setIsRecommendationsModalOpen: () => {},
};
export default CollapseMenuBody;

View File

@@ -3,6 +3,13 @@ import { AppContext } from '@edx/frontend-platform/react';
import CollapseMenuBody from './CollapseMenuBody';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({
ORDER_HISTORY_URL: 'http://account-profile-url.test',
CAREER_LINK_URL: 'http://account-profile-url.test',
})),
}));
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
@@ -15,6 +22,7 @@ jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: () => ({
url: 'url',
dashboard: false,
}),
usePlatformSettingsData: () => ({
courseSearchUrl: '/courseSearchUrl',

View File

@@ -13,22 +13,27 @@ exports[`CollapseMenuBody render 1`] = `
</Button>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
href="undefined/dashboard/programs"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
href="undefined/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("undefined/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
onClick={[Function]}
variant="inverse-primary"
>
Recommended for you
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
@@ -42,25 +47,35 @@ exports[`CollapseMenuBody render 1`] = `
</Button>
<Button
as="a"
href="http://localhost:18000/u/username"
href="undefined/u/username"
variant="inverse-primary"
>
Profile
</Button>
<Button
as="a"
href="http://localhost:18000/account/settings"
href="undefined/account/settings"
variant="inverse-primary"
>
Account
</Button>
<Button
as="a"
href="http://localhost:18000/logout"
href="http://account-profile-url.test"
variant="inverse-primary"
>
Orders & Subscriptions
</Button>
<Button
as="a"
variant="inverse-primary"
>
Sign Out
</Button>
<ModalView
isOpen={false}
onClose={[Function]}
/>
</div>
`;
@@ -79,25 +94,34 @@ exports[`CollapseMenuBody render unauthenticated 1`] = `
</Button>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
href="undefined/dashboard/programs"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
href="undefined/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("undefined/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
onClick={[Function]}
variant="inverse-primary"
>
Recommended for you
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
<ModalView
isOpen={false}
onClose={[Function]}
/>
</div>
`;

View File

@@ -20,6 +20,8 @@ exports[`CollapsedHeader renders 1`] = `
</header>
<mockConstructor
isOpen={false}
isRecommendationsModalOpen={false}
setIsRecommendationsModalOpen={[MockFunction]}
/>
</Fragment>
`;
@@ -43,6 +45,8 @@ exports[`CollapsedHeader renders with isOpen true 1`] = `
</header>
<mockConstructor
isOpen={true}
isRecommendationsModalOpen={false}
setIsRecommendationsModalOpen={[MockFunction]}
/>
</Fragment>
`;

View File

@@ -5,6 +5,7 @@ import { Menu, Close } from '@edx/paragon/icons';
import { IconButton, Icon } from '@edx/paragon';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
import CollapseMenuBody from './CollapseMenuBody';
import BrandLogo from '../BrandLogo';
@@ -15,6 +16,7 @@ export const CollapsedHeader = () => {
const { formatMessage } = useIntl();
const isCollapsed = useIsCollapsed();
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderData();
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
return (
isCollapsed && (
@@ -36,7 +38,11 @@ export const CollapsedHeader = () => {
/>
<BrandLogo />
</header>
<CollapseMenuBody isOpen={isOpen} />
<CollapseMenuBody
isOpen={isOpen}
setIsRecommendationsModalOpen={toggleRecommendationsModal}
isRecommendationsModalOpen={isRecommendationsModalOpen}
/>
</>
)
);

View File

@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
import CollapsedHeader from '.';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
jest.mock('../BrandLogo', () => jest.fn(() => 'BrandLogo'));
jest.mock('./CollapseMenuBody', () => jest.fn(() => 'CollapseMenuBody'));
@@ -15,14 +16,26 @@ jest.mock('../hooks', () => ({
})),
}));
jest.mock('../../../components/ModalView/hooks', () => ({
useRecommendationsModal: jest.fn(),
}));
describe('CollapsedHeader', () => {
it('renders', () => {
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper).toMatchSnapshot();
});
it('render nothing if not collapsed', () => {
useIsCollapsed.mockReturnValueOnce(false);
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper).toMatchSnapshot();
});
@@ -32,6 +45,10 @@ describe('CollapsedHeader', () => {
isOpen: true,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
});
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper).toMatchSnapshot();
});

View File

@@ -33,6 +33,13 @@ exports[`ExpandedHeader render 1`] = `
>
Discover New
</Button>
<Button
className="p-4"
onClick={[MockFunction]}
variant="inverse-primary"
>
Recommended for you
</Button>
<span
className="flex-grow-1"
/>
@@ -46,6 +53,10 @@ exports[`ExpandedHeader render 1`] = `
</Button>
</div>
<AuthenticatedUserDropdown />
<ModalView
isOpen={false}
onClose={[MockFunction]}
/>
</header>
`;

View File

@@ -10,13 +10,16 @@ import { reduxHooks } from 'hooks';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
import messages from '../messages';
import ModalView from '../../../components/ModalView';
import BrandLogo from '../BrandLogo';
export const ExpandedHeader = () => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const isCollapsed = useIsCollapsed();
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
const exploreCoursesClick = findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
@@ -51,6 +54,13 @@ export const ExpandedHeader = () => {
>
{formatMessage(messages.discoverNew)}
</Button>
<Button
variant="inverse-primary"
className="p-4"
onClick={toggleRecommendationsModal}
>
{formatMessage(messages.recommendedForYou)}
</Button>
<span className="flex-grow-1" />
<Button
as="a"
@@ -63,6 +73,7 @@ export const ExpandedHeader = () => {
</div>
<AuthenticatedUserDropdown />
<ModalView isOpen={isRecommendationsModalOpen} onClose={toggleRecommendationsModal} />
</header>
)
);

View File

@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
import ExpandedHeader from '.';
import { useIsCollapsed } from '../hooks';
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
jest.mock('data/services/lms/urls', () => ({
programsUrl: 'programsUrl',
@@ -19,21 +20,34 @@ jest.mock('hooks', () => ({
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(),
useRecommendationsModal: jest.fn(),
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
}));
jest.mock('../../../components/ModalView/hooks', () => ({
useRecommendationsModal: jest.fn(),
}));
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
jest.mock('../BrandLogo', () => 'BrandLogo');
describe('ExpandedHeader', () => {
test('render', () => {
useIsCollapsed.mockReturnValueOnce(false);
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper).toMatchSnapshot();
});
test('render empty if collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true);
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);

View File

@@ -51,6 +51,11 @@ const messages = defineMessages({
defaultMessage: 'Discover New',
description: 'Header link for switching to discover page.',
},
recommendedForYou: {
id: 'learnerVariantDashboard.recommendedForYou',
defaultMessage: 'Recommended for you',
description: 'Header link for recommended page.',
},
logoAltText: {
id: 'learnerVariantDashboard.logoAltText',
defaultMessage: 'edX, Inc. Dashboard',

View File

@@ -1,51 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { Search } from '@edx/paragon/icons';
import { baseAppUrl } from 'data/services/lms/urls';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import track from './track';
import CourseCard from './components/CourseCard';
import messages from './messages';
import ModalView from '../../components/ModalView';
import './index.scss';
export const LoadedView = ({
courses,
isControl,
setIsRecommendationsModalOpen,
isRecommendationsModalOpen,
}) => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
return (
<div className="p-4 w-100 panel-background">
<h3 className="pb-2">{isControl === false
? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)}
</h3>
<div>
{courses.map((course) => (
<CourseCard
key={course.courseKey}
course={course}
isControl={isControl}
/>
))}
<>
<div className="p-4 w-100 panel-background">
<h3 className="pb-2">{isControl === false
? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)}
</h3>
<div>
{courses.map((course) => (
<CourseCard
key={course.courseKey}
course={course}
isControl={isControl}
/>
))}
</div>
<div className="text-center explore-courses-btn">
<Button
variant="brand"
onClick={setIsRecommendationsModalOpen}
>
{formatMessage(messages.seeAllRecommendationsButton)}
</Button>
</div>
</div>
<div className="text-center explore-courses-btn">
<Button
variant="tertiary"
iconBefore={Search}
as="a"
href={baseAppUrl(courseSearchUrl)}
onClick={track.findCoursesWidgetClicked(baseAppUrl(courseSearchUrl))}
>
{formatMessage(messages.exploreCoursesButton)}
</Button>
</div>
</div>
<ModalView isOpen={isRecommendationsModalOpen} onClose={setIsRecommendationsModalOpen} />
</>
);
};
@@ -61,6 +59,8 @@ LoadedView.propTypes = {
marketingUrl: PropTypes.string,
})).isRequired,
isControl: PropTypes.oneOf([true, false, null]),
setIsRecommendationsModalOpen: PropTypes.func.isRequired,
isRecommendationsModalOpen: PropTypes.bool.isRequired,
};
export default LoadedView;

View File

@@ -24,6 +24,8 @@ describe('RecommendationsPanel LoadedView', () => {
const props = {
courses: mockData.courses,
isControl: null,
setIsRecommendationsModalOpen: () => {},
isRecommendationsModalOpen: false,
};
describe('snapshot', () => {
test('without personalize recommendation', () => {

View File

@@ -1,151 +1,157 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RecommendationsPanel LoadedView snapshot with personalize recommendation 1`] = `
<div
className="p-4 w-100 panel-background"
>
<h3
className="pb-2"
>
Recommendations for you
</h3>
<div>
<CourseCard
course={
Object {
"courseKey": "cs-1",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 1",
}
}
isControl={false}
key="cs-1"
/>
<CourseCard
course={
Object {
"courseKey": "cs-2",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 2 with a really really really long name for some reason",
}
}
isControl={false}
key="cs-2"
/>
<CourseCard
course={
Object {
"courseKey": "cs-3",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 3",
}
}
isControl={false}
key="cs-3"
/>
<CourseCard
course={
Object {
"courseKey": "cs-4",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 4",
}
}
isControl={false}
key="cs-4"
/>
</div>
<Fragment>
<div
className="text-center explore-courses-btn"
className="p-4 w-100 panel-background"
>
<Button
as="a"
href="http://localhost:18000/course-search-url"
iconBefore={[MockFunction icons.Search]}
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
variant="tertiary"
<h3
className="pb-2"
>
Explore courses
</Button>
Recommendations for you
</h3>
<div>
<CourseCard
course={
Object {
"courseKey": "cs-1",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 1",
}
}
isControl={false}
key="cs-1"
/>
<CourseCard
course={
Object {
"courseKey": "cs-2",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 2 with a really really really long name for some reason",
}
}
isControl={false}
key="cs-2"
/>
<CourseCard
course={
Object {
"courseKey": "cs-3",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 3",
}
}
isControl={false}
key="cs-3"
/>
<CourseCard
course={
Object {
"courseKey": "cs-4",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 4",
}
}
isControl={false}
key="cs-4"
/>
</div>
<div
className="text-center explore-courses-btn"
>
<Button
onClick={[Function]}
variant="brand"
>
See All Recommendations
</Button>
</div>
</div>
</div>
<ModalView
isOpen={false}
onClose={[Function]}
/>
</Fragment>
`;
exports[`RecommendationsPanel LoadedView snapshot without personalize recommendation 1`] = `
<div
className="p-4 w-100 panel-background"
>
<h3
className="pb-2"
>
Popular courses
</h3>
<div>
<CourseCard
course={
Object {
"courseKey": "cs-1",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 1",
}
}
isControl={null}
key="cs-1"
/>
<CourseCard
course={
Object {
"courseKey": "cs-2",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 2 with a really really really long name for some reason",
}
}
isControl={null}
key="cs-2"
/>
<CourseCard
course={
Object {
"courseKey": "cs-3",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 3",
}
}
isControl={null}
key="cs-3"
/>
<CourseCard
course={
Object {
"courseKey": "cs-4",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 4",
}
}
isControl={null}
key="cs-4"
/>
</div>
<Fragment>
<div
className="text-center explore-courses-btn"
className="p-4 w-100 panel-background"
>
<Button
as="a"
href="http://localhost:18000/course-search-url"
iconBefore={[MockFunction icons.Search]}
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
variant="tertiary"
<h3
className="pb-2"
>
Explore courses
</Button>
Popular courses
</h3>
<div>
<CourseCard
course={
Object {
"courseKey": "cs-1",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 1",
}
}
isControl={null}
key="cs-1"
/>
<CourseCard
course={
Object {
"courseKey": "cs-2",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 2 with a really really really long name for some reason",
}
}
isControl={null}
key="cs-2"
/>
<CourseCard
course={
Object {
"courseKey": "cs-3",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 3",
}
}
isControl={null}
key="cs-3"
/>
<CourseCard
course={
Object {
"courseKey": "cs-4",
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
"marketingUrl": "www.edx/recommended-course",
"title": "Recommended course 4",
}
}
isControl={null}
key="cs-4"
/>
</div>
<div
className="text-center explore-courses-btn"
>
<Button
onClick={[Function]}
variant="brand"
>
See All Recommendations
</Button>
</div>
</div>
</div>
<ModalView
isOpen={false}
onClose={[Function]}
/>
</Fragment>
`;

View File

@@ -4,6 +4,7 @@ import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
import LoadingView from './LoadingView';
import LoadedView from './LoadedView';
import hooks from './hooks';
import { useRecommendationsModal } from '../../components/ModalView/hooks';
export const RecommendationsPanel = () => {
const {
@@ -14,12 +15,19 @@ export const RecommendationsPanel = () => {
isLoading,
} = hooks.useRecommendationPanelData();
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
if (isLoading) {
return (<LoadingView />);
}
if (isLoaded && courses.length > 0) {
return (
<LoadedView courses={courses} isControl={isControl} />
<LoadedView
courses={courses}
isControl={isControl}
setIsRecommendationsModalOpen={toggleRecommendationsModal}
isRecommendationsModalOpen={isRecommendationsModalOpen}
/>
);
}
if (isFailed) {

View File

@@ -7,10 +7,14 @@ import mockData from './mockData';
import LoadedView from './LoadedView';
import LoadingView from './LoadingView';
import RecommendationsPanel from '.';
import { useRecommendationsModal } from '../../components/ModalView/hooks';
jest.mock('./hooks', () => ({
useRecommendationPanelData: jest.fn(),
}));
jest.mock('../../components/ModalView/hooks', () => ({
useRecommendationsModal: jest.fn(),
}));
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('./LoadingView', () => 'LoadingView');
jest.mock('./LoadedView', () => 'LoadedView');
@@ -21,6 +25,8 @@ describe('RecommendationsPanel snapshot', () => {
const defaultLoadedViewProps = {
courses: [],
isControl: false,
setIsRecommendationsModalOpen: jest.fn(),
isRecommendationsModalOpen: false,
};
const defaultValues = {
isFailed: false,
@@ -29,6 +35,10 @@ describe('RecommendationsPanel snapshot', () => {
...defaultLoadedViewProps,
};
it('displays LoadingView if request is loading', () => {
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
isLoading: true,
@@ -36,16 +46,23 @@ describe('RecommendationsPanel snapshot', () => {
expect(shallow(<RecommendationsPanel />)).toMatchObject(shallow(<LoadingView />));
});
it('displays LoadedView with courses if request is loaded', () => {
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
courses,
isLoaded: true,
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(
shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />),
);
expect(JSON.stringify(shallow(<RecommendationsPanel />)))
.toEqual(JSON.stringify(shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />)));
});
it('displays LookingForChallengeWidget if request is failed', () => {
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
isFailed: true,
@@ -55,6 +72,10 @@ describe('RecommendationsPanel snapshot', () => {
);
});
it('defaults to LookingForChallengeWidget if no flags are true', () => {
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
});

View File

@@ -16,6 +16,11 @@ const messages = defineMessages({
defaultMessage: 'Explore courses',
description: 'Button to explore more courses on recommendations panel',
},
seeAllRecommendationsButton: {
id: 'RecommendationsPanel.seeAllRecommendationsButton',
defaultMessage: 'See All Recommendations',
description: 'Button to see all recommendations',
},
});
export default messages;