feat: update static recommendation placement (#1002)

* feat: update static recommendation placement
* refactor: clean recommendation code (#1003)

---------

Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
This commit is contained in:
Zainab Amir
2023-07-27 15:04:56 +05:00
committed by GitHub
parent bfa7874108
commit 21e6bb6eec
11 changed files with 147 additions and 634 deletions

View File

@@ -1,72 +1,27 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import { Launch } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import { isEdxProgram } from '../../data/utils';
import {
cardFooterMessages,
externalLinkIconMessages,
} from '../../messages';
const ProductCardFooter = ({
factoid,
quickFacts,
courseLength,
footerMessage,
cardType,
is2UDegreeProgram,
isSubscriptionView,
}) => {
const intl = useIntl();
const courseLengthLabel = courseLength > 1 ? 'Courses' : 'Course';
if (isSubscriptionView) {
return (
<p className="d-inline-block x-small">
{intl.formatMessage(
cardFooterMessages[
'recommendation.2u-product-card.footer-text.number-of-courses'
],
{ length: courseLength, label: courseLengthLabel },
)}
<span className="p-2"></span>
<p className="d-inline-block">
{intl.formatMessage(
cardFooterMessages[
'recommendation.2u-product-card.footer-text.subscription'
],
)}
</p>
</p>
);
}
if (footerMessage) {
return (
<div className="footer-message d-flex align-items-center">
<p className="x-small">{footerMessage}</p>
<Icon
src={Launch}
className="ml-1 footer-icon"
screenReaderText={intl.formatMessage(
externalLinkIconMessages[
'recommendation.2u-product-card.launch-icon.sr-text'
],
)}
/>
</div>
);
}
if (courseLength) {
return (
<p className="x-small">
{intl.formatMessage(
cardFooterMessages[
'recommendation.2u-product-card.footer-text.number-of-courses'
'recommendation.product-card.footer-text.number-of-courses'
],
{ length: courseLength, label: courseLengthLabel },
)}
@@ -74,7 +29,7 @@ const ProductCardFooter = ({
);
}
if (isEdxProgram({ cardType, is2UDegreeProgram })) {
if (cardType === 'program') {
if (quickFacts && quickFacts.length > 0) {
const quickFactsCount = quickFacts.length;
@@ -106,21 +61,15 @@ const ProductCardFooter = ({
ProductCardFooter.propTypes = {
cardType: PropTypes.string,
factoid: PropTypes.string,
footerMessage: PropTypes.string,
quickFacts: PropTypes.arrayOf(PropTypes.shape({})),
courseLength: PropTypes.number,
is2UDegreeProgram: PropTypes.bool,
isSubscriptionView: PropTypes.bool,
};
ProductCardFooter.defaultProps = {
cardType: '',
factoid: '',
footerMessage: '',
quickFacts: [],
courseLength: undefined,
is2UDegreeProgram: false,
isSubscriptionView: false,
};
export default ProductCardFooter;

View File

@@ -5,11 +5,9 @@ import PropTypes from 'prop-types';
import BaseCard from './BaseCard';
import Footer from './Footer';
import { EXTERNAL_PRODUCT_SOURCES } from '../data/constants';
import { createCodeFriendlyProduct, getVariant, useProductType } from '../data/utils';
import {
cardBadgesMessages,
cardFooterMessages,
} from '../messages';
import { trackRecommendationCardClickOptimizely } from '../optimizelyExperiment';
import { trackRecommendationsClicked } from '../track';
@@ -27,15 +25,6 @@ const ProductCard = ({
const headerImage = product?.cardImageUrl || product?.image?.src;
const footerMessagesObj = {
[EXTERNAL_PRODUCT_SOURCES.EMERITUS]: formatMessage(
cardFooterMessages['recommendation.2u-product-card.footer-text.emeritus'],
),
[EXTERNAL_PRODUCT_SOURCES.SHORELIGHT]: formatMessage(
cardFooterMessages['recommendation.2u-product-card.footer-text.shorelight'],
),
};
const schoolName = product?.organizationShortCodeOverride
|| product?.owners?.[0]?.name
|| product?.authoringOrganizations?.[0]?.name
@@ -71,7 +60,7 @@ const ProductCard = ({
const productTypeCopy = formatMessage(
cardBadgesMessages[
`recommendation.2u-product-card.pill-text.${createCodeFriendlyProduct(productType)}`
`recommendation.product-card.pill-text.${createCodeFriendlyProduct(productType)}`
],
);
const handleCardClick = () => {
@@ -100,7 +89,6 @@ const ProductCard = ({
variant={variant}
footer={(
<Footer
footerMessage={footerMessagesObj?.[product.productSource?.slug]}
quickFacts={product.degree?.quickFacts}
externalUrl={product.additionalMetadata?.externalUrl
|| product.degree?.additionalMetadata?.externalUrl}

View File

@@ -1,79 +1,32 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Container, Dropdown, DropdownButton } from '@edx/paragon';
import PropTypes from 'prop-types';
import { RECOMMENDATIONS_OPTION_LIST } from './data/constants';
import messages from './messages';
import ProductCard from './ProductCard';
const RecommendationsList = (props) => {
const { formatMessage } = useIntl();
const {
title, recommendations, userId, setSelectedRecommendationsType, selectedRecommendationsType,
} = props;
const { recommendations, userId } = props;
return (
<Container id="course-recommendations" size="lg" className="recommendations-container">
<h2 className="text-sm-center mb-4 text-left recommendations-container__heading">
{title}
</h2>
<DropdownButton
id="dropdown-basic-button"
title={(
<>
{formatMessage(messages[`recommendation.option.${selectedRecommendationsType.value}`])}
</>
)}
className="bg-white mt-5.5 mb-3"
>
{RECOMMENDATIONS_OPTION_LIST.map((option) => (
<Dropdown.Item
onClick={() => setSelectedRecommendationsType(option)}
id={`option-${option.value}`}
key={`option-${option.value}`}
>
{formatMessage(messages[`recommendation.option.${option.value}`])}
</Dropdown.Item>
))}
</DropdownButton>
<div className="d-flex recommendations-container__card-list">
{
recommendations.map((recommendation, idx) => (
<div className="d-flex recommendations-container__card-list">
{
recommendations.map((recommendation, idx) => (
<span key={recommendation.uuid}>
<ProductCard
key={recommendation.activeRunKey}
product={recommendation}
position={idx}
userId={userId}
/>
))
}
</div>
</Container>
</span>
))
}
</div>
);
};
RecommendationsList.propTypes = {
title: PropTypes.string.isRequired,
setSelectedRecommendationsType: PropTypes.func.isRequired,
selectedRecommendationsType: PropTypes.shape({
title: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
}).isRequired,
recommendations: PropTypes.arrayOf(PropTypes.shape({
courseKey: PropTypes.string.isRequired,
activeRunKey: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
cardImageUrl: PropTypes.string.isRequired,
owners: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
logoImageUrl: PropTypes.string.isRequired,
})),
marketingUrl: PropTypes.string.isRequired,
recommendationType: PropTypes.string,
uuid: PropTypes.string,
})),
userId: PropTypes.number,
};

View File

@@ -1,38 +1,26 @@
import React, { useState } from 'react';
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Hyperlink, Image, StatefulButton,
Container, Hyperlink, Image, StatefulButton, Tab, Tabs,
} from '@edx/paragon';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { RECOMMENDATIONS_COUNT, RECOMMENDATIONS_OPTION_LIST } from './data/constants';
import messages from './messages';
import RecommendationsList from './RecommendationsList';
import { DEFAULT_REDIRECT_URL } from '../data/constants';
const RecommendationsPage = (props) => {
const { location } = props;
const RecommendationsPage = ({ location }) => {
const { formatMessage } = useIntl();
const registrationResponse = location.state?.registrationResult;
const userId = location.state?.userId;
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
const POPULAR_PRODUCTS = JSON.parse(getConfig().POPULAR_PRODUCTS);
const TRENDING_PRODUCTS = JSON.parse(getConfig().TRENDING_PRODUCTS);
const { formatMessage } = useIntl();
const [selectedRecommendationsType, setSelectedRecommendationsType] = useState(RECOMMENDATIONS_OPTION_LIST[1]);
const [recommendations, setRecommendations] = useState(POPULAR_PRODUCTS);
const handleRecommendationType = (option) => {
setSelectedRecommendationsType(option);
setRecommendations(option.value === 'popular' ? POPULAR_PRODUCTS : TRENDING_PRODUCTS);
};
if (!registrationResponse) {
global.location.assign(DASHBOARD_URL);
return null;
}
const handleRedirection = () => {
window.history.replaceState(location.state, null, '');
@@ -43,15 +31,20 @@ const RecommendationsPage = (props) => {
}
};
if (recommendations.length < RECOMMENDATIONS_COUNT) {
handleRedirection();
}
const handleSkip = (e) => {
e.preventDefault();
handleRedirection();
};
if (!registrationResponse) {
window.location.href = DASHBOARD_URL;
return null;
}
if (!POPULAR_PRODUCTS.length || !TRENDING_PRODUCTS.length) {
handleRedirection();
}
return (
<>
<Helmet>
@@ -66,28 +59,42 @@ const RecommendationsPage = (props) => {
<Image className="logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
</Hyperlink>
</div>
{(recommendations) && (
<div className="d-flex flex-column align-items-center justify-content-center flex-grow-1 p-1">
<RecommendationsList
title={formatMessage(messages['recommendation.page.heading'])}
recommendations={recommendations}
userId={userId}
setSelectedRecommendationsType={handleRecommendationType}
selectedRecommendationsType={selectedRecommendationsType}
<div className="d-flex flex-column align-items-center justify-content-center flex-grow-1 p-1">
<Container id="course-recommendations" size="lg" className="recommendations-container">
<h2 className="text-sm-center mb-4 text-left recommendations-container__heading">
{formatMessage(messages['recommendation.page.heading'])}
</h2>
<Tabs
variant="tabs"
defaultActiveKey="popular"
id="recommendations-selection"
>
<Tab tabClassName="mb-3" eventKey="popular" title={formatMessage(messages['recommendation.option.popular'])}>
<RecommendationsList
recommendations={POPULAR_PRODUCTS}
userId={userId}
/>
</Tab>
<Tab tabClassName="mb-3" eventKey="trending" title={formatMessage(messages['recommendation.option.trending'])}>
<RecommendationsList
recommendations={TRENDING_PRODUCTS}
userId={userId}
/>
</Tab>
</Tabs>
</Container>
<div className="text-center">
<StatefulButton
className="font-weight-500"
type="submit"
variant="brand"
labels={{
default: formatMessage(messages['recommendation.skip.button']),
}}
onClick={handleSkip}
/>
<div className="text-center">
<StatefulButton
className="font-weight-500"
type="submit"
variant="brand"
labels={{
default: formatMessage(messages['recommendation.skip.button']),
}}
onClick={handleSkip}
/>
</div>
</div>
)}
</div>
</div>
</>
);
@@ -100,7 +107,6 @@ RecommendationsPage.propTypes = {
redirectUrl: PropTypes.string,
}),
userId: PropTypes.number,
educationLevel: PropTypes.string,
}),
}),

View File

@@ -9,10 +9,3 @@ export const EDUCATION_LEVEL_MAPPING = {
hs: 'Introductory',
jhs: 'Introductory',
};
export const EXTERNAL_PRODUCT_SOURCES = {
EMERITUS: 'emeritus',
SHORELIGHT: 'shorelight',
};
export const RECOMMENDATIONS_OPTION_LIST = [{ title: 'Trending courses', value: 'trending' }, { title: 'Popular courses', value: 'popular' }];

View File

@@ -20,24 +20,10 @@ const courseTypeToProductTypeMap = {
'credit-verified-audit': 'Course',
'spoc-verified-audit': 'Course',
professional: 'Professional Certificate',
'bootcamp-2u': 'Boot Camp',
'executive-education-2u': 'Executive Education',
'executive-education': 'Executive Education',
masters: "Master's",
'masters-verified-audit': "Master's",
};
const programTypeToProductTypeMap = {
xseries: 'XSeries',
micromasters: 'MicroMasters',
microbachelors: 'MicroBachelors',
'professional certificate': 'Professional Certificate',
"bachelor's": "Bachelor's",
bachelors: "Bachelor's",
"master's": "Master's",
masters: "Master's",
doctorate: 'Doctorate',
license: 'License',
certificate: 'Certificate',
};
@@ -61,8 +47,6 @@ export const getVariant = (productType) => (
export const createCodeFriendlyProduct = (type) => type?.replace(/\s+/g, '-').replace(/'/g, '').toLowerCase();
export const isEdxProgram = ({ cardType, is2UDegreeProgram }) => cardType === 'program' && !is2UDegreeProgram;
export const truncateText = (input) => (input?.length > 50 ? `${input.substring(0, 50)}...` : input);
export default convertCourseRunKeytoCourseKey;

View File

@@ -18,106 +18,55 @@ const messages = defineMessages({
},
'recommendation.option.trending': {
id: 'recommendation.option.trending',
defaultMessage: 'Trending Courses',
description: 'Trending courses option',
defaultMessage: 'Trending',
description: 'Title for trending products',
},
'recommendation.option.popular': {
id: 'recommendation.option.popular',
defaultMessage: 'Popular Courses',
description: 'Popular courses option',
defaultMessage: 'Most Popular',
description: 'Title for popular products',
},
});
export const cardBadgesMessages = defineMessages({
'recommendation.2u-product-card.pill-text.course': {
id: 'recommendation.2u-product-card.pill-text.course',
'recommendation.product-card.pill-text.course': {
id: 'recommendation.product-card.pill-text.course',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Course',
},
'recommendation.2u-product-card.pill-text.microbachelors': {
id: 'recommendation.2u-product-card.pill-text.microbachelors',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'MicroBachelors®',
},
'recommendation.2u-product-card.pill-text.micromasters': {
id: 'recommendation.2u-product-card.pill-text.micromasters',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'MicroMasters®',
},
'recommendation.2u-product-card.pill-text.xseries': {
id: 'recommendation.2u-product-card.pill-text.xseries',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'XSeries',
},
'recommendation.2u-product-card.pill-text.professional-certificate': {
id: 'recommendation.2u-product-card.pill-text.professional-certificate',
'recommendation.product-card.pill-text.professional-certificate': {
id: 'recommendation.product-card.pill-text.professional-certificate',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Professional Certificate',
},
// 2U Products
'recommendation.2u-product-card.pill-text.executive-education': {
id: 'recommendation.2u-product-card.pill-text.executive-education',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Executive Education',
},
'recommendation.2u-product-card.pill-text.boot-camp': {
id: 'recommendation.2u-product-card.pill-text.boot-camp',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Boot Camp',
},
'recommendation.2u-product-card.pill-text.bachelors': {
id: 'recommendation.2u-product-card.pill-text.bachelors',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Bachelor\'s Degree',
},
'recommendation.2u-product-card.pill-text.masters': {
id: 'recommendation.2u-product-card.pill-text.masters',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Master\'s Degree',
},
'recommendation.2u-product-card.pill-text.doctorate': {
id: 'recommendation.2u-product-card.pill-text.doctorate',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Doctoral Program',
},
'recommendation.2u-product-card.pill-text.certificate': {
id: 'recommendation.2u-product-card.pill-text.certificate',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Certificate Program',
},
'recommendation.2u-product-card.pill-text.license': {
id: 'recommendation.2u-product-card.pill-text.license',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Licensure Program',
},
});
export const cardFooterMessages = defineMessages({
'recommendation.2u-product-card.footer-text.emeritus': {
id: 'recommendation.2u-product-card.pill-text.emeritus',
'recommendation.product-card.footer-text.emeritus': {
id: 'recommendation.product-card.pill-text.emeritus',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Offered on Emeritus',
},
'recommendation.2u-product-card.footer-text.shorelight': {
id: 'recommendation.2u-product-card.pill-text.shorelight',
'recommendation.product-card.footer-text.shorelight': {
id: 'recommendation.product-card.pill-text.shorelight',
description: 'Text on a product card that describes which product line this item belongs to',
defaultMessage: 'Offered through Shorelight',
},
'recommendation.2u-product-card.footer-text.number-of-courses': {
id: 'recommendation.2u-product-card.footer-text.number-of-courses',
'recommendation.product-card.footer-text.number-of-courses': {
id: 'recommendation.product-card.footer-text.number-of-courses',
description: 'Label in card footer that shows how many courses are in a program',
defaultMessage: '{length} {label}',
},
'recommendation.2u-product-card.footer-text.subscription': {
id: 'recommendation.2u-product-card.footer-text.subscription',
'recommendation.product-card.footer-text.subscription': {
id: 'recommendation.product-card.footer-text.subscription',
description: 'Label in card footer that describes that it is a subscription program',
defaultMessage: 'Subscription',
},
});
export const externalLinkIconMessages = defineMessages({
'recommendation.2u-product-card.launch-icon.sr-text': {
id: 'recommendation.2u-product-card.launch-icon.sr-text',
'recommendation.product-card.launch-icon.sr-text': {
id: 'recommendation.product-card.launch-icon.sr-text',
description: 'Screen reader text for the launch icon on the cards',
defaultMessage: 'Opens a link in a new tab',
},

View File

@@ -1,140 +1,42 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import configureStore from 'redux-mock-store';
import mockedCoursesData from './mockedData';
import { trackRecommendationCardClickOptimizely } from '../optimizelyExperiment';
import mockedProductData from './mockedData';
import RecommendationList from '../RecommendationsList';
const IntlRecommendationList = injectIntl(RecommendationList);
const mockStore = configureStore();
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
jest.mock('../data/service', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('../optimizelyExperiment', () => ({
trackRecommendationCardClickOptimizely: jest.fn(),
}));
describe('RecommendationsListTests', () => {
mergeConfig({
GENERAL_RECOMMENDATIONS: '[]',
});
let defaultProps = {};
let store = {};
const registrationResult = {
redirectUrl: getConfig().LMS_BASE_URL.concat('/course-about-page-url'),
success: true,
};
const store = mockStore({});
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
const getRecommendationsList = async (props = defaultProps) => {
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationList {...props} />));
await act(async () => {
await Promise.resolve(recommendationsPage);
recommendationsPage.update();
});
return recommendationsPage;
};
beforeEach(() => {
store = mockStore({});
defaultProps = {
location: {
state: {
registrationResult,
userId: 111,
},
},
it('should render the product card', () => {
const props = {
recommendations: mockedProductData,
userId: 1234567,
};
const recommendationsList = mount(reduxWrapper(<IntlRecommendationList {...props} />));
expect(recommendationsList.find('.recommendation-card').length).toEqual(mockedProductData.length);
});
it('should render the product card with formatted message in card', async () => {
it('should render the recommendations card with footer content', () => {
const props = {
recommendations: [mockedCoursesData[4]],
title: 'We have a few recommendations to get you started.',
recommendations: mockedProductData,
userId: 1234567,
setSelectedRecommendationsType: jest.fn(),
selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
};
const recommendationsList = await getRecommendationsList(props);
expect(recommendationsList.find('.x-small').at(0).text()).toEqual('Offered on Emeritus');
});
it('should call trackRecommendationCardClickOptimizely when card is clicked', async () => {
const props = {
recommendations: [mockedCoursesData[1]],
title: 'We have a few recommendations to get you started.',
userId: 1234567,
setSelectedRecommendationsType: jest.fn(),
selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
};
const recommendationsList = await getRecommendationsList(props);
recommendationsList.find('.card-box').first().simulate('click');
expect(trackRecommendationCardClickOptimizely).toHaveBeenCalledTimes(1);
});
it('should render the recommendations card with with facets', async () => {
const props = {
recommendations: [mockedCoursesData[1]],
title: 'We have a few recommendations to get you started.',
userId: 1234567,
setSelectedRecommendationsType: jest.fn(),
selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
};
const recommendationsList = await getRecommendationsList(props);
expect(recommendationsList.find('.d-inline-block').at(0).text()).toEqual('6 Modules');
});
it('should render the recommendations card with footer content when subscription view is enabled', async () => {
const props = {
recommendations: [mockedCoursesData[3]],
title: 'We have a few recommendations to get you started.',
userId: 1234567,
setSelectedRecommendationsType: jest.fn(),
selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
};
const recommendationsList = await getRecommendationsList(props);
expect(recommendationsList.find('.d-inline-block').at(1).text()).toEqual('Subscription');
});
it('should render the recommendations card with footer content', async () => {
const props = {
recommendations: [mockedCoursesData[0]],
title: 'We have a few recommendations to get you started.',
userId: 1234567,
setSelectedRecommendationsType: jest.fn(),
selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
};
const recommendationsList = await getRecommendationsList(props);
expect(recommendationsList.find('.x-small').text()).toEqual('4 Courses');
});
it('should render the recommendations list', async () => {
const props = {
recommendations: mockedCoursesData,
title: 'We have a few recommendations to get you started.',
userId: 1234567,
setSelectedRecommendationsType: jest.fn(),
selectedRecommendationsType: { title: 'Trending courses', value: 'trending' },
};
const recommendationsList = await getRecommendationsList(props);
expect(recommendationsList.find('.recommendation-card').length).toBe(5);
const recommendationsList = mount(reduxWrapper(<IntlRecommendationList {...props} />));
expect(recommendationsList.find('.x-small').at(0).text()).toEqual('1 Course');
expect(recommendationsList.find('.x-small').at(1).text()).toEqual('2 Courses');
});
});

View File

@@ -4,11 +4,9 @@ import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import configureStore from 'redux-mock-store';
import { DEFAULT_REDIRECT_URL } from '../../data/constants';
import getPersonalizedRecommendations from '../data/service';
import RecommendationsPage from '../RecommendationsPage';
const IntlRecommendationsPage = injectIntl(RecommendationsPage);
@@ -32,8 +30,11 @@ describe('RecommendationsPageTests', () => {
let defaultProps = {};
let store = {};
let registrationResult = {
redirectUrl: getConfig().LMS_BASE_URL.concat('/course-about-page-url'),
const dashboardUrl = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
const redirectUrl = getConfig().LMS_BASE_URL.concat('/course-about-page-url');
const registrationResult = {
redirectUrl,
success: true,
};
const reduxWrapper = children => (
@@ -42,16 +43,6 @@ describe('RecommendationsPageTests', () => {
</IntlProvider>
);
const getRecommendationsPage = async (props = defaultProps) => {
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage {...props} />));
await act(async () => {
await Promise.resolve(recommendationsPage);
recommendationsPage.update();
});
return recommendationsPage;
};
beforeEach(() => {
store = mockStore({});
defaultProps = {
@@ -64,53 +55,25 @@ describe('RecommendationsPageTests', () => {
};
});
it('redirects to dashboard if user click on skip button', async () => {
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
registrationResult = {
...registrationResult,
redirectUrl: getConfig().LMS_BASE_URL.concat('/dashboard'),
};
const props = {
location: {
state: {
registrationResult,
userId: 111,
},
},
};
const recommendationsPage = await getRecommendationsPage(props);
it('should redirect to dashboard if user is not coming from registration workflow', () => {
mount(reduxWrapper(<IntlRecommendationsPage />));
expect(window.location.href).toEqual(dashboardUrl);
});
it('should redirect if either popular or trending recommendations are not configured', () => {
mount(reduxWrapper(<IntlRecommendationsPage {...defaultProps} />));
expect(window.location.href).toEqual(redirectUrl);
});
it('should redirect user if they click "Skip for now" button', () => {
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage {...defaultProps} />));
recommendationsPage.find('.pgn__stateful-btn-state-default').first().simulate('click');
expect(window.location.href).toEqual(DASHBOARD_URL);
expect(window.location.href).toEqual(redirectUrl);
});
it('should change the option when click the dropdown option', async () => {
const recommendationsPage = await getRecommendationsPage();
recommendationsPage.find('#dropdown-basic-button').at(1).simulate('click');
recommendationsPage.find('.dropdown-item').at(0).simulate('click',
{ target: { title: 'Trending courses', value: 'trending' } });
expect(recommendationsPage.find('#dropdown-basic-button').at(1).text()).toEqual('Trending Courses');
});
it('should redirect if recommended courses count is less than RECOMMENDATIONS_COUNT', async () => {
delete window.location;
window.location = { assign: jest.fn() };
const recommendationsPage = await getRecommendationsPage();
expect(recommendationsPage.find('#course-recommendations').exists()).toBeTruthy();
});
it('redirects to dashboard if user tries to access the page directly', async () => {
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
delete window.location;
window.location = {
href: getConfig().BASE_URL,
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
};
await getRecommendationsPage({});
expect(getPersonalizedRecommendations).toHaveBeenCalledTimes(0);
expect(window.location.href).toEqual(DASHBOARD_URL);
it('displays popular products as default recommendations', () => {
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage {...defaultProps} />));
expect(recommendationsPage.find('.nav-link .active a').text()).toEqual('Most Popular');
});
});

View File

@@ -1,241 +1,80 @@
const mockedCoursesData = [
const mockedProductData = [
{
uuid: '1fcffef0-468a-483f-95aa-87e2d4d2c408',
title: 'Google Cloud Computing Foundations',
subtitle: 'The Google Cloud Computing Foundations courses provide an overview of concepts central to cloud basics, big data, and machine learning, and where and how Google Cloud fits in.',
cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/card_images/1fcffef0-468a-483f-95aa-87e2d4d2c408-fc006d5bbc06.png',
uuid: 'test-uuid-1',
title: 'How to Learn Online 1',
subtitle: 'Org 1',
cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-1.png',
authoringOrganizations: [
{
key: 'GoogleCloud',
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/7a30f2c7-0d59-4890-ab19-fc8324e9c7d6-6ff36c37bf48.png',
name: 'Google Cloud',
key: 'org-1',
logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-1.png',
name: 'Org 1',
},
],
courses: [
{
course: {
title: 'Google Cloud Computing Foundations: Cloud Computing Fundamentals',
title: 'How to learn online course 1',
topics: [],
},
},
{
course: null,
},
{
course: null,
},
{
course: null,
},
],
type: 'Professional Certificate',
marketingPath: '/professional-certificate/google-cloud-computing-foundations',
url: '/test-professional-certificate/how-to-learn-online-1',
organizationLogoOverrideUrl: null,
organizationShortCodeOverride: '',
productSource: {
name: 'edX',
slug: 'edx',
description: '',
name: 'company X',
slug: 'companyX',
},
status: 'active',
hidden: false,
inProspectus: true,
is2UDegreeProgram: false,
degree: null,
locationRestriction: null,
subscriptionEligible: null,
objectID: 'program-1fcffef0-468a-483f-95aa-87e2d4d2c408',
cardType: 'program',
cardIndex: 0,
},
{
uuid: 'ff89bff4-42c4-4728-ae8b-7e9276ebfcac',
title: 'Online Masters Degree in Public Health',
subtitle: 'from Boston University',
cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/card_images/ff89bff4-42c4-4728-ae8b-7e9276ebfcac-18ae533b0500.jpeg',
uuid: 'test-uuid-2',
title: 'How to Learn Online 2',
subtitle: 'Org 2',
cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-2.png',
authoringOrganizations: [
{
key: 'BUx',
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/36cfd0bb-1d18-4355-ae44-cb946573df3c-1e18515c3e4b.png',
name: 'Boston University',
},
],
courses: [],
type: 'Masters',
marketingPath: '/masters/online-masters-in-public-health-boston-university',
organizationLogoOverrideUrl: null,
organizationShortCodeOverride: '',
productSource: {
name: 'edX',
slug: 'edx',
description: '',
},
status: 'active',
hidden: false,
inProspectus: true,
is2UDegreeProgram: false,
degree: {
quickFacts: [
{
text: '$24,000',
icon: 'fa-dollar',
},
{
text: '6 Modules',
icon: 'fa-book',
},
{
text: 'Fully Online',
icon: 'fa-desktop',
},
{
text: '24 Months',
icon: 'fa-clock-o',
},
],
additionalMetadata: null,
organizationLogoOverrideUrl: null,
},
locationRestriction: null,
subscriptionEligible: null,
objectID: 'program-ff89bff4-42c4-4728-ae8b-7e9276ebfcac',
cardType: 'program',
cardIndex: 1,
},
{
uuid: '123f9327-bfef-459f-8bf2-c53277aa58f8',
title: 'Bachelor of Science in Data Science and Business Analytics',
subtitle: '',
cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/card_images/123f9327-bfef-459f-8bf2-c53277aa58f8-60ed48b6667d.png',
authoringOrganizations: [
{
key: 'UniversityofLondon',
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/5427ee88-6dba-46ff-8ed8-87b71e3234da-0af1465ab67e.png',
name: 'University of London',
},
],
courses: [],
type: 'Bachelors',
marketingPath: '/bachelors/london-bachelor-of-science-in-data-science-and-business-analytics',
organizationLogoOverrideUrl: null,
organizationShortCodeOverride: 'University of London',
productSource: {
name: '2u',
slug: '2u',
description: '2U, Trilogy, Getsmarter -- external source for 2u courses and programs',
},
status: 'active',
hidden: false,
inProspectus: true,
is2UDegreeProgram: true,
degree: {
quickFacts: [],
additionalMetadata: {
externalIdentifier: '2ca2718e-643c-4430-b521-b9ebb91ad226',
externalUrl: 'https://programs.edx.org/requestinfo/lse?utm_source=edx&utm_medium=referral',
organicUrl: 'https://programs.edx.org/requestinfo/lse?utm_source=edx&utm_medium=referral',
},
organizationLogoOverrideUrl: null,
},
locationRestriction: {
restrictionType: 'blocklist',
countries: [
'IN',
],
states: [],
},
subscriptionEligible: null,
objectID: 'program-123f9327-bfef-459f-8bf2-c53277aa58f8',
cardType: 'program',
cardIndex: 0,
},
{
uuid: '51037067-2c57-4a58-a963-91918ccee5c8',
title: 'Leading in a Remote Environment',
subtitle: 'Remote work presents unique challenges to maintaining relationships and achieving goals. Leaders need to be adaptive in order to mobilize their teams to meet these new challenges.',
cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/card_images/51037067-2c57-4a58-a963-91918ccee5c8-b24ebade984c.jpeg',
authoringOrganizations: [
{
key: 'HarvardX',
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/44022f13-20df-4666-9111-cede3e5dc5b6-2cc39992c67a.png',
name: 'Harvard University',
key: 'org-2',
logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-2.png',
name: 'Org 2',
},
],
courses: [
{
course: {
title: 'Exercising Leadership: Foundational Principles',
title: 'How to learn online course 1',
topics: [],
},
},
{
course: {
title: 'Remote Work Revolution for Everyone',
title: 'How to learn online course 2',
topics: [],
},
},
],
type: 'Professional Certificate',
marketingPath: '/professional-certificate/harvardx-leading-in-a-remote-environment',
url: '/test-professional-certificate/how-to-learn-online-2',
organizationLogoOverrideUrl: null,
organizationShortCodeOverride: '',
productSource: {
name: 'edX',
slug: 'edx',
description: '',
name: 'company X',
slug: 'companyX',
},
status: 'active',
hidden: false,
inProspectus: true,
is2UDegreeProgram: false,
degree: null,
locationRestriction: null,
subscriptionEligible: true,
objectID: 'program-51037067-2c57-4a58-a963-91918ccee5c8',
cardType: 'program',
cardIndex: 1,
},
{
uuid: '89f39d1a-bb23-4944-b6e0-b51fe25ce932',
title: 'Master of Science in Artificial Intelligence (MSAI)',
subtitle: '',
cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/card_images/89f39d1a-bb23-4944-b6e0-b51fe25ce932-c0b01712082d.jpg',
authoringOrganizations: [
{
key: 'UTAustinX',
logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/9d38ad58-87fb-4a89-9f23-c8df318112e3-aec8e9e98a5f.png',
name: 'The University of Texas at Austin',
},
],
courses: [],
type: 'Masters',
marketingPath: '/masters/online-master-artificial-intelligence-utaustinx',
organizationLogoOverrideUrl: null,
organizationShortCodeOverride: '',
productSource: {
name: '2u',
slug: 'emeritus',
description: '2U, Trilogy, Getsmarter -- external source for 2u courses and programs',
},
status: 'active',
hidden: false,
inProspectus: true,
is2UDegreeProgram: true,
degree: {
quickFacts: [],
additionalMetadata: {
externalIdentifier: '89f39d1a-bb23-4944-b6e0-b51fe25ce932',
externalUrl: 'https://www.edx.org/masters/online-master-artificial-intelligence-utaustinx',
organicUrl: 'https://www.edx.org/masters/online-master-artificial-intelligence-utaustinx',
},
organizationLogoOverrideUrl: null,
},
locationRestriction: null,
subscriptionEligible: null,
objectID: 'program-89f39d1a-bb23-4944-b6e0-b51fe25ce932',
cardType: 'program',
cardIndex: 0,
},
];
export default mockedCoursesData;
export default mockedProductData;

View File

@@ -1,6 +1,11 @@
.nav-tabs {
border-bottom: 2px solid transparent;
}
.recommendations-container__card-list {
padding-left: 0.0625rem;
padding-bottom: 0.125rem;
@include media-breakpoint-down(xl) {
overflow-x: scroll;
overflow-y: hidden;
@@ -13,6 +18,7 @@
.recommendations-container {
padding: 0 1rem;
margin: 0 0 1.875rem 0;
@include media-breakpoint-up(lg) {
max-width: $max-width-sm + 5 * $grid-gutter-width !important;
}
@@ -24,25 +30,6 @@
@include media-breakpoint-up(xxl) {
max-width: $max-width-lg + $grid-gutter-width !important;
}
.dropdown {
background-color: #FBFAF9 !important;
.dropdown-toggle {
background-color: #FBFAF9 !important;
border: none;
color: #000000;
font-weight: 700;
font-size: 22px;
}
.dropdown-toggle:hover {
background-color: #FBFAF9 !important;
border: none;
color: #000000;
}
.dropdown-toggle::after {
margin-left: 1.3em;
}
}
}
.recommendation-card {