diff --git a/src/containers/WidgetContainers/WidgetSidebar/index.jsx b/src/containers/WidgetContainers/WidgetSidebar/index.jsx
new file mode 100644
index 0000000..744387c
--- /dev/null
+++ b/src/containers/WidgetContainers/WidgetSidebar/index.jsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { reduxHooks } from 'hooks';
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+
+// eslint-disable-next-line arrow-body-style
+export const WidgetSidebar = () => {
+ const hasCourses = reduxHooks.useHasCourses();
+
+ const widgetSidebarClassNames = classNames('widget-sidebar', { 'px-2': !hasCourses });
+ const innerWrapperClassNames = classNames('d-flex', { 'flex-column': hasCourses });
+
+ return (
+
+ );
+};
+
+export default WidgetSidebar;
diff --git a/src/containers/WidgetContainers/WidgetSidebar/index.test.jsx b/src/containers/WidgetContainers/WidgetSidebar/index.test.jsx
new file mode 100644
index 0000000..a8a6e23
--- /dev/null
+++ b/src/containers/WidgetContainers/WidgetSidebar/index.test.jsx
@@ -0,0 +1,18 @@
+import { shallow } from '@edx/react-unit-test-utils';
+
+import WidgetSidebar from '.';
+
+jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
+
+jest.mock('@openedx/frontend-plugin-framework', () => ({
+ PluginSlot: 'PluginSlot',
+}));
+
+describe('WidgetSidebar', () => {
+ beforeEach(() => jest.resetAllMocks());
+
+ test('snapshots', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.snapshot).toMatchSnapshot();
+ });
+});
diff --git a/src/test/app.test.jsx b/src/test/app.test.jsx
index 8005018..a81eabb 100644
--- a/src/test/app.test.jsx
+++ b/src/test/app.test.jsx
@@ -42,9 +42,7 @@ jest.unmock('react-redux');
jest.unmock('reselect');
jest.unmock('hooks');
-jest.mock('containers/WidgetContainers/LoadedSidebar', () => jest.fn(() => 'loaded-widget-sidebar'));
-jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => jest.fn(() => 'no-courses-widget-sidebar'));
-jest.mock('containers/WidgetContainers/WidgetFooter', () => 'product-recommendations-footer');
+jest.mock('containers/WidgetContainers/WidgetSidebar', () => jest.fn(() => 'widget-sidebar'));
jest.mock('components/NoticesWrapper', () => 'notices-wrapper');
jest.mock('@edx/frontend-platform', () => ({
@@ -61,10 +59,6 @@ jest.mock('@edx/frontend-platform/auth', () => ({
getLoginRedirectUrl: jest.fn(),
}));
-jest.mock('ExperimentContext', () => ({
- ExperimentProvider: 'div'
-}));
-
jest.mock('@edx/frontend-enterprise-hotjar', () => ({
initializeHotjar: jest.fn(),
}));
diff --git a/src/widgets/ProductRecommendations/__snapshots__/index.test.jsx.snap b/src/widgets/ProductRecommendations/__snapshots__/index.test.jsx.snap
deleted file mode 100644
index e363b04..0000000
--- a/src/widgets/ProductRecommendations/__snapshots__/index.test.jsx.snap
+++ /dev/null
@@ -1,110 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ProductRecommendations matches snapshot 1`] = `
-
-`;
diff --git a/src/widgets/ProductRecommendations/api.js b/src/widgets/ProductRecommendations/api.js
deleted file mode 100644
index 22d45f3..0000000
--- a/src/widgets/ProductRecommendations/api.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { get, stringifyUrl } from 'data/services/lms/utils';
-import urls from 'data/services/lms/urls';
-
-export const crossProductAndAmplitudeRecommendationsUrl = (courseId) => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/cross_product/${courseId}/`;
-export const amplitudeRecommendationsUrl = () => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/amplitude/v2/`;
-export const recommendationsContextUrl = () => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/recommendations_context/`;
-
-const fetchRecommendationsContext = () => get(stringifyUrl(recommendationsContextUrl()));
-
-const fetchCrossProductRecommendations = (courseId) => (
- get(stringifyUrl(crossProductAndAmplitudeRecommendationsUrl(courseId)))
-);
-const fetchAmplitudeRecommendations = () => get(stringifyUrl(amplitudeRecommendationsUrl()));
-
-export default {
- fetchCrossProductRecommendations,
- fetchAmplitudeRecommendations,
- fetchRecommendationsContext,
-};
diff --git a/src/widgets/ProductRecommendations/api.test.js b/src/widgets/ProductRecommendations/api.test.js
deleted file mode 100644
index 5db8c40..0000000
--- a/src/widgets/ProductRecommendations/api.test.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { get, stringifyUrl } from 'data/services/lms/utils';
-
-import api, { crossProductAndAmplitudeRecommendationsUrl, amplitudeRecommendationsUrl, recommendationsContextUrl } from './api';
-
-jest.mock('data/services/lms/utils', () => ({
- stringifyUrl: (...args) => ({ stringifyUrl: args }),
- get: (...args) => ({ get: args }),
-}));
-
-describe('productRecommendationCourses api', () => {
- describe('fetchCrossProductRecommendations', () => {
- it('calls get with the correct recommendation courses URL', () => {
- expect(api.fetchCrossProductRecommendations('CourseRunKey')).toEqual(
- get(stringifyUrl(crossProductAndAmplitudeRecommendationsUrl('CourseRunKey'))),
- );
- });
- });
-
- describe('fetchAmplitudeRecommendations', () => {
- it('calls get with the correct recommendation courses URL', () => {
- expect(api.fetchAmplitudeRecommendations()).toEqual(
- get(stringifyUrl(amplitudeRecommendationsUrl())),
- );
- });
- });
-
- describe('fetchRecommendationsContext', () => {
- it('calls get with the correct recommendation courses URL', () => {
- expect(api.fetchRecommendationsContext()).toEqual(
- get(stringifyUrl(recommendationsContextUrl())),
- );
- });
- });
-});
diff --git a/src/widgets/ProductRecommendations/components/LoadedView.jsx b/src/widgets/ProductRecommendations/components/LoadedView.jsx
deleted file mode 100644
index 1aea77f..0000000
--- a/src/widgets/ProductRecommendations/components/LoadedView.jsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, { useMemo } from 'react';
-import PropTypes from 'prop-types';
-import { Container } from '@openedx/paragon';
-import { useIntl } from '@edx/frontend-platform/i18n';
-
-import messages from '../messages';
-import { courseShape, courseTypeToProductTypeMap } from '../utils';
-import ProductCardContainer from './ProductCardContainer';
-
-const LoadedView = ({ crossProductCourses, openCourses }) => {
- const { formatMessage } = useIntl();
- const includesCrossProductTypes = crossProductCourses.length === 2;
-
- const finalProductList = useMemo(() => {
- if (includesCrossProductTypes) {
- const openCourseList = openCourses.slice(0, 2);
- return crossProductCourses.concat(openCourseList);
- }
- return openCourses;
- }, [crossProductCourses, openCourses, includesCrossProductTypes]);
-
- const courseTypes = [...new Set(finalProductList.map((item) => courseTypeToProductTypeMap[item.courseType]))];
-
- return (
-
-
- {formatMessage(messages.recommendationsHeading)}
-
-
-
- );
-};
-
-LoadedView.propTypes = {
- crossProductCourses: PropTypes.arrayOf(
- PropTypes.shape(courseShape),
- ).isRequired,
- openCourses: PropTypes.arrayOf(
- PropTypes.shape(courseShape),
- ).isRequired,
-};
-
-export default LoadedView;
diff --git a/src/widgets/ProductRecommendations/components/LoadedView.test.jsx b/src/widgets/ProductRecommendations/components/LoadedView.test.jsx
deleted file mode 100644
index a538b90..0000000
--- a/src/widgets/ProductRecommendations/components/LoadedView.test.jsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import { shallow } from '@edx/react-unit-test-utils';
-
-import { mockCrossProductCourses, mockOpenCourses } from '../testData';
-import LoadedView from './LoadedView';
-
-describe('ProductRecommendations LoadedView', () => {
- it('matches snapshot', () => {
- expect(
- shallow(
-
,
- ).snapshot,
- ).toMatchSnapshot();
- });
- describe('with less than 2 cross product courses', () => {
- it('passes in one course type and 4 open courses to the ProductCardContainer props', () => {
- const wrapper = shallow(
-
,
- );
-
- const productCardContainerProps = wrapper.instance.findByType('ProductCardContainer')[0].props;
-
- expect(productCardContainerProps.courseTypes.length).toEqual(1);
- expect(productCardContainerProps.courseTypes[0]).toEqual('Course');
- expect(productCardContainerProps.finalProductList).toEqual(mockOpenCourses);
- });
- });
-});
diff --git a/src/widgets/ProductRecommendations/components/LoadingView.jsx b/src/widgets/ProductRecommendations/components/LoadingView.jsx
deleted file mode 100644
index b6f75aa..0000000
--- a/src/widgets/ProductRecommendations/components/LoadingView.jsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import React from 'react';
-import { Skeleton } from '@openedx/paragon';
-
-export const LoadingView = () => (
-
-);
-
-export default LoadingView;
diff --git a/src/widgets/ProductRecommendations/components/LoadingView.test.jsx b/src/widgets/ProductRecommendations/components/LoadingView.test.jsx
deleted file mode 100644
index 45229d7..0000000
--- a/src/widgets/ProductRecommendations/components/LoadingView.test.jsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-import { shallow } from '@edx/react-unit-test-utils';
-
-import LoadingView from './LoadingView';
-
-describe('ProductRecommendations LoadingView', () => {
- it('matches snapshot', () => {
- expect(shallow(
).snapshot).toMatchSnapshot();
- });
-});
diff --git a/src/widgets/ProductRecommendations/components/ProductCard.jsx b/src/widgets/ProductRecommendations/components/ProductCard.jsx
deleted file mode 100644
index 3b50d3d..0000000
--- a/src/widgets/ProductRecommendations/components/ProductCard.jsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {
- Badge,
- Card,
- Truncate,
- Hyperlink,
-} from '@openedx/paragon';
-import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
-
-import { trackProductCardClicked, trackCourseCardClicked } from '../optimizelyExperiment';
-import { productCardClicked, discoveryCardClicked } from '../track';
-import { bootCamp, executiveEducation } from '../constants';
-
-const ProductCard = ({
- title,
- subtitle,
- headerImage,
- courseRunKey,
- schoolLogo,
- courseType,
- url,
-}) => {
- const handleClick = (type, link) => {
- const crossProductTypes = [executiveEducation, bootCamp];
- const userId = getAuthenticatedUser().userId.toString();
-
- if (crossProductTypes.includes(type)) {
- trackProductCardClicked(userId);
- productCardClicked(courseRunKey, title, type, link);
- } else {
- trackCourseCardClicked(userId);
- discoveryCardClicked(courseRunKey, title, link);
- }
- };
-
- const getRedirectUrl = (link) => {
- const urlObj = new URL(link);
- const hasQueryStringParameters = urlObj.search !== '';
-
- if (hasQueryStringParameters) {
- return `${link}&linked_from=recommender`;
- }
-
- return `${link}?linked_from=recommender`;
- };
-
- const redirectUrl = getRedirectUrl(url);
-
- return (
-
{
- handleClick(courseType, redirectUrl);
- }}
- isClickable
- >
-
-
- {title}
-
- )}
- subtitle={(
-
- {subtitle}
-
- )}
- />
-
-
- {courseType}
-
-
-
- );
-};
-
-ProductCard.propTypes = {
- title: PropTypes.string.isRequired,
- subtitle: PropTypes.string.isRequired,
- headerImage: PropTypes.string.isRequired,
- courseRunKey: PropTypes.string.isRequired,
- schoolLogo: PropTypes.string.isRequired,
- courseType: PropTypes.string.isRequired,
- url: PropTypes.string.isRequired,
-};
-
-export default ProductCard;
diff --git a/src/widgets/ProductRecommendations/components/ProductCard.test.jsx b/src/widgets/ProductRecommendations/components/ProductCard.test.jsx
deleted file mode 100644
index 2b5a5a2..0000000
--- a/src/widgets/ProductRecommendations/components/ProductCard.test.jsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import React from 'react';
-import { shallow } from '@edx/react-unit-test-utils';
-
-import { mockCrossProductCourses, mockOpenCourses, mockFallbackOpenCourse } from '../testData';
-import { trackProductCardClicked, trackCourseCardClicked } from '../optimizelyExperiment';
-import { productCardClicked, discoveryCardClicked } from '../track';
-import ProductCard from './ProductCard';
-import { courseTypeToProductTypeMap } from '../utils';
-
-jest.mock('../optimizelyExperiment', () => ({
- trackProductCardClicked: jest.fn(),
- trackCourseCardClicked: jest.fn(),
-}));
-
-jest.mock('../track', () => ({
- productCardClicked: jest.fn(),
- discoveryCardClicked: jest.fn(),
-}));
-
-jest.mock('@edx/frontend-platform/auth', () => ({
- getAuthenticatedUser: jest.fn(() => ({ userId: '1' })),
-}));
-
-describe('ProductRecommendations ProductCard', () => {
- const getProps = (course) => {
- const {
- title,
- owners: [{ name: subtitle }],
- image: { src: headerImage },
- owners: [{ logoImageUrl: schoolLogo }],
- } = course;
-
- return {
- title,
- subtitle,
- headerImage,
- schoolLogo,
- courseType: courseTypeToProductTypeMap[course.courseType],
- courseRunKey: course.courseRunKey,
- url: course.marketingUrl,
- };
- };
-
- const crossProductProps = getProps(mockCrossProductCourses[0]);
- const openCourseProps = getProps(mockOpenCourses[0]);
- const fallbackOpenCourseProps = getProps(mockFallbackOpenCourse[0]);
-
- it('matches snapshot', () => {
- expect(shallow(
).snapshot).toMatchSnapshot();
- });
-
- it('has the query string parameter attached to a fallback recommendations url', () => {
- const wrapper = shallow(
);
- const cardUrl = wrapper.instance.findByType('Card')[0].props.destination;
-
- expect(cardUrl).toEqual('https://www.edx.org/course/some-course?linked_from=recommender');
- });
-
- it('send outs experiment events related to open courses when clicked', () => {
- const wrapper = shallow(
);
- const { courseRunKey, title, url } = openCourseProps;
-
- wrapper.instance.props.onClick();
-
- expect(trackCourseCardClicked).toHaveBeenCalledWith('1');
- expect(discoveryCardClicked).toHaveBeenCalledWith(courseRunKey, title, `${url}&linked_from=recommender`);
- });
-
- it('send outs experiment events related to cross product courses when clicked', () => {
- const wrapper = shallow(
);
- const {
- courseRunKey,
- title,
- courseType,
- url,
- } = crossProductProps;
-
- wrapper.instance.props.onClick();
-
- expect(trackProductCardClicked).toHaveBeenCalledWith('1');
- expect(productCardClicked).toHaveBeenCalledWith(courseRunKey, title, courseType, `${url}&linked_from=recommender`);
- });
-});
diff --git a/src/widgets/ProductRecommendations/components/ProductCardContainer.jsx b/src/widgets/ProductRecommendations/components/ProductCardContainer.jsx
deleted file mode 100644
index c591e11..0000000
--- a/src/widgets/ProductRecommendations/components/ProductCardContainer.jsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-
-import { courseShape, courseTypeToProductTypeMap } from '../utils';
-import { course } from '../constants';
-import ProductCard from './ProductCard';
-import ProductCardHeader from './ProductCardHeader';
-
-const ProductCardContainer = ({ finalProductList, courseTypes }) => (
-
- {finalProductList
- && courseTypes.map((type) => (
-
-
-
- {finalProductList
- .filter((courseObj) => courseTypeToProductTypeMap[courseObj.courseType] === type)
- .map((item) => (
-
- ))}
-
-
- ))}
-
-);
-
-ProductCardContainer.propTypes = {
- finalProductList: PropTypes.arrayOf(
- PropTypes.shape(courseShape),
- ).isRequired,
- courseTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
-};
-
-export default ProductCardContainer;
diff --git a/src/widgets/ProductRecommendations/components/ProductCardContainer.test.jsx b/src/widgets/ProductRecommendations/components/ProductCardContainer.test.jsx
deleted file mode 100644
index 6c6ae52..0000000
--- a/src/widgets/ProductRecommendations/components/ProductCardContainer.test.jsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import { shallow } from '@edx/react-unit-test-utils';
-
-import { mockCrossProductCourses, mockOpenCourses } from '../testData';
-import ProductCardContainer from './ProductCardContainer';
-import { executiveEducation, bootCamp, course } from '../constants';
-
-describe('ProductRecommendations ProductCardContainer', () => {
- const props = {
- finalProductList: [...mockCrossProductCourses, ...mockOpenCourses],
- courseTypes: [executiveEducation, bootCamp, course],
- };
-
- it('matches snapshot', () => {
- expect(shallow(
).snapshot).toMatchSnapshot();
- });
-
- describe('with finalCourseList containing cross product and open courses', () => {
- it('renders 3 ProductCardHeaders with the 3 different course types', () => {
- const wrapper = shallow(
);
- const productCardHeaders = wrapper.instance.findByType('ProductCardHeader');
-
- expect(productCardHeaders.length).toEqual(3);
- productCardHeaders.forEach((header, index) => {
- expect(header.props.courseType).toEqual(props.courseTypes[index]);
- });
- });
- });
-
- describe('with finalCourseList containing only open courses', () => {
- it('renders 1 ProductHeader with the one course type', () => {
- const openCoursesProps = {
- finalProductList: [...mockOpenCourses, ...mockOpenCourses],
- courseTypes: ['Course'],
- };
-
- const wrapper = shallow(
);
- const productCardHeaders = wrapper.instance.findByType('ProductCardHeader');
-
- expect(productCardHeaders.length).toEqual(1);
- expect(productCardHeaders[0].props.courseType).toEqual(openCoursesProps.courseTypes[0]);
- });
- });
-});
diff --git a/src/widgets/ProductRecommendations/components/ProductCardHeader.jsx b/src/widgets/ProductRecommendations/components/ProductCardHeader.jsx
deleted file mode 100644
index 9a0d95e..0000000
--- a/src/widgets/ProductRecommendations/components/ProductCardHeader.jsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { useIntl } from '@edx/frontend-platform/i18n';
-import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
-
-import { Icon, Hyperlink } from '@openedx/paragon';
-import { ChevronRight } from '@openedx/paragon/icons';
-import { getConfig } from '@edx/frontend-platform';
-import { trackProductHeaderClicked } from '../optimizelyExperiment';
-import { recommendationsHeaderClicked } from '../track';
-import { executiveEducation, bootCamp } from '../constants';
-import messages from '../messages';
-
-const ProductCardHeader = ({ courseType }) => {
- const getProductTypeDetail = (type) => {
- switch (type) {
- case executiveEducation:
- return {
- heading: messages.executiveEducationHeading,
- description: messages.executiveEducationDescription,
- url: '/executive-education?linked_from=recommender',
- };
- case bootCamp:
- return {
- heading: messages.bootcampHeading,
- description: messages.bootcampDescription,
- url: '/boot-camps?linked_from=recommender',
- };
- default: {
- return {
- heading: messages.courseHeading,
- description: messages.courseDescription,
- url: '/search?tab=course',
- };
- }
- }
- };
-
- const handleClick = (type, url) => {
- const userId = getAuthenticatedUser().userId.toString();
-
- trackProductHeaderClicked(userId);
- recommendationsHeaderClicked(type, url);
- };
-
- const { formatMessage } = useIntl();
- const productTypeDetail = getProductTypeDetail(courseType);
- const headerUrl = `${getConfig().MARKETING_SITE_BASE_URL}${productTypeDetail.url}`;
-
- return (
-
-
{
- handleClick(courseType, headerUrl);
- }}
- >
-
-
- {formatMessage(productTypeDetail.heading)}
-
-
-
-
-
- {formatMessage(productTypeDetail.description)}
-
-
- );
-};
-
-ProductCardHeader.propTypes = {
- courseType: PropTypes.string.isRequired,
-};
-
-export default ProductCardHeader;
diff --git a/src/widgets/ProductRecommendations/components/ProductCardHeader.test.jsx b/src/widgets/ProductRecommendations/components/ProductCardHeader.test.jsx
deleted file mode 100644
index 4709cd8..0000000
--- a/src/widgets/ProductRecommendations/components/ProductCardHeader.test.jsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-import { shallow } from '@edx/react-unit-test-utils';
-
-import ProductCardHeader from './ProductCardHeader';
-import { executiveEducation, bootCamp } from '../constants';
-import { trackProductHeaderClicked } from '../optimizelyExperiment';
-import { recommendationsHeaderClicked } from '../track';
-
-jest.mock('@edx/frontend-platform/auth', () => ({
- getAuthenticatedUser: jest.fn(() => ({ userId: '1' })),
-}));
-
-jest.mock('../optimizelyExperiment', () => ({
- trackProductHeaderClicked: jest.fn(),
-}));
-
-jest.mock('../track', () => ({
- recommendationsHeaderClicked: jest.fn(),
-}));
-
-describe('ProductRecommendations ProductCardHeader', () => {
- const coursesType = 'Courses';
-
- it('matches snapshot', () => {
- expect(shallow(
).snapshot).toMatchSnapshot();
- });
-
- describe('with bootcamp courseType prop', () => {
- it('renders a bootcamp header', () => {
- const wrapper = shallow(
);
-
- expect(wrapper.instance.findByType('h3')[0].children[0].el).toEqual(bootCamp);
- });
- });
-
- describe('with courses courseType prop', () => {
- it('renders a courses header', () => {
- const wrapper = shallow(
);
-
- expect(wrapper.instance.findByType('h3')[0].children[0].el).toEqual(coursesType);
- });
- });
-
- it('send outs experiment events when clicked', () => {
- const wrapper = shallow(
);
- const hyperLink = wrapper.instance.findByType('Hyperlink')[0];
- const execEdLink = 'http://localhost:18000/executive-education?linked_from=recommender';
-
- hyperLink.props.onClick();
-
- expect(trackProductHeaderClicked).toHaveBeenCalledWith('1');
- expect(recommendationsHeaderClicked).toHaveBeenCalledWith(executiveEducation, execEdLink);
- });
-});
diff --git a/src/widgets/ProductRecommendations/components/__snapshots__/LoadedView.test.jsx.snap b/src/widgets/ProductRecommendations/components/__snapshots__/LoadedView.test.jsx.snap
deleted file mode 100644
index b2915b6..0000000
--- a/src/widgets/ProductRecommendations/components/__snapshots__/LoadedView.test.jsx.snap
+++ /dev/null
@@ -1,89 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ProductRecommendations LoadedView matches snapshot 1`] = `
-
-
- You might also like
-
-
-
-`;
diff --git a/src/widgets/ProductRecommendations/components/__snapshots__/LoadingView.test.jsx.snap b/src/widgets/ProductRecommendations/components/__snapshots__/LoadingView.test.jsx.snap
deleted file mode 100644
index 1203443..0000000
--- a/src/widgets/ProductRecommendations/components/__snapshots__/LoadingView.test.jsx.snap
+++ /dev/null
@@ -1,7 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ProductRecommendations LoadingView matches snapshot 1`] = `
-
-`;
diff --git a/src/widgets/ProductRecommendations/components/__snapshots__/ProductCard.test.jsx.snap b/src/widgets/ProductRecommendations/components/__snapshots__/ProductCard.test.jsx.snap
deleted file mode 100644
index 13e228e..0000000
--- a/src/widgets/ProductRecommendations/components/__snapshots__/ProductCard.test.jsx.snap
+++ /dev/null
@@ -1,49 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ProductRecommendations ProductCard matches snapshot 1`] = `
-
-
-
- Harvard University
-
- }
- title={
-
- Introduction to Computer Science
-
- }
- />
-
-
-
- Executive Education
-
-
-
-
-`;
diff --git a/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardContainer.test.jsx.snap b/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardContainer.test.jsx.snap
deleted file mode 100644
index 3d4969b..0000000
--- a/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardContainer.test.jsx.snap
+++ /dev/null
@@ -1,101 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ProductRecommendations ProductCardContainer matches snapshot 1`] = `
-
-`;
diff --git a/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardHeader.test.jsx.snap b/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardHeader.test.jsx.snap
deleted file mode 100644
index c8c4d9a..0000000
--- a/src/widgets/ProductRecommendations/components/__snapshots__/ProductCardHeader.test.jsx.snap
+++ /dev/null
@@ -1,30 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ProductRecommendations ProductCardHeader matches snapshot 1`] = `
-
-
-
-
- Executive Education
-
-
-
-
-
- Short Courses to develop leadership skills
-
-
-`;
diff --git a/src/widgets/ProductRecommendations/constants.js b/src/widgets/ProductRecommendations/constants.js
deleted file mode 100644
index 8cb3b88..0000000
--- a/src/widgets/ProductRecommendations/constants.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export const bootCamp = 'Boot Camp';
-export const executiveEducation = 'Executive Education';
-export const course = 'Course';
-
-export const control = 'control';
-export const treatment = 'treatment';
-export const noExperiment = 'no experiment';
diff --git a/src/widgets/ProductRecommendations/hooks.js b/src/widgets/ProductRecommendations/hooks.js
deleted file mode 100644
index d2047cd..0000000
--- a/src/widgets/ProductRecommendations/hooks.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import { useState, useEffect } from 'react';
-import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
-
-import { RequestStates, RequestKeys } from 'data/constants/requests';
-import { StrictDict } from 'utils';
-import { reduxHooks } from 'hooks';
-import { SortKeys } from 'data/constants/app';
-import { useWindowSize, breakpoints } from '@openedx/paragon';
-import { useExperimentContext } from 'ExperimentContext';
-import { control, treatment, noExperiment } from './constants';
-import { activateProductRecommendationsExperiment, trackProductRecommendationsViewed } from './optimizelyExperiment';
-import { recommendationsViewed } from './track';
-
-import api from './api';
-import * as module from './hooks';
-
-export const state = StrictDict({
- requestState: (val) => useState(val), // eslint-disable-line
- data: (val) => useState(val), // eslint-disable-line
-});
-
-export const useIsMobile = () => {
- const { width } = useWindowSize();
- return width < breakpoints.small.minWidth;
-};
-
-export const useMostRecentCourseRunKey = () => {
- const mostRecentCourseRunKey = reduxHooks.useCurrentCourseList({
- sortBy: SortKeys.enrolled,
- filters: [],
- pageSize: 0,
- }).visible[0]?.courseRun?.courseId;
-
- return mostRecentCourseRunKey;
-};
-
-export const useActivateRecommendationsExperiment = () => {
- const enterpriseDashboardData = reduxHooks.useEnterpriseDashboardData();
- const hasRequestCompleted = reduxHooks.useRequestIsCompleted(RequestKeys.initialize);
- const mostRecentCourseRunKey = module.useMostRecentCourseRunKey();
- const userId = getAuthenticatedUser().userId.toString();
-
- const {
- experiment: { isExperimentActive },
- setExperiment,
- isMobile,
- countryCode,
- } = useExperimentContext();
-
- useEffect(() => {
- if (!isExperimentActive && countryCode !== null) {
- const activateExperiment = () => {
- const userAttributes = {
- is_mobile_user: isMobile,
- is_enterprise_user: !!enterpriseDashboardData,
- location: countryCode ? countryCode.toLowerCase() : '',
- };
- const experiment = activateProductRecommendationsExperiment(userId, userAttributes);
-
- setExperiment((prev) => ({
- ...prev,
- isExperimentActive: true,
- inRecommendationsVariant: experiment.inExperimentVariant,
- }));
-
- return experiment;
- };
-
- const sendViewedEvent = () => {
- trackProductRecommendationsViewed(userId);
- recommendationsViewed(true, control, mostRecentCourseRunKey);
- };
-
- if (hasRequestCompleted) {
- const { experimentActivated, inExperimentVariant } = activateExperiment();
-
- if (experimentActivated && !inExperimentVariant) {
- sendViewedEvent();
- }
- }
- }
- /* eslint-disable */
- }, [isExperimentActive, countryCode])
-};
-
-export const useShowRecommendationsFooter = () => {
- const { experiment } = useExperimentContext();
-
- return experiment;
-};
-
-export const useFetchRecommendations = (setRequestState, setData) => {
- const courseRunKey = module.useMostRecentCourseRunKey();
-
- useEffect(() => {
- let isMounted = true;
-
- const handleSuccess = (response) => {
- if (isMounted) {
- setData(response.data);
- setRequestState(RequestStates.completed);
- }
- };
-
- const handleError = () => {
- if (isMounted) {
- setRequestState(RequestStates.failed);
- }
- };
-
- if (courseRunKey) {
- api
- .fetchCrossProductRecommendations(courseRunKey)
- .then(handleSuccess)
- .catch(handleError);
- } else {
- api
- .fetchAmplitudeRecommendations()
- .then(handleSuccess)
- .catch(handleError);
- }
- return () => {
- isMounted = false;
- };
- /* eslint-disable */
- }, []);
-};
-
-export const useSendViewedEvents = (requestState, data) => {
- const mostRecentCourseRunKey = module.useMostRecentCourseRunKey();
- const userId = getAuthenticatedUser().userId.toString();
-
- useEffect(() => {
- if (requestState === RequestStates.completed) {
- if (data.crossProductCourses?.length === 2) {
- trackProductRecommendationsViewed(userId);
- recommendationsViewed(false, treatment, mostRecentCourseRunKey);
- } else {
- trackProductRecommendationsViewed(userId);
- recommendationsViewed(true, noExperiment, mostRecentCourseRunKey);
- }
- }
- }, [data, requestState])
-}
-
-export const useProductRecommendationsData = () => {
- const [requestState, setRequestState] = module.state.requestState(RequestStates.pending);
- const [data, setData] = module.state.data({});
-
- module.useFetchRecommendations(setRequestState, setData);
- module.useSendViewedEvents(requestState, data);
-
- return {
- productRecommendations: data,
- isLoading: requestState === RequestStates.pending,
- isLoaded: requestState === RequestStates.completed,
- hasFailed: requestState === RequestStates.failed
- };
-};
-
-export default { useProductRecommendationsData, useShowRecommendationsFooter, useIsMobile, useActivateRecommendationsExperiment };
diff --git a/src/widgets/ProductRecommendations/hooks.test.js b/src/widgets/ProductRecommendations/hooks.test.js
deleted file mode 100644
index bc4a3ae..0000000
--- a/src/widgets/ProductRecommendations/hooks.test.js
+++ /dev/null
@@ -1,548 +0,0 @@
-import React from 'react';
-import { waitFor } from '@testing-library/react';
-
-import { MockUseState } from 'testUtils';
-import { RequestStates } from 'data/constants/requests';
-import { reduxHooks } from 'hooks';
-import { useWindowSize } from '@openedx/paragon';
-import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
-import { useExperimentContext } from 'ExperimentContext';
-import { recommendationsViewed } from './track';
-import { activateProductRecommendationsExperiment, trackProductRecommendationsViewed } from './optimizelyExperiment';
-import { control, treatment, noExperiment } from './constants';
-import { wait } from './utils';
-import { mockCrossProductResponse, mockAmplitudeResponse } from './testData';
-
-import api from './api';
-import * as hooks from './hooks';
-
-jest.mock('./api', () => ({
- fetchCrossProductRecommendations: jest.fn(),
- fetchAmplitudeRecommendations: jest.fn(),
- fetchRecommendationsContext: jest.fn(),
-}));
-
-jest.mock('@edx/frontend-platform/auth', () => ({
- getAuthenticatedUser: jest.fn(),
-}));
-
-jest.mock('ExperimentContext', () => ({
- useExperimentContext: jest.fn(),
-}));
-
-jest.mock('hooks', () => ({
- reduxHooks: {
- useCurrentCourseList: jest.fn(),
- useEnterpriseDashboardData: jest.fn(),
- useRequestIsCompleted: jest.fn(),
- },
-}));
-
-jest.mock('./track', () => ({
- recommendationsViewed: jest.fn(),
-}));
-
-jest.mock('./optimizelyExperiment', () => ({
- trackProductRecommendationsViewed: jest.fn(),
- activateProductRecommendationsExperiment: jest.fn(),
-}));
-
-const state = new MockUseState(hooks);
-const mostRecentCourseRunKey = 'course ID 1';
-
-const courses = [
- {
- courseRun: {
- courseId: mostRecentCourseRunKey,
- },
- },
- {
- courseRun: {
- courseId: 'course ID 2',
- },
- },
-];
-
-const populatedCourseListData = {
- visible: courses,
- numPages: 0,
-};
-
-const emptyCourseListData = {
- visible: [],
- numPages: 0,
-};
-
-let output;
-describe('ProductRecommendations hooks', () => {
- beforeEach(() => {
- jest.resetAllMocks();
- getAuthenticatedUser.mockImplementation(() => ({ userId: '1' }));
- });
-
- describe('state fields', () => {
- state.testGetter(state.keys.requestState);
- state.testGetter(state.keys.data);
- });
-
- describe('useMostRecentCourseRunKey', () => {
- it('returns the courseId of the first course in the sorted visible array', () => {
- reduxHooks.useCurrentCourseList.mockReturnValueOnce(populatedCourseListData);
-
- expect(hooks.useMostRecentCourseRunKey()).toBe(mostRecentCourseRunKey);
- });
- });
-
- describe('useIsMobile', () => {
- it('returns false if the width of the window is greater than or equal to 576px', () => {
- useWindowSize
- .mockReturnValueOnce({ width: 576, height: 943 })
- .mockReturnValueOnce({ width: 1400, height: 943 });
-
- expect(hooks.useIsMobile()).toBeFalsy();
- expect(hooks.useIsMobile()).toBeFalsy();
- });
-
- it('returns true if the width of the window is less than 576px', () => {
- useWindowSize.mockReturnValueOnce({ width: 575, height: 943 });
-
- expect(hooks.useIsMobile()).toBeTruthy();
- });
- });
-
- describe('useShowRecommendationsFooter', () => {
- it('returns the experiment object, stating if the experiment has activated and the variant', () => {
- useExperimentContext
- .mockImplementationOnce(() => ({ experiment: { inRecommendationsVariant: true, isExperimentActive: false } }));
-
- const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
-
- expect(useExperimentContext).toHaveBeenCalled();
- expect(inRecommendationsVariant).toBeTruthy();
- expect(isExperimentActive).toBeFalsy();
- });
- });
-
- describe('useActivateRecommendationsExperiment', () => {
- describe('behavior', () => {
- describe('useEffect call', () => {
- let cb;
- let calls;
- let prereqs;
- const setExperiment = jest.fn();
- const setCountryCode = jest.fn();
- const userAttributes = { is_enterprise_user: false, is_mobile_user: false, location: 'za' };
-
- const optimizelyExperimentMock = ({
- experimentActivated = false,
- inExperimentVariant = false,
- }) => ({
- experimentActivated,
- inExperimentVariant,
- });
-
- const experimentContextMock = ({
- isExperimentActive = false,
- inRecommendationsVariant = true,
- countryCode = 'ZA',
- isMobile = false,
- }) => ({
- experiment: { isExperimentActive, inRecommendationsVariant },
- countryCode,
- isMobile,
- setExperiment,
- setCountryCode,
- });
-
- const setUp = (
- isCompleted,
- experimentContext = experimentContextMock({}),
- optimizelyExperiment = optimizelyExperimentMock({}),
- ) => {
- reduxHooks.useCurrentCourseList.mockReturnValueOnce(populatedCourseListData);
- reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
- reduxHooks.useRequestIsCompleted.mockReturnValueOnce(isCompleted);
- useExperimentContext.mockReturnValueOnce(experimentContext);
- activateProductRecommendationsExperiment.mockReturnValueOnce(optimizelyExperiment);
-
- hooks.useActivateRecommendationsExperiment();
-
- ({ calls } = React.useEffect.mock);
- ([[cb, prereqs]] = calls);
- };
-
- it('runs when isExperimentActive or countryCode changes (prereqs)', () => {
- setUp(true);
- expect(prereqs).toEqual([false, 'ZA']);
- expect(calls.length).toEqual(1);
- });
- describe('when the request state is not completed', () => {
- it('does not activate or send any events', () => {
- setUp(false);
- cb();
- expect(activateProductRecommendationsExperiment).not.toHaveBeenCalled();
- expect(trackProductRecommendationsViewed).not.toHaveBeenCalled();
- expect(recommendationsViewed).not.toHaveBeenCalled();
- });
- });
- describe('when the experiment is active', () => {
- it('does not activate or send any events', () => {
- setUp(true, experimentContextMock({ isExperimentActive: true }));
- cb();
- expect(activateProductRecommendationsExperiment).not.toHaveBeenCalled();
- expect(trackProductRecommendationsViewed).not.toHaveBeenCalled();
- expect(recommendationsViewed).not.toHaveBeenCalled();
- });
- });
- describe('when the experiment is inactive but user country code has not been fetched', () => {
- it('does not activate or send any events', () => {
- setUp(true, experimentContextMock({ countryCode: null }));
- cb();
- expect(activateProductRecommendationsExperiment).not.toHaveBeenCalled();
- expect(trackProductRecommendationsViewed).not.toHaveBeenCalled();
- expect(recommendationsViewed).not.toHaveBeenCalled();
- });
- });
- describe('when the experiment is inactive and user country code has been fetched', () => {
- it('activates the experiment and sends viewed event for control group', () => {
- setUp(
- true,
- experimentContextMock({}),
- optimizelyExperimentMock({ experimentActivated: true, inExperimentVariant: false }),
- );
- cb();
- expect(activateProductRecommendationsExperiment).toHaveBeenCalledWith('1', userAttributes);
- expect(setExperiment).toHaveBeenCalled();
- expect(trackProductRecommendationsViewed).toHaveBeenCalledWith('1');
- expect(recommendationsViewed).toHaveBeenCalledWith(true, control, mostRecentCourseRunKey);
- });
- it('activates the experiment and does not sends viewed event for treatment group', () => {
- setUp(
- true,
- experimentContextMock({ countryCode: '' }),
- optimizelyExperimentMock({ experimentActivated: true, inExperimentVariant: true }),
- );
- cb();
- expect(activateProductRecommendationsExperiment).toHaveBeenCalledWith('1', { ...userAttributes, location: '' });
- expect(setExperiment).toHaveBeenCalled();
- expect(trackProductRecommendationsViewed).not.toHaveBeenCalled();
- expect(recommendationsViewed).not.toHaveBeenCalled();
- });
- });
- });
- });
- });
-
- describe('useSendViewedEvents', () => {
- describe('behavior', () => {
- describe('useEffect call', () => {
- let cb;
- let calls;
- let prereqs;
- const { completed, pending } = RequestStates;
-
- const setUp = (requestState, response) => {
- reduxHooks.useCurrentCourseList.mockReturnValueOnce(populatedCourseListData);
- hooks.useSendViewedEvents(requestState, response);
- ({ calls } = React.useEffect.mock);
- ([[cb, prereqs]] = calls);
- };
-
- it('runs when data or requestState changes (prereqs)', () => {
- setUp(completed, mockCrossProductResponse);
- expect(prereqs).toEqual([mockCrossProductResponse, completed]);
- expect(calls.length).toEqual(1);
- });
- describe('when the request state is not completed', () => {
- it('does not send any events', () => {
- setUp(pending, mockCrossProductResponse);
- cb();
- expect(trackProductRecommendationsViewed).not.toHaveBeenCalled();
- expect(recommendationsViewed).not.toHaveBeenCalled();
- });
- });
- describe('when the request state is completed', () => {
- describe('with crossProduct data that has 2 cross product courses', () => {
- it('sends out recommendations viewed event for "treatment" group', () => {
- setUp(completed, mockCrossProductResponse);
- cb();
- expect(trackProductRecommendationsViewed).toHaveBeenCalledWith('1');
- expect(recommendationsViewed).toHaveBeenCalledWith(false, treatment, mostRecentCourseRunKey);
- });
- });
- describe('with amplitude data and no cross product data', () => {
- it('sends out recommendations viewed event for "no experiment" group', () => {
- setUp(completed, mockAmplitudeResponse);
- cb();
- expect(trackProductRecommendationsViewed).toHaveBeenCalledWith('1');
- expect(recommendationsViewed).toHaveBeenCalledWith(true, noExperiment, mostRecentCourseRunKey);
- });
- });
- });
- });
- });
- });
-
- describe('useFetchRecommendations', () => {
- describe('behavior', () => {
- describe('useEffect call', () => {
- let calls;
- let cb;
- const response = { data: 'response data' };
- const setRequestState = jest.fn();
- const setData = jest.fn();
-
- const setUp = (mockCourseListData) => {
- reduxHooks.useCurrentCourseList.mockReturnValue(mockCourseListData);
- hooks.useFetchRecommendations(setRequestState, setData);
- ({ calls } = React.useEffect.mock);
- ([[cb]] = calls);
- };
-
- it('calls useEffect once', () => {
- setUp(populatedCourseListData);
- expect(calls.length).toEqual(1);
- });
- describe('without no courseId due to no enrolled courses', () => {
- it('calls fetchAmplitudeRecommendations', () => {
- setUp(emptyCourseListData);
- api.fetchAmplitudeRecommendations.mockReturnValueOnce(Promise.resolve(response));
- cb();
- expect(api.fetchAmplitudeRecommendations).toHaveBeenCalled();
- });
- });
- describe('with most recently enrolled courseId', () => {
- it('calls fetchCrossProductRecommendations with the most recently enrolled courseId', () => {
- setUp(populatedCourseListData);
- api.fetchCrossProductRecommendations.mockReturnValueOnce(Promise.resolve(response));
- cb();
- expect(api.fetchCrossProductRecommendations).toHaveBeenCalledWith(mostRecentCourseRunKey);
- });
- });
- describe('fetching cross product recommendations', () => {
- beforeEach(() => setUp(populatedCourseListData));
-
- describe('successful fetch on mounted component', () => {
- it('sets the request state to completed and loads response', async () => {
- let resolveFn;
- api.fetchCrossProductRecommendations.mockReturnValueOnce(new Promise(resolve => {
- resolveFn = resolve;
- }));
- cb();
- expect(api.fetchCrossProductRecommendations).toHaveBeenCalledWith(mostRecentCourseRunKey);
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- resolveFn(response);
- await waitFor(() => {
- expect(setRequestState).toHaveBeenCalledWith(RequestStates.completed);
- expect(setData).toHaveBeenCalledWith(response.data);
- });
- });
- });
- describe('successful fetch on unmounted component', () => {
- it('does not set the state', async () => {
- let resolveFn;
- api.fetchCrossProductRecommendations.mockReturnValueOnce(new Promise(resolve => {
- resolveFn = resolve;
- }));
- const unMount = cb();
- expect(api.fetchCrossProductRecommendations).toHaveBeenCalledWith(mostRecentCourseRunKey);
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- unMount();
- resolveFn(response);
- await wait(10);
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- });
- });
- describe('unsuccessful fetch on mounted component', () => {
- it('sets the request state to failed and does not set the data state', async () => {
- let rejectFn;
- api.fetchCrossProductRecommendations.mockReturnValueOnce(new Promise((resolve, reject) => {
- rejectFn = reject;
- }));
- cb();
- expect(api.fetchCrossProductRecommendations).toHaveBeenCalledWith(mostRecentCourseRunKey);
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- rejectFn();
- await waitFor(() => {
- expect(setRequestState).toHaveBeenCalledWith(RequestStates.failed);
- expect(setData).not.toHaveBeenCalled();
- });
- });
- });
- describe('unsuccessful fetch on unmounted component', () => {
- it('does not set the state', async () => {
- let rejectFn;
- api.fetchCrossProductRecommendations.mockReturnValueOnce(new Promise((resolve, reject) => {
- rejectFn = reject;
- }));
- const unMount = cb();
- expect(api.fetchCrossProductRecommendations).toHaveBeenCalledWith(mostRecentCourseRunKey);
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- unMount();
- rejectFn();
- await wait(10);
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- });
- });
- });
- describe('fetching Amplitude recommendations', () => {
- beforeEach(() => setUp(emptyCourseListData));
-
- describe('successful fetch on mounted component', () => {
- it('sets the request state to completed and loads response', async () => {
- let resolveFn;
- api.fetchAmplitudeRecommendations.mockReturnValueOnce(new Promise(resolve => {
- resolveFn = resolve;
- }));
- cb();
- expect(api.fetchAmplitudeRecommendations).toHaveBeenCalled();
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- resolveFn(response);
- await waitFor(() => {
- expect(setRequestState).toHaveBeenCalledWith(RequestStates.completed);
- expect(setData).toHaveBeenCalledWith(response.data);
- });
- });
- });
- describe('successful fetch on unmounted component', () => {
- it('does not set the state', async () => {
- let resolveFn;
- api.fetchAmplitudeRecommendations.mockReturnValueOnce(new Promise(resolve => {
- resolveFn = resolve;
- }));
- const unMount = cb();
- expect(api.fetchAmplitudeRecommendations).toHaveBeenCalled();
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- unMount();
- resolveFn(response);
- await wait(10);
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- });
- });
- describe('unsuccessful fetch on mounted component', () => {
- it('sets the request state to failed and does not set the data state', async () => {
- let rejectFn;
- api.fetchAmplitudeRecommendations.mockReturnValueOnce(new Promise((resolve, reject) => {
- rejectFn = reject;
- }));
- cb();
- expect(api.fetchAmplitudeRecommendations).toHaveBeenCalled();
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- rejectFn();
- await waitFor(() => {
- expect(setRequestState).toHaveBeenCalledWith(RequestStates.failed);
- expect(setData).not.toHaveBeenCalled();
- });
- });
- });
- describe('unsuccessful fetch on unmounted component', () => {
- it('does not set the state', async () => {
- let rejectFn;
- api.fetchAmplitudeRecommendations.mockReturnValueOnce(new Promise((resolve, reject) => {
- rejectFn = reject;
- }));
- const unMount = cb();
- expect(api.fetchAmplitudeRecommendations).toHaveBeenCalled();
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- unMount();
- rejectFn();
- await wait(10);
- expect(setRequestState).not.toHaveBeenCalled();
- expect(setData).not.toHaveBeenCalled();
- });
- });
- });
- });
- });
- });
- describe('useProductRecommendationsData', () => {
- let fetchRecommendationsSpy;
- let sendViewedEventsSpy;
- beforeEach(() => {
- state.mock();
- fetchRecommendationsSpy = jest.spyOn(hooks, 'useFetchRecommendations').mockImplementationOnce(() => {});
- sendViewedEventsSpy = jest.spyOn(hooks, 'useSendViewedEvents').mockImplementationOnce(() => {});
- output = hooks.useProductRecommendationsData();
- });
- it('calls useFetchRecommendations with setRequestState and setData', () => {
- expect(fetchRecommendationsSpy).toHaveBeenCalledWith(state.setState.requestState, state.setState.data);
- });
- it('calls useFetchViewedEvents with requestState and data', () => {
- expect(sendViewedEventsSpy).toHaveBeenCalledWith(state.stateVals.requestState, state.stateVals.data);
- });
- it('initializes requestState as RequestStates.pending', () => {
- state.expectInitializedWith(state.keys.requestState, RequestStates.pending);
- });
- describe('return values', () => {
- describe('when the request is completed, with returned response object', () => {
- const mockResponse = { crossProductCourses: {}, amplitudeCourses: {} };
- beforeEach(() => {
- state.mockVal(state.keys.requestState, RequestStates.completed);
- state.mockVal(state.keys.data, mockResponse);
- output = hooks.useProductRecommendationsData();
- });
- it('is not loading', () => {
- expect(output.isLoading).toEqual(false);
- });
- it('is loaded', () => {
- expect(output.isLoaded).toEqual(true);
- });
- it('has not failed', () => {
- expect(output.hasFailed).toEqual(false);
- });
- it('returns country code', () => {
- expect(output.productRecommendations).toEqual(mockResponse);
- });
- });
- describe('when the request is pending', () => {
- beforeEach(() => {
- state.mockVal(state.keys.requestState, RequestStates.pending);
- state.mockVal(state.keys.data, {});
- output = hooks.useProductRecommendationsData();
- });
- it('is loading', () => {
- expect(output.isLoading).toEqual(true);
- });
- it('is not loaded', () => {
- expect(output.isLoaded).toEqual(false);
- });
- it('has not failed', () => {
- expect(output.hasFailed).toEqual(false);
- });
- it('returns empty object', () => {
- expect(output.productRecommendations).toEqual({});
- });
- });
- describe('when the request has failed', () => {
- beforeEach(() => {
- state.mockVal(state.keys.requestState, RequestStates.failed);
- state.mockVal(state.keys.data, {});
- output = hooks.useProductRecommendationsData();
- });
- it('is not loading', () => {
- expect(output.isLoading).toEqual(false);
- });
- it('is not loaded', () => {
- expect(output.isLoaded).toEqual(false);
- });
- it('has failed', () => {
- expect(output.hasFailed).toEqual(true);
- });
- it('returns empty object', () => {
- expect(output.productRecommendations).toEqual({});
- });
- });
- });
- });
-});
diff --git a/src/widgets/ProductRecommendations/index.jsx b/src/widgets/ProductRecommendations/index.jsx
deleted file mode 100644
index 2b61877..0000000
--- a/src/widgets/ProductRecommendations/index.jsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react';
-import './index.scss';
-import { reduxHooks } from 'hooks';
-import NoCoursesView from 'containers/CoursesPanel/NoCoursesView';
-import LoadingView from './components/LoadingView';
-import LoadedView from './components/LoadedView';
-import hooks from './hooks';
-
-const ProductRecommendations = () => {
- const checkEmptyResponse = (obj) => {
- const values = Object.values(obj);
- const result = values.filter((item) => item.length === 0);
- return result.length === values.length;
- };
-
- const { productRecommendations, isLoading, isLoaded } = hooks.useProductRecommendationsData();
- const isMobile = hooks.useIsMobile();
- const hasCourses = reduxHooks.useHasCourses();
- const shouldShowPlaceholder = checkEmptyResponse(productRecommendations);
-
- if (isLoading && !isMobile) {
- return
;
- }
-
- if (isLoaded && !isMobile && !shouldShowPlaceholder) {
- return (
-
- );
- }
-
- if (isLoaded && hasCourses && !isMobile && shouldShowPlaceholder) {
- return
;
- }
- return null;
-};
-
-export default ProductRecommendations;
diff --git a/src/widgets/ProductRecommendations/index.scss b/src/widgets/ProductRecommendations/index.scss
deleted file mode 100644
index 78955c3..0000000
--- a/src/widgets/ProductRecommendations/index.scss
+++ /dev/null
@@ -1,73 +0,0 @@
-@import "@openedx/paragon/scss/core/core";
-
-$horizontal-card-gap: 20px;
-$vertical-card-gap: 24px;
-
-.base-card {
- height: 332px;
- width: 270px !important;
-
- .pgn__card-image-cap {
- height: 104px;
- object: {
- fit: cover;
- position: top center;
- }
- }
-
- .pgn__card-logo-cap {
- bottom: -1.5rem;
- object: {
- fit: scale-down;
- position: center center;
- }
- }
-
- .product-card-title {
- font: {
- size: 1.125rem;
- }
-
- line-height: 24px ;
- }
-
- .product-card-subtitle {
- font: {
- size: 0.875rem;
- }
-
- line-height: 24px;
- }
-
- .product-badge {
- bottom: 2.75rem;
- }
-}
-
-.product-card-container {
- gap: $vertical-card-gap $horizontal-card-gap;
- margin: 0 (-$horizontal-card-gap);
- padding: 1rem $horizontal-card-gap;
-
- .course-subcontainer {
- gap: $vertical-card-gap $horizontal-card-gap;
- }
-
- @include media-breakpoint-down(lg) {
- overflow-x: scroll;
- }
-}
-
-// Workaround for giving the sub-container a greyish background that stretches to the full width of the browser window
-// while being placed within the boundaries of the parent container's dimensions
-.recommendations-container::before {
- content: "";
- position: absolute;
- top: 0;
- left: 50%;
- transform: translateX(-50%);
- width: 100vw;
- height: 100%;
- background-color: $light-200;
- z-index: -1;
-}
diff --git a/src/widgets/ProductRecommendations/index.test.jsx b/src/widgets/ProductRecommendations/index.test.jsx
deleted file mode 100644
index 8ab3a4e..0000000
--- a/src/widgets/ProductRecommendations/index.test.jsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import React from 'react';
-import { shallow } from '@edx/react-unit-test-utils';
-import { reduxHooks } from 'hooks';
-import hooks from './hooks';
-import ProductRecommendations from './index';
-import LoadingView from './components/LoadingView';
-import LoadedView from './components/LoadedView';
-import NoCoursesView from '../../containers/CoursesPanel/NoCoursesView';
-import { mockCrossProductResponse, mockAmplitudeResponse } from './testData';
-
-jest.mock('./hooks', () => ({
- useProductRecommendationsData: jest.fn(),
- useIsMobile: jest.fn(),
-}));
-
-jest.mock('hooks', () => ({
- reduxHooks: {
- useHasCourses: jest.fn(),
- },
-}));
-
-jest.mock('./components/LoadingView', () => 'LoadingView');
-jest.mock('./components/LoadedView', () => 'LoadedView');
-jest.mock('containers/CoursesPanel/NoCoursesView', () => 'NoCoursesView');
-
-describe('ProductRecommendations', () => {
- const defaultValues = {
- productRecommendations: {},
- isLoading: false,
- isLoaded: false,
- hasFailed: false,
- };
-
- const successfulLoadValues = {
- ...defaultValues,
- isLoaded: true,
- productRecommendations: mockCrossProductResponse,
- };
-
- beforeEach(() => reduxHooks.useHasCourses.mockReturnValueOnce(true));
-
- it('matches snapshot', () => {
- hooks.useIsMobile.mockReturnValueOnce(false);
- hooks.useProductRecommendationsData.mockReturnValueOnce({
- ...successfulLoadValues,
- });
-
- expect(shallow(
).snapshot).toMatchSnapshot();
- });
-
- it('renders the LoadingView if the request is pending', () => {
- hooks.useIsMobile.mockReturnValueOnce(false);
- hooks.useProductRecommendationsData.mockReturnValueOnce({
- ...defaultValues,
- isLoading: true,
- });
-
- expect({ ...shallow(
).shallowWrapper, children: expect.any(Array) }).toMatchObject(
- shallow(
),
- );
- });
- it('renders nothing if the request has failed', () => {
- hooks.useIsMobile.mockReturnValueOnce(false);
- hooks.useProductRecommendationsData.mockReturnValueOnce({
- ...defaultValues,
- hasFailed: true,
- });
-
- const wrapper = shallow(
);
-
- expect(wrapper.shallowWrapper).toBeNull();
- });
- it('renders nothing if the user is on the mobile view', () => {
- hooks.useIsMobile.mockReturnValueOnce(true);
- hooks.useProductRecommendationsData.mockReturnValueOnce({
- ...successfulLoadValues,
- });
-
- const wrapper = shallow(
);
-
- expect(wrapper.shallowWrapper).toBeNull();
- });
-
- it('renders NoCoursesView if the request is loaded, user has courses, and the response is empty', () => {
- hooks.useIsMobile.mockReturnValueOnce(false);
- hooks.useProductRecommendationsData.mockReturnValueOnce({
- ...successfulLoadValues,
- productRecommendations: {
- amplitudeCourses: [],
- crossProductCourses: [],
- },
- });
-
- expect({ ...shallow(
).shallowWrapper, children: expect.any(Array) }).toMatchObject(
- shallow(
),
- );
- });
-
- describe('LoadedView', () => {
- it('renders with cross product data if the request completed and the user has courses', () => {
- hooks.useIsMobile.mockReturnValueOnce(false);
- hooks.useProductRecommendationsData.mockReturnValueOnce({
- ...successfulLoadValues,
- });
-
- expect({ ...shallow(
).shallowWrapper, children: expect.any(Array) }).toMatchObject(
- shallow(
-
,
- ),
- );
- });
-
- it('renders the LoadedView with Amplitude course data if the request completed', () => {
- hooks.useIsMobile.mockReturnValueOnce(false);
- hooks.useProductRecommendationsData.mockReturnValueOnce({
- ...successfulLoadValues,
- productRecommendations: mockAmplitudeResponse,
- });
-
- expect({ ...shallow(
).shallowWrapper, children: expect.any(Array) }).toMatchObject(
- shallow(
-
,
- ),
- );
- });
- });
-});
diff --git a/src/widgets/ProductRecommendations/messages.js b/src/widgets/ProductRecommendations/messages.js
deleted file mode 100644
index 1615181..0000000
--- a/src/widgets/ProductRecommendations/messages.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { defineMessages } from '@edx/frontend-platform/i18n';
-
-const messages = defineMessages({
- recommendationsHeading: {
- id: 'ProductRecommendations.recommendationsHeading',
- defaultMessage: 'You might also like',
- description: 'Title for a list of recommended courses',
- },
- executiveEducationHeading: {
- id: 'ProductRecommendations.executiveEducationHeading',
- defaultMessage: 'Executive Education',
- description: 'Heading for an executive education course recommendation',
- },
- executiveEducationDescription: {
- id: 'ProductRecommendations.executiveEducationDescription',
- defaultMessage: 'Short Courses to develop leadership skills',
- description: 'Short description of an executive education course',
- },
- bootcampHeading: {
- id: 'ProductRecommendations.bootcampHeading',
- defaultMessage: 'Boot Camp',
- description: 'Heading for a bootcamp course recommendation',
- },
- bootcampDescription: {
- id: 'ProductRecommendations.bootcampDescription',
- defaultMessage: 'Intensive, hands-on, project based training',
- description: 'Short description of a bootcamp course',
- },
- courseHeading: {
- id: 'ProductRecommendations.courseHeading',
- defaultMessage: 'Courses',
- description: 'Heading for an open course recommendation',
- },
- courseDescription: {
- id: 'ProductRecommendations.courseDescription',
- defaultMessage: 'Find new interests and advance your career',
- description: 'Heading for an open course recommendation',
- },
-});
-
-export default messages;
diff --git a/src/widgets/ProductRecommendations/optimizely.js b/src/widgets/ProductRecommendations/optimizely.js
deleted file mode 100644
index 504cd96..0000000
--- a/src/widgets/ProductRecommendations/optimizely.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { createInstance, setLogLevel } from '@optimizely/react-sdk';
-
-import { getConfig } from '@edx/frontend-platform';
-
-const OPTIMIZELY_SDK_KEY = getConfig().OPTIMIZELY_FULL_STACK_SDK_KEY;
-
-const configureClient = () => {
- setLogLevel('error');
-
- return createInstance({
- sdkKey: OPTIMIZELY_SDK_KEY,
- });
-};
-
-const optimizelyClient = configureClient();
-
-export default optimizelyClient;
diff --git a/src/widgets/ProductRecommendations/optimizely.test.js b/src/widgets/ProductRecommendations/optimizely.test.js
deleted file mode 100644
index a97631c..0000000
--- a/src/widgets/ProductRecommendations/optimizely.test.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { createInstance, setLogLevel } from '@optimizely/react-sdk';
-import optimizelyClient from './optimizely';
-
-jest.mock('@optimizely/react-sdk', () => ({
- createInstance: jest.fn(() => 'mockedClient'),
- setLogLevel: jest.fn(),
-}));
-
-jest.mock('@edx/frontend-platform', () => ({
- getConfig: jest.fn(() => ({ OPTIMIZELY_FULL_STACK_SDK_KEY: 'SDK Key' })),
-}));
-
-describe('optimizelyClient', () => {
- it('should configure an Optimizely client instance with the correct SDK key', () => {
- expect(optimizelyClient).toBeDefined();
- expect(setLogLevel).toHaveBeenCalledWith('error');
- expect(createInstance).toHaveBeenCalledWith({ sdkKey: 'SDK Key' });
- });
-});
diff --git a/src/widgets/ProductRecommendations/optimizelyExperiment.js b/src/widgets/ProductRecommendations/optimizelyExperiment.js
deleted file mode 100644
index 72b0b31..0000000
--- a/src/widgets/ProductRecommendations/optimizelyExperiment.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { StrictDict } from 'utils';
-import optimizelyClient from './optimizely';
-
-export const PRODUCT_RECOMMENDATIONS_EXP_KEY = 'learner_dashboard_product_recommendations_exp';
-export const PRODUCT_RECOMMENDATIONS_EXP_VARIATION = 'learner_dashboard_product_recommendations_enabled';
-
-export const eventNames = StrictDict({
- productRecommendationsViewed: 'product_recommendations_viewed',
- productHeaderClicked: 'product_header_clicked',
- productCardClicked: 'product_card_clicked',
- courseCardClicked: 'course_card_clicked',
-});
-
-export const activateProductRecommendationsExperiment = (userId, userAttributes) => {
- const variation = optimizelyClient?.activate(
- PRODUCT_RECOMMENDATIONS_EXP_KEY,
- userId,
- userAttributes,
- );
-
- return {
- experimentActivated: variation !== null,
- inExperimentVariant: variation === PRODUCT_RECOMMENDATIONS_EXP_VARIATION,
- };
-};
-
-export const trackProductRecommendationsViewed = (userId, userAttributes = {}) => {
- optimizelyClient.track(eventNames.productRecommendationsViewed, userId, userAttributes);
-};
-
-export const trackProductHeaderClicked = (userId, userAttributes = {}) => {
- optimizelyClient.track(eventNames.productHeaderClicked, userId, userAttributes);
-};
-
-export const trackProductCardClicked = (userId, userAttributes = {}) => {
- optimizelyClient.track(eventNames.productCardClicked, userId, userAttributes);
-};
-
-export const trackCourseCardClicked = (userId, userAttributes = {}) => {
- optimizelyClient.track(eventNames.courseCardClicked, userId, userAttributes);
-};
diff --git a/src/widgets/ProductRecommendations/optimizelyExperiment.test.js b/src/widgets/ProductRecommendations/optimizelyExperiment.test.js
deleted file mode 100644
index dbd57d9..0000000
--- a/src/widgets/ProductRecommendations/optimizelyExperiment.test.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import optimizelyClient from './optimizely';
-import {
- eventNames,
- PRODUCT_RECOMMENDATIONS_EXP_KEY,
- PRODUCT_RECOMMENDATIONS_EXP_VARIATION,
- activateProductRecommendationsExperiment,
- trackProductRecommendationsViewed,
- trackProductHeaderClicked,
- trackProductCardClicked,
- trackCourseCardClicked,
-} from './optimizelyExperiment';
-
-jest.mock('./optimizely', () => ({
- activate: jest.fn(),
- track: jest.fn(),
-}));
-
-const userId = '1';
-const userAttributes = {
- is_enterprise_user: false,
- location: 'us',
- is_mobile_user: false,
-};
-
-describe('Optimizely events', () => {
- describe('activateProductRecommendationsExperiment', () => {
- it('activates the experiment and returns in experiment variant', () => {
- optimizelyClient.activate.mockReturnValueOnce(PRODUCT_RECOMMENDATIONS_EXP_VARIATION);
- const experiment = activateProductRecommendationsExperiment(userId, userAttributes);
-
- expect(experiment.experimentActivated).toBeTruthy();
- expect(experiment.inExperimentVariant).toBeTruthy();
- expect(optimizelyClient.activate).toHaveBeenCalledWith(
- PRODUCT_RECOMMENDATIONS_EXP_KEY,
- userId,
- userAttributes,
- );
- });
-
- it('does not activate the experiment and returns not in experiment variant', () => {
- optimizelyClient.activate.mockReturnValueOnce(null);
- const experiment = activateProductRecommendationsExperiment(userId, userAttributes);
-
- expect(experiment.experimentActivated).toBeFalsy();
- expect(experiment.inExperimentVariant).toBeFalsy();
- expect(optimizelyClient.activate).toHaveBeenCalledWith(
- PRODUCT_RECOMMENDATIONS_EXP_KEY,
- userId,
- userAttributes,
- );
- });
- });
- describe('trackProductRecommendationsViewed', () => {
- it('sends the productRecommendationsViewed event', () => {
- trackProductRecommendationsViewed(userId);
- expect(optimizelyClient.track).toHaveBeenCalledWith(
- eventNames.productRecommendationsViewed,
- userId,
- {},
- );
- });
- });
- describe('trackProductHeaderClicked', () => {
- it('sends the productHeaderClicked event', () => {
- trackProductHeaderClicked(userId);
- expect(optimizelyClient.track).toHaveBeenCalledWith(
- eventNames.productHeaderClicked,
- userId,
- {},
- );
- });
- });
- describe('trackProductCardClicked', () => {
- it('sends the productCardClicked event', () => {
- trackProductCardClicked(userId);
- expect(optimizelyClient.track).toHaveBeenCalledWith(
- eventNames.productCardClicked,
- userId,
- {},
- );
- });
- });
- describe('trackCourseCardClicked', () => {
- it('sends the courseCardClicked event', () => {
- trackCourseCardClicked(userId);
- expect(optimizelyClient.track).toHaveBeenCalledWith(
- eventNames.courseCardClicked,
- userId,
- {},
- );
- });
- });
-});
diff --git a/src/widgets/ProductRecommendations/testData.js b/src/widgets/ProductRecommendations/testData.js
deleted file mode 100644
index 45739a2..0000000
--- a/src/widgets/ProductRecommendations/testData.js
+++ /dev/null
@@ -1,46 +0,0 @@
-export const getCoursesWithType = (courseTypes, parameters = true) => {
- const courses = [];
- const marketingUrl = parameters
- ? 'https://www.edx.org/course/some-course?utm_source=source'
- : 'https://www.edx.org/course/some-course';
-
- courseTypes.forEach((type) => {
- courses.push({
- title: 'Introduction to Computer Science',
- courseRunKey: 'course-v1:Test+Course+2022T2',
- marketingUrl,
- courseType: type,
- image: {
- src: 'https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg',
- },
- owners: [
- {
- key: 'HarvardX',
- name: 'Harvard University',
- logoImageUrl: 'http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png',
- },
- ],
- });
- });
-
- return courses;
-};
-
-export const mockFooterRecommendationsHook = {
- default: { isExperimentActive: false, inRecommendationsVariant: true },
- activeControl: { isExperimentActive: true, inRecommendationsVariant: false },
- activeTreatment: { isExperimentActive: true, inRecommendationsVariant: true },
-};
-
-export const mockCrossProductCourses = getCoursesWithType(['executive-education-2u', 'bootcamp-2u']);
-export const mockOpenCourses = getCoursesWithType(['verified-audit', 'audit', 'verified', 'course']);
-export const mockFallbackOpenCourse = getCoursesWithType(['course'], false);
-
-export const mockCrossProductResponse = {
- crossProductCourses: mockCrossProductCourses,
- amplitudeCourses: mockOpenCourses,
-};
-
-export const mockAmplitudeResponse = {
- amplitudeCourses: mockOpenCourses,
-};
diff --git a/src/widgets/ProductRecommendations/track.js b/src/widgets/ProductRecommendations/track.js
deleted file mode 100644
index 6afe292..0000000
--- a/src/widgets/ProductRecommendations/track.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import { StrictDict } from 'utils';
-import { createLinkTracker, createEventTracker } from 'data/services/segment/utils';
-import { courseTypeToProductLineMap, convertCourseRunKeyToCourseKey } from './utils';
-
-export const eventNames = StrictDict({
- productCardClicked: 'edx.bi.2u-product-card.clicked',
- discoveryCardClicked: 'edx.bi.user.discovery.card.click',
- recommendationsHeaderClicked: 'edx.bi.link.recommendations.header.clicked',
- recommendationsViewed: 'edx.bi.user.recommendations.viewed',
-});
-
-export const productCardClicked = (courseRunKey, courseTitle, courseType, href) => {
- createLinkTracker(
- createEventTracker(eventNames.productCardClicked, {
- label: courseTitle,
- courserun_key: courseRunKey,
- page: 'dashboard',
- product_line: courseTypeToProductLineMap[courseType],
- }),
- href,
- );
-};
-
-export const discoveryCardClicked = (courseRunKey, courseTitle, href) => {
- createLinkTracker(
- createEventTracker(eventNames.discoveryCardClicked, {
- label: courseTitle,
- courserun_key: courseRunKey,
- page: 'dashboard',
- product_line: 'open-course',
- }),
- href,
- );
-};
-
-export const recommendationsHeaderClicked = (courseType, href) => {
- createLinkTracker(
- createEventTracker(eventNames.recommendationsHeaderClicked, {
- page: 'dashboard',
- product_line: courseTypeToProductLineMap[courseType],
- }),
- href,
- );
-};
-
-export const recommendationsViewed = (isControl, recommenderGroup, courseRunKey) => {
- createEventTracker(eventNames.recommendationsViewed, {
- is_control: isControl,
- productRecommenderGroup: recommenderGroup,
- page: 'dashboard',
- course_key: courseRunKey ? convertCourseRunKeyToCourseKey(courseRunKey) : '',
- });
-};
diff --git a/src/widgets/ProductRecommendations/track.test.js b/src/widgets/ProductRecommendations/track.test.js
deleted file mode 100644
index 7fcd583..0000000
--- a/src/widgets/ProductRecommendations/track.test.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import { createLinkTracker, createEventTracker } from 'data/services/segment/utils';
-import { bootCamp, treatment, control } from './constants';
-import {
- eventNames,
- productCardClicked,
- discoveryCardClicked,
- recommendationsHeaderClicked,
- recommendationsViewed,
-} from './track';
-
-jest.mock('data/services/segment/utils', () => ({
- createEventTracker: jest.fn((args) => ({ createEventTracker: args })),
- createLinkTracker: jest.fn((args) => ({ createLinkTracker: args })),
-}));
-
-const courseKey = 'MITx+5.0.01';
-const courseRunKeyNew = `course-v1:${courseKey}+2022T2`;
-const courseRunKeyOld = 'MITx/5.0.01/2022T2/';
-const label = 'Web Design';
-const headerLink = 'https://www.edx.org/search?tab=course?linked_from=recommender';
-const courseUrl = 'https://www.edx.org/course/some-course';
-
-describe('product recommendations trackers', () => {
- describe('recommendationsViewed', () => {
- describe('with old course run key format', () => {
- it('creates an event tracker for when cross product recommendations are present', () => {
- recommendationsViewed(false, treatment, courseRunKeyOld);
- expect(createEventTracker).toHaveBeenCalledWith(
- eventNames.recommendationsViewed,
- {
- is_control: false,
- productRecommenderGroup: treatment,
- page: 'dashboard',
- course_key: courseKey,
- },
- );
- });
- });
- describe('with new course run key format', () => {
- it('creates an event tracker for when a user is bucketed into the conrol group', () => {
- recommendationsViewed(false, control, courseRunKeyNew);
-
- expect(createEventTracker).toHaveBeenCalledWith(
- eventNames.recommendationsViewed,
- {
- is_control: false,
- productRecommenderGroup: control,
- page: 'dashboard',
- course_key: courseKey,
- },
- );
- });
- });
- describe('with no course run key', () => {
- it('creates an event tracker for when a user is bucketed into the conrol group', () => {
- recommendationsViewed(false, control, '');
- expect(createEventTracker).toHaveBeenCalledWith(
- eventNames.recommendationsViewed,
- {
- is_control: false,
- productRecommenderGroup: control,
- page: 'dashboard',
- course_key: '',
- },
- );
- });
- });
- });
- describe('recommendationsHeaderClicked', () => {
- it('creates a link tracker for when a recommendations header is clicked', () => {
- const attributes = {
- product_line: 'open-courses',
- page: 'dashboard',
- };
- const args = [eventNames.recommendationsHeaderClicked, attributes];
-
- recommendationsHeaderClicked('Course', headerLink);
- expect(createEventTracker).toHaveBeenCalledWith(...args);
- expect(createLinkTracker).toHaveBeenCalledWith(createEventTracker(...args), headerLink);
- });
- });
- describe('discoveryCardClicked', () => {
- it('creates a link tracker for when a open course card is clicked', () => {
- const attributes = {
- label,
- courserun_key: courseRunKeyNew,
- page: 'dashboard',
- product_line: 'open-course',
- };
- const args = [eventNames.discoveryCardClicked, attributes];
-
- discoveryCardClicked(courseRunKeyNew, label, courseUrl);
- expect(createEventTracker).toHaveBeenCalledWith(...args);
- expect(createLinkTracker).toHaveBeenCalledWith(createEventTracker(...args), courseUrl);
- });
- });
- describe('productCardClicked', () => {
- it('creates a link tracker for when a cross product course card is clicked', () => {
- const attributes = {
- label,
- courserun_key: courseRunKeyNew,
- page: 'dashboard',
- product_line: 'boot-camps',
- };
- const args = [eventNames.productCardClicked, attributes];
-
- productCardClicked(courseRunKeyNew, label, bootCamp, courseUrl);
- expect(createEventTracker).toHaveBeenCalledWith(...args);
- expect(createLinkTracker).toHaveBeenCalledWith(createEventTracker(...args), courseUrl);
- });
- });
-});
diff --git a/src/widgets/ProductRecommendations/utils.js b/src/widgets/ProductRecommendations/utils.js
deleted file mode 100644
index 36b8b93..0000000
--- a/src/widgets/ProductRecommendations/utils.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import PropTypes from 'prop-types';
-import { executiveEducation, course, bootCamp } from './constants';
-
-export const courseShape = {
- uuid: PropTypes.string,
- title: PropTypes.string,
- image: PropTypes.shape({
- src: PropTypes.string,
- }),
- prospectusPath: PropTypes.string,
- owners: PropTypes.arrayOf(
- PropTypes.shape({
- key: PropTypes.string,
- name: PropTypes.string,
- logoImageUrl: PropTypes.string,
- }),
- ),
- activeCourseRun: PropTypes.shape({
- key: PropTypes.string,
- marketingUrl: PropTypes.string,
- }),
- courseType: PropTypes.string,
-};
-
-export const courseTypeToProductTypeMap = {
- course,
- 'verified-audit': course,
- verified: course,
- audit: course,
- 'credit-verified-audit': course,
- 'spoc-verified-audit': course,
- professional: 'Professional Certificate',
- 'bootcamp-2u': bootCamp,
- 'executive-education-2u': executiveEducation,
- 'executive-education': executiveEducation,
- masters: "Master's",
- 'masters-verified-audit': "Master's",
-};
-
-export const courseTypeToProductLineMap = {
- [executiveEducation]: 'executive-education',
- [bootCamp]: 'boot-camps',
- [course]: 'open-courses',
-};
-
-export const convertCourseRunKeyToCourseKey = (courseRunKey) => {
- const newKeyFormat = courseRunKey.includes('+');
- if (newKeyFormat) {
- const splitCourseRunKey = courseRunKey.split(':').slice(-1)[0];
- const splitCourseKey = splitCourseRunKey.split('+').slice(0, 2);
- return `${splitCourseKey[0]}+${splitCourseKey[1]}`;
- }
- const splitCourseKey = courseRunKey.split('/').slice(0, 2);
- return `${splitCourseKey[0]}+${splitCourseKey[1]}`;
-};
-
-export const wait = (time) => new Promise((resolve) => {
- setTimeout(resolve, time);
-});