feat!: Remove RecommendationsPanel (#437)
This commit is contained in:
@@ -8,7 +8,7 @@ import { reduxHooks } from 'hooks';
|
||||
import moreCoursesSVG from 'assets/more-courses-sidewidget.svg';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import track from '../RecommendationsPanel/track';
|
||||
import track from './track';
|
||||
import messages from './messages';
|
||||
import './index.scss';
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../RecommendationsPanel/track', () => ({
|
||||
jest.mock('./track', () => ({
|
||||
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
|
||||
}));
|
||||
|
||||
|
||||
10
src/widgets/LookingForChallengeWidget/track.js
Normal file
10
src/widgets/LookingForChallengeWidget/track.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import track from 'tracking';
|
||||
|
||||
export const linkNames = StrictDict({
|
||||
findCoursesWidget: 'learner_home_widget_explore',
|
||||
});
|
||||
|
||||
export const findCoursesWidgetClicked = (href) => track.findCourses.findCoursesClicked(href, {
|
||||
linkName: linkNames.findCoursesWidget,
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { Search } from '@openedx/paragon/icons';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from './track';
|
||||
import CourseCard from './components/CourseCard';
|
||||
import messages from './messages';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const LoadedView = ({
|
||||
courses,
|
||||
isControl,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
|
||||
return (
|
||||
<div className="p-4 w-100 panel-background">
|
||||
<h3 className="pb-2">{isControl === false
|
||||
? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)}
|
||||
</h3>
|
||||
<div>
|
||||
{courses.map((course) => (
|
||||
<CourseCard
|
||||
key={course.courseKey}
|
||||
course={course}
|
||||
isControl={isControl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center explore-courses-btn">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
iconBefore={Search}
|
||||
as="a"
|
||||
href={baseAppUrl(courseSearchUrl)}
|
||||
onClick={track.findCoursesWidgetClicked(baseAppUrl(courseSearchUrl))}
|
||||
>
|
||||
{formatMessage(messages.exploreCoursesButton)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LoadedView.defaultProps = {
|
||||
isControl: true,
|
||||
};
|
||||
|
||||
LoadedView.propTypes = {
|
||||
courses: PropTypes.arrayOf(PropTypes.shape({
|
||||
courseKey: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
logoImageUrl: PropTypes.string,
|
||||
marketingUrl: PropTypes.string,
|
||||
})).isRequired,
|
||||
isControl: PropTypes.oneOf([true, false, null]),
|
||||
};
|
||||
|
||||
export default LoadedView;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import LoadedView from './LoadedView';
|
||||
import mockData from './mockData';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: '/course-search-url',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
baseAppUrl: (url) => (`http://localhost:18000${url}`),
|
||||
}));
|
||||
jest.mock('./track', () => ({
|
||||
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
|
||||
}));
|
||||
jest.mock('./components/CourseCard', () => 'CourseCard');
|
||||
|
||||
describe('RecommendationsPanel LoadedView', () => {
|
||||
const props = {
|
||||
courses: mockData.courses,
|
||||
isControl: null,
|
||||
};
|
||||
|
||||
describe('RecommendationPanelLoadedView', () => {
|
||||
test('without personalize recommendation', () => {
|
||||
const el = shallow(<LoadedView {...props} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.instance.findByType('h3')[0].children[0].el).toEqual(messages.popularCoursesHeading.defaultMessage);
|
||||
});
|
||||
|
||||
test('with personalize recommendation', () => {
|
||||
const el = shallow(<LoadedView {...props} isControl={false} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.instance.findByType('h3')[0].children[0].el).toEqual(messages.recommendationsHeading.defaultMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Spinner } from '@openedx/paragon';
|
||||
|
||||
import { useDashboardMessages } from 'containers/Dashboard/hooks';
|
||||
|
||||
export const LoadingView = () => {
|
||||
const { spinnerScreenReaderText } = useDashboardMessages();
|
||||
return (
|
||||
<div className="recommendations-loading w-100">
|
||||
<Spinner
|
||||
animation="border"
|
||||
variant="light"
|
||||
screenReaderText={spinnerScreenReaderText}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingView;
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { useDashboardMessages } from 'containers/Dashboard/hooks';
|
||||
import LoadingView from './LoadingView';
|
||||
|
||||
jest.mock('./components/CourseCard', () => 'CourseCard');
|
||||
jest.mock('containers/Dashboard/hooks', () => ({
|
||||
useDashboardMessages: jest.fn(),
|
||||
}));
|
||||
|
||||
const spinnerScreenReaderText = 'test-spinner-screen-reader-text';
|
||||
useDashboardMessages.mockReturnValue(spinnerScreenReaderText);
|
||||
|
||||
describe('RecommendationsPanel LoadingView', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<LoadingView />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,151 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RecommendationsPanel LoadedView RecommendationPanelLoadedView with personalize recommendation 1`] = `
|
||||
<div
|
||||
className="p-4 w-100 panel-background"
|
||||
>
|
||||
<h3
|
||||
className="pb-2"
|
||||
>
|
||||
Recommendations for you
|
||||
</h3>
|
||||
<div>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-1",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 1",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-1"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-2",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 2 with a really really really long name for some reason",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-2"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-3",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 3",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-3"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-4",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 4",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-center explore-courses-btn"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
|
||||
variant="tertiary"
|
||||
>
|
||||
Explore courses
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RecommendationsPanel LoadedView RecommendationPanelLoadedView without personalize recommendation 1`] = `
|
||||
<div
|
||||
className="p-4 w-100 panel-background"
|
||||
>
|
||||
<h3
|
||||
className="pb-2"
|
||||
>
|
||||
Popular courses
|
||||
</h3>
|
||||
<div>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-1",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 1",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-1"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-2",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 2 with a really really really long name for some reason",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-2"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-3",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 3",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-3"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
{
|
||||
"courseKey": "cs-4",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 4",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-center explore-courses-btn"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
|
||||
variant="tertiary"
|
||||
>
|
||||
Explore courses
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RecommendationsPanel LoadingView snapshot 1`] = `
|
||||
<div
|
||||
className="recommendations-loading w-100"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
variant="light"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { get, stringifyUrl } from 'data/services/lms/utils';
|
||||
import urls from 'data/services/lms/urls';
|
||||
|
||||
export const getFetchUrl = () => (`${urls.getApiUrl()}/edx_recommendations/learner_dashboard/course_recommendations/`);
|
||||
export const apiKeys = StrictDict({ user: 'user' });
|
||||
|
||||
const fetchRecommendedCourses = () => get(stringifyUrl(getFetchUrl()));
|
||||
|
||||
export default {
|
||||
fetchRecommendedCourses,
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { get, stringifyUrl } from 'data/services/lms/utils';
|
||||
import api, { getFetchUrl } from './api';
|
||||
|
||||
jest.mock('data/services/lms/utils', () => ({
|
||||
stringifyUrl: (...args) => ({ stringifyUrl: args }),
|
||||
get: (...args) => ({ get: args }),
|
||||
}));
|
||||
|
||||
describe('recommendedCourses api', () => {
|
||||
describe('fetchRecommendedCourses', () => {
|
||||
it('calls get with the correct recommendation courses URL and user', () => {
|
||||
expect(api.fetchRecommendedCourses()).toEqual(
|
||||
get(stringifyUrl(getFetchUrl())),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Card, Hyperlink, Truncate } from '@openedx/paragon';
|
||||
|
||||
import { useIsCollapsed } from 'containers/CourseCard/hooks';
|
||||
import useCourseCardData from './hooks';
|
||||
import './index.scss';
|
||||
|
||||
export const CourseCard = ({ course, isControl }) => {
|
||||
const isCollapsed = useIsCollapsed();
|
||||
const { handleCourseClick } = useCourseCardData(course, isControl);
|
||||
|
||||
return (
|
||||
<Hyperlink
|
||||
destination={course?.marketingUrl}
|
||||
className="card-link"
|
||||
onClick={handleCourseClick}
|
||||
>
|
||||
<Card orientation={isCollapsed ? 'vertical' : 'horizontal'} className="p-3 mb-1 recommended-course-card">
|
||||
<div className={isCollapsed ? '' : 'd-flex align-items-center'}>
|
||||
<Card.ImageCap
|
||||
src={course.logoImageUrl}
|
||||
srcAlt={course.title}
|
||||
/>
|
||||
<Card.Body className="d-flex align-items-center">
|
||||
<Card.Section className={isCollapsed ? 'pt-3' : 'pl-3'}>
|
||||
<h4 className="text-info-500">
|
||||
<Truncate lines={3}>
|
||||
{course.title}
|
||||
</Truncate>
|
||||
</h4>
|
||||
</Card.Section>
|
||||
</Card.Body>
|
||||
</div>
|
||||
</Card>
|
||||
</Hyperlink>
|
||||
);
|
||||
};
|
||||
|
||||
CourseCard.propTypes = {
|
||||
course: PropTypes.shape({
|
||||
courseKey: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
logoImageUrl: PropTypes.string,
|
||||
marketingUrl: PropTypes.string,
|
||||
}).isRequired,
|
||||
isControl: PropTypes.oneOf([true, false, null]).isRequired,
|
||||
};
|
||||
|
||||
export default CourseCard;
|
||||
@@ -1,17 +0,0 @@
|
||||
import track from '../track';
|
||||
import './index.scss';
|
||||
|
||||
export const useCourseCardData = (course, isControl) => {
|
||||
const handleCourseClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
track.recommendedCourseClicked(
|
||||
course.courseKey,
|
||||
isControl,
|
||||
course?.marketingUrl,
|
||||
)(e);
|
||||
};
|
||||
return { handleCourseClick };
|
||||
};
|
||||
|
||||
export default useCourseCardData;
|
||||
@@ -1,33 +0,0 @@
|
||||
@import "@openedx/paragon/scss/core/core";
|
||||
|
||||
.card-link{
|
||||
display: block !important;
|
||||
margin: 0.5rem 0 0.5rem 0 !important;
|
||||
}
|
||||
|
||||
.recommended-course-card {
|
||||
margin: 0.5rem 0 0.5rem 0 !important;
|
||||
|
||||
.pgn__card-wrapper-image-cap {
|
||||
width: 7.188rem !important;
|
||||
max-width: 7.188rem !important;
|
||||
min-width: 7.188rem !important;
|
||||
height: 4.125rem !important;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15), 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
|
||||
.pgn__card-image-cap {
|
||||
max-width: 100% !important;
|
||||
max-height: 100% !important;
|
||||
}
|
||||
}
|
||||
.pgn__card-section {
|
||||
padding: 0 !important;
|
||||
}
|
||||
margin-top: 0.313rem;
|
||||
}
|
||||
|
||||
.text-info-500 {
|
||||
margin: 0 !important;
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { RequestStates } from 'data/constants/requests';
|
||||
|
||||
import * as module from './hooks';
|
||||
import testData from './testData';
|
||||
import api from './api';
|
||||
|
||||
export const searchCourseEventName = 'learner_home.widget.search_course';
|
||||
|
||||
export const state = StrictDict({
|
||||
requestState: (val) => React.useState(val), // eslint-disable-line
|
||||
data: (val) => React.useState(val), // eslint-disable-line
|
||||
courses: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useFetchCourses = (setRequestState, setData) => {
|
||||
React.useEffect(() => {
|
||||
let isMounted = true;
|
||||
api.fetchRecommendedCourses().then((response) => {
|
||||
if (isMounted) {
|
||||
setRequestState(RequestStates.completed);
|
||||
setData(response);
|
||||
}
|
||||
}).catch(() => {
|
||||
if (isMounted) {
|
||||
setRequestState(RequestStates.failed);
|
||||
}
|
||||
});
|
||||
return () => { isMounted = false; };
|
||||
/* eslint-disable */
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useRecommendationPanelData = () => {
|
||||
const [requestState, setRequestState] = module.state.requestState(RequestStates.pending);
|
||||
const [data, setData] = module.state.data({});
|
||||
module.useFetchCourses(setRequestState, setData);
|
||||
const [courses, setCourses] = module.state.courses(data.data?.courses || []);
|
||||
const isControl = data.data?.isControl === undefined ? null : data.data?.isControl;
|
||||
|
||||
React.useEffect(() => {
|
||||
window.loadMockRecommendations = () => {
|
||||
setCourses(testData.courses);
|
||||
setRequestState(RequestStates.completed);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
setCourses(data.data?.courses || []);
|
||||
}, [data]);
|
||||
|
||||
return {
|
||||
courses,
|
||||
isControl,
|
||||
isLoaded: requestState === RequestStates.completed && courses.length > 0,
|
||||
isFailed: requestState === RequestStates.failed
|
||||
|| (requestState === RequestStates.completed && courses.length === 0),
|
||||
isLoading: requestState === RequestStates.pending,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
useRecommendationPanelData,
|
||||
};
|
||||
@@ -1,191 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { RequestStates } from 'data/constants/requests';
|
||||
|
||||
import api from './api';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('./api', () => ({
|
||||
fetchRecommendedCourses: jest.fn(),
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const testList = [1, 2, 3];
|
||||
let out;
|
||||
describe('RecommendationsPanel hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
describe('state fields', () => {
|
||||
state.testGetter(state.keys.requestState);
|
||||
});
|
||||
describe('useFetchCourse', () => {
|
||||
describe('behavior', () => {
|
||||
describe('useEffect call', () => {
|
||||
let calls;
|
||||
let cb;
|
||||
let prereqs;
|
||||
const response = 'test-response';
|
||||
const setRequestState = jest.fn();
|
||||
const setData = jest.fn();
|
||||
beforeEach(() => {
|
||||
out = hooks.useFetchCourses(setRequestState, setData);
|
||||
({ calls } = React.useEffect.mock);
|
||||
([[cb, prereqs]] = calls);
|
||||
});
|
||||
it('calls useEffect once', () => {
|
||||
expect(calls.length).toEqual(1);
|
||||
});
|
||||
it('it is only run once (no prereqs)', () => {
|
||||
expect(prereqs).toEqual([]);
|
||||
});
|
||||
it('calls fetchRecommendedCourses', () => {
|
||||
api.fetchRecommendedCourses.mockReturnValueOnce(Promise.resolve(response));
|
||||
cb();
|
||||
expect(api.fetchRecommendedCourses).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('successful fetch on mounted component', () => {
|
||||
it('sets request state to completed and loads response', async () => {
|
||||
let resolveFn;
|
||||
api.fetchRecommendedCourses.mockReturnValueOnce(new Promise(resolve => {
|
||||
resolveFn = resolve;
|
||||
}));
|
||||
cb();
|
||||
expect(api.fetchRecommendedCourses).toHaveBeenCalledWith();
|
||||
expect(setRequestState).not.toHaveBeenCalled();
|
||||
expect(setData).not.toHaveBeenCalledWith(response);
|
||||
await resolveFn(response);
|
||||
expect(setRequestState).toHaveBeenCalledWith(RequestStates.completed);
|
||||
expect(setData).toHaveBeenCalledWith(response);
|
||||
});
|
||||
});
|
||||
describe('successful fetch on unmounted component', () => {
|
||||
it('it does nothing', async () => {
|
||||
let resolveFn;
|
||||
api.fetchRecommendedCourses.mockReturnValueOnce(new Promise(resolve => {
|
||||
resolveFn = resolve;
|
||||
}));
|
||||
const unMount = cb();
|
||||
expect(api.fetchRecommendedCourses).toHaveBeenCalledWith();
|
||||
expect(setRequestState).not.toHaveBeenCalled();
|
||||
expect(setData).not.toHaveBeenCalledWith(response);
|
||||
unMount();
|
||||
await resolveFn(response);
|
||||
expect(setRequestState).not.toHaveBeenCalled();
|
||||
expect(setData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('useRecommendationPanelData', () => {
|
||||
let fetchSpy;
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
fetchSpy = jest.spyOn(hooks, 'useFetchCourses').mockImplementationOnce(() => {});
|
||||
out = hooks.useRecommendationPanelData();
|
||||
});
|
||||
it('calls useFetchCourses with setRequestState and setData', () => {
|
||||
expect(fetchSpy).toHaveBeenCalledWith(state.setState.requestState, state.setState.data);
|
||||
});
|
||||
it('initializes requestState as RequestStates.pending', () => {
|
||||
state.expectInitializedWith(state.keys.requestState, RequestStates.pending);
|
||||
});
|
||||
it('initializes requestState as RequestStates.pending', () => {
|
||||
state.expectInitializedWith(state.keys.requestState, RequestStates.pending);
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('request is completed, with returned courses', () => {
|
||||
beforeEach(() => {
|
||||
state.mockVal(state.keys.requestState, RequestStates.completed);
|
||||
state.mockVal(state.keys.data, { data: { courses: testList } });
|
||||
out = hooks.useRecommendationPanelData();
|
||||
});
|
||||
it('is not loading', () => {
|
||||
expect(out.isLoading).toEqual(false);
|
||||
});
|
||||
it('is loaded', () => {
|
||||
expect(out.isLoaded).toEqual(true);
|
||||
});
|
||||
it('is not failed', () => {
|
||||
expect(out.isFailed).toEqual(false);
|
||||
});
|
||||
it('returns passed courses list', () => {
|
||||
expect(out.courses).toEqual(testList);
|
||||
});
|
||||
});
|
||||
describe('personalize recommendation', () => {
|
||||
it('default to null', () => {
|
||||
state.mockVal(state.keys.data, {});
|
||||
out = hooks.useRecommendationPanelData();
|
||||
expect(out.isControl).toEqual(null);
|
||||
});
|
||||
it('is based on data', () => {
|
||||
const expectOutput = { test: 'abirary' };
|
||||
state.mockVal(state.keys.data, { data: { isControl: expectOutput } });
|
||||
out = hooks.useRecommendationPanelData();
|
||||
expect(out.isControl).toEqual(expectOutput);
|
||||
});
|
||||
});
|
||||
describe('request is completed, with no returned courses', () => {
|
||||
beforeEach(() => {
|
||||
state.mockVal(state.keys.requestState, RequestStates.completed);
|
||||
state.mockVal(state.keys.data, { data: { courses: [] } });
|
||||
out = hooks.useRecommendationPanelData();
|
||||
});
|
||||
it('is not loading', () => {
|
||||
expect(out.isLoading).toEqual(false);
|
||||
});
|
||||
it('is not loaded', () => {
|
||||
expect(out.isLoaded).toEqual(false);
|
||||
});
|
||||
it('is failed', () => {
|
||||
expect(out.isFailed).toEqual(true);
|
||||
});
|
||||
it('returns empty courses list', () => {
|
||||
expect(out.courses).toEqual([]);
|
||||
});
|
||||
});
|
||||
describe('request is failed', () => {
|
||||
beforeEach(() => {
|
||||
state.mockVal(state.keys.requestState, RequestStates.failed);
|
||||
state.mockVal(state.keys.data, {});
|
||||
out = hooks.useRecommendationPanelData();
|
||||
});
|
||||
it('is not loading', () => {
|
||||
expect(out.isLoading).toEqual(false);
|
||||
});
|
||||
it('is not loaded', () => {
|
||||
expect(out.isLoaded).toEqual(false);
|
||||
});
|
||||
it('is failed', () => {
|
||||
expect(out.isFailed).toEqual(true);
|
||||
});
|
||||
it('returns empty courses list', () => {
|
||||
expect(out.courses).toEqual([]);
|
||||
});
|
||||
});
|
||||
describe('request is pending', () => {
|
||||
beforeEach(() => {
|
||||
state.mockVal(state.keys.requestState, RequestStates.pending);
|
||||
state.mockVal(state.keys.data, {});
|
||||
out = hooks.useRecommendationPanelData();
|
||||
});
|
||||
it('is loading', () => {
|
||||
expect(out.isLoading).toEqual(true);
|
||||
});
|
||||
it('is not loaded', () => {
|
||||
expect(out.isLoaded).toEqual(false);
|
||||
});
|
||||
it('is not failed', () => {
|
||||
expect(out.isFailed).toEqual(false);
|
||||
});
|
||||
it('returns empty courses list', () => {
|
||||
expect(out.courses).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
|
||||
import LoadingView from './LoadingView';
|
||||
import LoadedView from './LoadedView';
|
||||
import hooks from './hooks';
|
||||
|
||||
export const RecommendationsPanel = () => {
|
||||
const {
|
||||
courses,
|
||||
isControl,
|
||||
isFailed,
|
||||
isLoaded,
|
||||
isLoading,
|
||||
} = hooks.useRecommendationPanelData();
|
||||
|
||||
if (isLoading) {
|
||||
return (<LoadingView />);
|
||||
}
|
||||
if (isLoaded && courses.length > 0) {
|
||||
return (
|
||||
<LoadedView courses={courses} isControl={isControl} />
|
||||
);
|
||||
}
|
||||
if (isFailed) {
|
||||
return (<LookingForChallengeWidget />);
|
||||
}
|
||||
// default fallback
|
||||
return (<LookingForChallengeWidget />);
|
||||
};
|
||||
|
||||
export default RecommendationsPanel;
|
||||
@@ -1,17 +0,0 @@
|
||||
@import "@openedx/paragon/scss/core/core";
|
||||
|
||||
.explore-courses-btn {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.panel-background {
|
||||
background: $light-200;
|
||||
}
|
||||
|
||||
.recommendations-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 7.813rem;
|
||||
background: $light-200;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
|
||||
import hooks from './hooks';
|
||||
import mockData from './mockData';
|
||||
import LoadedView from './LoadedView';
|
||||
import LoadingView from './LoadingView';
|
||||
import RecommendationsPanel from '.';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useRecommendationPanelData: jest.fn(),
|
||||
}));
|
||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||
jest.mock('./LoadingView', () => 'LoadingView');
|
||||
jest.mock('./LoadedView', () => 'LoadedView');
|
||||
|
||||
const { courses } = mockData;
|
||||
|
||||
describe('RecommendationsPanel snapshot', () => {
|
||||
const defaultLoadedViewProps = {
|
||||
courses: [],
|
||||
isControl: false,
|
||||
};
|
||||
const defaultValues = {
|
||||
isFailed: false,
|
||||
isLoaded: false,
|
||||
isLoading: false,
|
||||
...defaultLoadedViewProps,
|
||||
};
|
||||
describe('RecommendationsPanel recommendations tests', () => {
|
||||
it('displays LoadingView if request is loading', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
...defaultValues,
|
||||
isLoading: true,
|
||||
});
|
||||
expect({ ...shallow(<RecommendationsPanel />).shallowWrapper, children: expect.any(Array) })
|
||||
.toMatchObject(shallow(<LoadingView />));
|
||||
});
|
||||
it('displays LoadedView with courses if request is loaded', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
...defaultValues,
|
||||
courses,
|
||||
isLoaded: true,
|
||||
});
|
||||
expect({ ...shallow(<RecommendationsPanel />).shallowWrapper, children: expect.any(Array) }).toMatchObject(
|
||||
shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />),
|
||||
);
|
||||
});
|
||||
it('displays LookingForChallengeWidget if request is failed', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
...defaultValues,
|
||||
isFailed: true,
|
||||
});
|
||||
expect({ ...shallow(<RecommendationsPanel />).shallowWrapper, children: expect.any(Array) }).toMatchObject(
|
||||
shallow(<LookingForChallengeWidget />),
|
||||
);
|
||||
});
|
||||
it('defaults to LookingForChallengeWidget if no flags are true', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
...defaultValues,
|
||||
});
|
||||
expect({ ...shallow(<RecommendationsPanel />).shallowWrapper, children: expect.any(Array) }).toMatchObject(
|
||||
shallow(<LookingForChallengeWidget />),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
recommendationsHeading: {
|
||||
id: 'RecommendationsPanel.recommendationsHeading',
|
||||
defaultMessage: 'Recommendations for you',
|
||||
description: 'Personalize courses heading on recommendations panel',
|
||||
},
|
||||
popularCoursesHeading: {
|
||||
id: 'RecommendationsPanel.popularCoursesHeading',
|
||||
defaultMessage: 'Popular courses',
|
||||
description: 'Popular courses heading on recommendations panel',
|
||||
},
|
||||
exploreCoursesButton: {
|
||||
id: 'RecommendationsPanel.exploreCoursesButton',
|
||||
defaultMessage: 'Explore courses',
|
||||
description: 'Button to explore more courses on recommendations panel',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,31 +0,0 @@
|
||||
export const recommendedCoursesData = {
|
||||
courses: [
|
||||
{
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
|
||||
title: 'Recommended course 1',
|
||||
marketingUrl: 'www.edx/recommended-course',
|
||||
courseKey: 'cs-1',
|
||||
},
|
||||
{
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
|
||||
title: 'Recommended course 2 with a really really really long name for some reason',
|
||||
marketingUrl: 'www.edx/recommended-course',
|
||||
courseKey: 'cs-2',
|
||||
},
|
||||
{
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
|
||||
title: 'Recommended course 3',
|
||||
marketingUrl: 'www.edx/recommended-course',
|
||||
courseKey: 'cs-3',
|
||||
},
|
||||
{
|
||||
logoImageUrl: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
|
||||
title: 'Recommended course 4',
|
||||
marketingUrl: 'www.edx/recommended-course',
|
||||
courseKey: 'cs-4',
|
||||
},
|
||||
],
|
||||
isControl: true,
|
||||
};
|
||||
|
||||
export default recommendedCoursesData;
|
||||
@@ -1,2 +0,0 @@
|
||||
// eslint-disable-next-line
|
||||
export default {"courses":[{"courseKey":"ETSx+TOEFLx","logoImageUrl":"https://prod-discovery.edx-cdn.org/organization/logos/9d9e1a30-c34d-4ad1-8c5a-d2410db8c123-8beea336c2a4.png","marketingUrl":"https://www.edx.org/course/toefl-test-preparation-the-insiders-guide?utm_source=lms_catalog_service_user&utm_medium=affiliate_partner","title":"TOEFL® Test Preparation: The Insider’s Guide"},{"courseKey":"HarvardX+CS50P","logoImageUrl":"https://prod-discovery.edx-cdn.org/organization/logos/44022f13-20df-4666-9111-cede3e5dc5b6-2cc39992c67a.png","marketingUrl":"https://www.edx.org/course/cs50s-introduction-to-programming-with-python?utm_source=lms_catalog_service_user&utm_medium=affiliate_partner","title":"CS50's Introduction to Programming with Python"},{"courseKey":"UQx+IELTSx","logoImageUrl":"https://prod-discovery.edx-cdn.org/organization/logos/8554749f-b920-4d7f-8986-af6bb95290aa-f336c6a2ca11.png","marketingUrl":"https://www.edx.org/course/ielts-academic-test-preparation?utm_source=lms_catalog_service_user&utm_medium=affiliate_partner","title":"IELTS Academic Test Preparation"},{"courseKey":"QUx+QU01X02","logoImageUrl":"https://prod-discovery.edx-cdn.org/organization/logos/7b6ca3d5-1030-408f-b1a4-fb140db1854e-7337e1e6deaf.png","marketingUrl":"https://www.edx.org/course/arabic-for-non-arabic-speakers?utm_source=lms_catalog_service_user&utm_medium=affiliate_partner","title":"Arabic for non-Arabic Speakers"},{"courseKey":"HarvardX+CS50W","logoImageUrl":"https://prod-discovery.edx-cdn.org/organization/logos/44022f13-20df-4666-9111-cede3e5dc5b6-2cc39992c67a.png","marketingUrl":"https://www.edx.org/course/cs50s-web-programming-with-python-and-javascript?utm_source=lms_catalog_service_user&utm_medium=affiliate_partner","title":"CS50's Web Programming with Python and JavaScript"}],"isControl":true};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { createLinkTracker, createEventTracker } from 'data/services/segment/utils';
|
||||
import track from 'tracking';
|
||||
|
||||
export const eventNames = StrictDict({
|
||||
recommendedCourseClicked: 'edx.bi.user.recommended.course.click',
|
||||
});
|
||||
|
||||
export const linkNames = StrictDict({
|
||||
findCoursesWidget: 'learner_home_widget_explore',
|
||||
});
|
||||
|
||||
export const findCoursesWidgetClicked = (href) => track.findCourses.findCoursesClicked(href, {
|
||||
linkName: linkNames.findCoursesWidget,
|
||||
});
|
||||
|
||||
export const recommendedCourseClicked = (courseKey, isControl, href) => createLinkTracker(
|
||||
createEventTracker(eventNames.recommendedCourseClicked, {
|
||||
course_key: courseKey,
|
||||
is_control: isControl,
|
||||
page: 'dashboard',
|
||||
}),
|
||||
href,
|
||||
);
|
||||
|
||||
export default {
|
||||
findCoursesWidgetClicked,
|
||||
recommendedCourseClicked,
|
||||
};
|
||||
Reference in New Issue
Block a user