Bw/recommendations panel (#63)
Co-authored-by: Shafqat Farhan <shafqat.farhan@arbisoft.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 45 KiB |
@@ -92,7 +92,7 @@ exports[`CertificateBanner snapshot is restricted and verified 1`] = `
|
||||
|
||||
exports[`CertificateBanner snapshot not passing and audit 1`] = `
|
||||
<Banner>
|
||||
Grade required to pass the course: 0.8
|
||||
Grade required to pass the course: 0.8%
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
@@ -114,6 +114,6 @@ exports[`CertificateBanner snapshot not passing and not audit and not finished 1
|
||||
<Banner
|
||||
variant="warning"
|
||||
>
|
||||
Grade required for a certificate: 0.8
|
||||
Grade required for a certificate: 0.8%
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
@@ -39,7 +39,7 @@ export const messages = StrictDict({
|
||||
passingGrade: {
|
||||
id: 'learner-dash.courseCard.banners.passingGrade',
|
||||
description: 'Message to learners with minimum passing grade for the course',
|
||||
defaultMessage: 'Grade required to pass the course: {minPassingGrade}',
|
||||
defaultMessage: 'Grade required to pass the course: {minPassingGrade}\u200f%',
|
||||
},
|
||||
notEligibleForCert: {
|
||||
id: 'learner-dash.courseCard.banners.notEligibleForCert',
|
||||
@@ -64,7 +64,7 @@ export const messages = StrictDict({
|
||||
certMinGrade: {
|
||||
id: 'learner-dash.courseCard.banners.certMinGrade',
|
||||
description: 'Passing grade requirement message',
|
||||
defaultMessage: 'Grade required for a certificate: {minPassingGrade}',
|
||||
defaultMessage: 'Grade required for a certificate: {minPassingGrade}\u200f%',
|
||||
},
|
||||
downloadCertificate: {
|
||||
id: 'learner-dash.courseCard.banners.downloadCertificate',
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import { Close, Tune } from '@edx/paragon/icons';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import FilterForm from './components/FilterForm';
|
||||
import SortForm from './components/SortForm';
|
||||
import useCourseFilterControlsData from './hooks';
|
||||
@@ -28,6 +30,7 @@ export const CourseFilterControls = ({
|
||||
setFilters,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const hasCourses = appHooks.useHasCourses();
|
||||
const {
|
||||
isOpen,
|
||||
open,
|
||||
@@ -50,6 +53,7 @@ export const CourseFilterControls = ({
|
||||
variant="outline-primary"
|
||||
iconBefore={Tune}
|
||||
onClick={open}
|
||||
disabled={!hasCourses}
|
||||
>
|
||||
{formatMessage(messages.refine)}
|
||||
</Button>
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import CourseFilterControls from './CourseFilterControls';
|
||||
import useCourseFilterControlsData from './hooks';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: { useHasCourses: jest.fn() },
|
||||
}));
|
||||
|
||||
jest.mock('./hooks', () => jest.fn().mockName('useCourseFilterControlsData'));
|
||||
|
||||
jest.mock('./components/FilterForm', () => 'FilterForm');
|
||||
jest.mock('./components/SortForm', () => 'SortForm');
|
||||
|
||||
appHooks.useHasCourses.mockReturnValue(true);
|
||||
|
||||
describe('CourseFilterControls', () => {
|
||||
const props = {
|
||||
sortBy: 'test-sort-by',
|
||||
@@ -30,13 +39,23 @@ describe('CourseFilterControls', () => {
|
||||
handleSortChange: jest.fn().mockName('handleSortChange'),
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('is mobile', () => {
|
||||
describe('no courses', () => {
|
||||
test('snapshot', () => {
|
||||
appHooks.useHasCourses.mockReturnValueOnce(false);
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
|
||||
const wrapper = shallow(<CourseFilterControls {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mobile', () => {
|
||||
test('snapshot', () => {
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth - 1 });
|
||||
const wrapper = shallow(<CourseFilterControls {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is not mobile', () => {
|
||||
});
|
||||
describe('is not mobile', () => {
|
||||
test('snapshot', () => {
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
|
||||
const wrapper = shallow(<CourseFilterControls {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
@@ -1,10 +1,63 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseFilterControls snapshot is mobile 1`] = `
|
||||
exports[`CourseFilterControls is not mobile snapshot 1`] = `
|
||||
<div
|
||||
id="course-filter-controls"
|
||||
>
|
||||
<Button
|
||||
disabled={false}
|
||||
iconBefore={[MockFunction icons.Tune]}
|
||||
onClick={[MockFunction open]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Refine
|
||||
</Button>
|
||||
<Form>
|
||||
<ModalPopup
|
||||
isOpen={false}
|
||||
onClose={[MockFunction close]}
|
||||
placement="bottom-end"
|
||||
positionRef="test-target"
|
||||
>
|
||||
<div
|
||||
className="bg-white p-3 rounded shadow d-flex flex-row"
|
||||
id="course-filter-controls-card"
|
||||
>
|
||||
<div
|
||||
className="filter-form-col"
|
||||
>
|
||||
<FilterForm
|
||||
filters={
|
||||
Array [
|
||||
"test-filter",
|
||||
]
|
||||
}
|
||||
handleFilterChange={[MockFunction handleFilterChange]}
|
||||
/>
|
||||
</div>
|
||||
<hr
|
||||
className="h-100 bg-primary-200 mx-3 my-0"
|
||||
/>
|
||||
<div
|
||||
className="filter-form-col text-left m-1"
|
||||
>
|
||||
<SortForm
|
||||
handleSortChange={[MockFunction handleSortChange]}
|
||||
sortBy="test-sort-by"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPopup>
|
||||
</Form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CourseFilterControls mobile snapshot 1`] = `
|
||||
<div
|
||||
id="course-filter-controls"
|
||||
>
|
||||
<Button
|
||||
disabled={false}
|
||||
iconBefore={[MockFunction icons.Tune]}
|
||||
onClick={[MockFunction open]}
|
||||
variant="outline-primary"
|
||||
@@ -63,11 +116,12 @@ exports[`CourseFilterControls snapshot is mobile 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CourseFilterControls snapshot is not mobile 1`] = `
|
||||
exports[`CourseFilterControls no courses snapshot 1`] = `
|
||||
<div
|
||||
id="course-filter-controls"
|
||||
>
|
||||
<Button
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Tune]}
|
||||
onClick={[MockFunction open]}
|
||||
variant="outline-primary"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NoCoursesView snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center mb-4.5"
|
||||
id="no-courses-content-view"
|
||||
>
|
||||
<Image
|
||||
alt="No Courses view banner"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
<h1>
|
||||
Looking for a new challenge?
|
||||
</h1>
|
||||
<p>
|
||||
Explore our courses to add them to your dashboard.
|
||||
</p>
|
||||
<Button
|
||||
as="a"
|
||||
href="course-search-url"
|
||||
variant="brand"
|
||||
>
|
||||
Explore courses
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
40
src/containers/CourseList/NoCoursesView/index.jsx
Normal file
40
src/containers/CourseList/NoCoursesView/index.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Image } from '@edx/paragon';
|
||||
import { Search } from '@edx/paragon/icons';
|
||||
|
||||
import emptyCourseSVG from 'assets/empty-course.svg';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import messages from './messages';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const NoCoursesView = () => {
|
||||
const { courseSearchUrl } = appHooks.usePlatformSettingsData();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<div
|
||||
id="no-courses-content-view"
|
||||
className="d-flex align-items-center justify-content-center mb-4.5"
|
||||
>
|
||||
<Image src={emptyCourseSVG} alt={formatMessage(messages.bannerAlt)} />
|
||||
<h1>
|
||||
{formatMessage(messages.lookingForChallengePrompt)}
|
||||
</h1>
|
||||
<p>
|
||||
{formatMessage(messages.exploreCoursesPrompt)}
|
||||
</p>
|
||||
<Button
|
||||
variant="brand"
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
iconBefore={Search}
|
||||
>
|
||||
{formatMessage(messages.exploreCoursesButton)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoCoursesView;
|
||||
@@ -1,13 +1,15 @@
|
||||
@import "@edx/paragon/scss/core/core";
|
||||
|
||||
.empty-course-hero {
|
||||
display: flex;
|
||||
#no-courses-content-view {
|
||||
border: 2px solid $light-400;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: map-get($spacers, 5);
|
||||
padding-top: map-get($spacers, 5);
|
||||
height: 100%;
|
||||
|
||||
& > * {
|
||||
margin-top: map-get($spacers, 3);
|
||||
margin-bottom: map-get($spacers, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
src/containers/CourseList/NoCoursesView/index.test.jsx
Normal file
24
src/containers/CourseList/NoCoursesView/index.test.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import EmptyCourse from '.';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useRecommendedCoursesData: jest.fn(() => ({ courses: [], isPersonalizedRecommendation: false })),
|
||||
useRequestIsPending: jest.fn(),
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: 'course-search-url',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('containers/Dashboard/hooks', () => ({
|
||||
useIsDashboardCollapsed: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
describe('NoCoursesView', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<EmptyCourse />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -2,20 +2,25 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
lookingForChallengePrompt: {
|
||||
id: 'EmptyCourse.lookingForChallengePrompt',
|
||||
id: 'Dashboard.NoCoursesView.lookingForChallengePrompt',
|
||||
defaultMessage: 'Looking for a new challenge?',
|
||||
description: 'Prompt user for new challenge',
|
||||
},
|
||||
exploreCoursesPrompt: {
|
||||
id: 'EmptyCourse.exploreCoursesPrompt',
|
||||
id: 'Dashboard.NoCoursesView.exploreCoursesPrompt',
|
||||
defaultMessage: 'Explore our courses to add them to your dashboard.',
|
||||
description: 'Prompt user to explore more courses',
|
||||
},
|
||||
exploreCoursesButton: {
|
||||
id: 'EmptyCourse.exploreCoursesButton',
|
||||
id: 'Dashboard.NoCoursesView.exploreCoursesButton',
|
||||
defaultMessage: 'Explore courses',
|
||||
description: 'Button to explore more courses',
|
||||
},
|
||||
bannerAlt: {
|
||||
id: 'Dashboard.NoCoursesView.bannerAlt',
|
||||
defaultMessage: 'No Courses view banner',
|
||||
description: 'No Courses view basnner',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseList snapshots collapsed with multiple courses and pages 1`] = `
|
||||
exports[`CourseList collapsed with multiple courses and pages snapshot 1`] = `
|
||||
<div
|
||||
className="course-list-container"
|
||||
>
|
||||
@@ -39,7 +39,7 @@ exports[`CourseList snapshots collapsed with multiple courses and pages 1`] = `
|
||||
key="baz"
|
||||
/>
|
||||
<Pagination
|
||||
className="mx-auto"
|
||||
className="mx-auto mb-2"
|
||||
onPageSelect={[MockFunction setPageNumber]}
|
||||
pageCount={3}
|
||||
paginationLabel="Course List"
|
||||
@@ -49,7 +49,55 @@ exports[`CourseList snapshots collapsed with multiple courses and pages 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CourseList snapshots with filters 1`] = `
|
||||
exports[`CourseList no courses snapshot 1`] = `
|
||||
<div
|
||||
className="course-list-container"
|
||||
>
|
||||
<div
|
||||
className="course-list-heading-container"
|
||||
>
|
||||
<h2
|
||||
className="course-list-title"
|
||||
>
|
||||
My Courses
|
||||
</h2>
|
||||
<div
|
||||
className="course-filter-controls-container"
|
||||
>
|
||||
<CourseFilterControls />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column flex-grow-1"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CourseList no filters snapshot 1`] = `
|
||||
<div
|
||||
className="course-list-container"
|
||||
>
|
||||
<div
|
||||
className="course-list-heading-container"
|
||||
>
|
||||
<h2
|
||||
className="course-list-title"
|
||||
>
|
||||
My Courses
|
||||
</h2>
|
||||
<div
|
||||
className="course-filter-controls-container"
|
||||
>
|
||||
<CourseFilterControls />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column flex-grow-1"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CourseList with filters snapshot 1`] = `
|
||||
<div
|
||||
className="course-list-container"
|
||||
>
|
||||
@@ -82,7 +130,7 @@ exports[`CourseList snapshots with filters 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CourseList snapshots with multiple courses and pages 1`] = `
|
||||
exports[`CourseList with multiple courses and pages snapshot 1`] = `
|
||||
<div
|
||||
className="course-list-container"
|
||||
>
|
||||
@@ -116,7 +164,7 @@ exports[`CourseList snapshots with multiple courses and pages 1`] = `
|
||||
key="baz"
|
||||
/>
|
||||
<Pagination
|
||||
className="mx-auto"
|
||||
className="mx-auto mb-2"
|
||||
onPageSelect={[MockFunction setPageNumber]}
|
||||
pageCount={3}
|
||||
paginationLabel="Course List"
|
||||
@@ -125,27 +173,3 @@ exports[`CourseList snapshots with multiple courses and pages 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CourseList snapshots with no filters 1`] = `
|
||||
<div
|
||||
className="course-list-container"
|
||||
>
|
||||
<div
|
||||
className="course-list-heading-container"
|
||||
>
|
||||
<h2
|
||||
className="course-list-title"
|
||||
>
|
||||
My Courses
|
||||
</h2>
|
||||
<div
|
||||
className="course-filter-controls-container"
|
||||
>
|
||||
<CourseFilterControls />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column flex-grow-1"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3,11 +3,13 @@ import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Pagination } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import {
|
||||
ActiveCourseFilters,
|
||||
CourseFilterControls,
|
||||
} from 'containers/CourseFilterControls';
|
||||
import CourseCard from 'containers/CourseCard';
|
||||
import NoCoursesView from './NoCoursesView';
|
||||
|
||||
import { useCourseListData, useIsCollapsed } from './hooks';
|
||||
|
||||
@@ -17,6 +19,7 @@ import './index.scss';
|
||||
|
||||
export const CourseList = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const hasCourses = appHooks.useHasCourses();
|
||||
const {
|
||||
filterOptions,
|
||||
setPageNumber,
|
||||
@@ -33,25 +36,32 @@ export const CourseList = () => {
|
||||
<CourseFilterControls {...filterOptions} />
|
||||
</div>
|
||||
</div>
|
||||
{showFilters && (
|
||||
<div id="course-list-active-filters-container">
|
||||
<ActiveCourseFilters {...filterOptions} />
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex flex-column flex-grow-1">
|
||||
{visibleList.map(({ cardId }) => (
|
||||
<CourseCard key={cardId} cardId={cardId} />
|
||||
))}
|
||||
{numPages > 1 && (
|
||||
<Pagination
|
||||
variant={isCollapsed ? 'reduced' : 'secondary'}
|
||||
paginationLabel="Course List"
|
||||
className="mx-auto"
|
||||
pageCount={numPages}
|
||||
onPageSelect={setPageNumber}
|
||||
/>
|
||||
{hasCourses
|
||||
? (
|
||||
<>
|
||||
{showFilters && (
|
||||
<div id="course-list-active-filters-container">
|
||||
<ActiveCourseFilters {...filterOptions} />
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex flex-column flex-grow-1">
|
||||
{visibleList.map(({ cardId }) => (
|
||||
<CourseCard key={cardId} cardId={cardId} />
|
||||
))}
|
||||
{numPages > 1 && (
|
||||
<Pagination
|
||||
variant={isCollapsed ? 'reduced' : 'secondary'}
|
||||
paginationLabel="Course List"
|
||||
className="mx-auto mb-2"
|
||||
pageCount={numPages}
|
||||
onPageSelect={setPageNumber}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<NoCoursesView />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import CourseList from '.';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { useCourseListData, useIsCollapsed } from './hooks';
|
||||
import CourseList from '.';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: { useHasCourses: jest.fn() },
|
||||
}));
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useCourseListData: jest.fn(),
|
||||
@@ -14,6 +19,8 @@ jest.mock('containers/CourseFilterControls', () => ({
|
||||
CourseFilterControls: 'CourseFilterControls',
|
||||
}));
|
||||
|
||||
appHooks.useHasCourses.mockReturnValue(true);
|
||||
|
||||
describe('CourseList', () => {
|
||||
const defaultCourseListData = {
|
||||
filterOptions: {},
|
||||
@@ -31,28 +38,39 @@ describe('CourseList', () => {
|
||||
return shallow(<CourseList />);
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
test('with no filters', () => {
|
||||
describe('no courses', () => {
|
||||
test('snapshot', () => {
|
||||
appHooks.useHasCourses.mockReturnValue(true);
|
||||
const wrapper = createWrapper();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('with filters', () => {
|
||||
});
|
||||
describe('no filters', () => {
|
||||
test('snapshot', () => {
|
||||
const wrapper = createWrapper();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('with filters', () => {
|
||||
test('snapshot', () => {
|
||||
const wrapper = createWrapper({
|
||||
filterOptions: {
|
||||
abitary: 'filter',
|
||||
},
|
||||
filterOptions: { abitary: 'filter' },
|
||||
showFilters: true,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('with multiple courses and pages', () => {
|
||||
});
|
||||
describe('with multiple courses and pages', () => {
|
||||
test('snapshot', () => {
|
||||
const wrapper = createWrapper({
|
||||
visibleList: [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }],
|
||||
numPages: 3,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('collapsed with multiple courses and pages', () => {
|
||||
});
|
||||
describe('collapsed with multiple courses and pages', () => {
|
||||
test('snapshot', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(true);
|
||||
const wrapper = createWrapper({
|
||||
visibleList: [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Container, Col, Row } from '@edx/paragon';
|
||||
|
||||
import CourseList from 'containers/CourseList';
|
||||
import WidgetSidebar from 'containers/WidgetSidebar';
|
||||
import hooks from './hooks';
|
||||
|
||||
export const columnConfig = {
|
||||
@@ -20,22 +20,26 @@ export const columnConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export const LoadedView = () => {
|
||||
export const DashboardLayout = ({ children, sidebar }) => {
|
||||
const isCollapsed = hooks.useIsDashboardCollapsed();
|
||||
|
||||
return (
|
||||
<Container fluid size="xl">
|
||||
<Row>
|
||||
<Col {...columnConfig.courseList} className="course-list-column">
|
||||
<CourseList />
|
||||
{children}
|
||||
</Col>
|
||||
<Col {...columnConfig.sidebar} className="sidebar-column">
|
||||
{!isCollapsed && (<h2 className="course-list-title"> </h2>)}
|
||||
<WidgetSidebar />
|
||||
{sidebar}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
DashboardLayout.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
sidebar: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default LoadedView;
|
||||
export default DashboardLayout;
|
||||
@@ -1,20 +1,22 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { Col, Row } from '@edx/paragon';
|
||||
|
||||
import CourseList from 'containers/CourseList';
|
||||
import WidgetSidebar from 'containers/WidgetSidebar';
|
||||
|
||||
import hooks from './hooks';
|
||||
import LoadedView, { columnConfig } from './LoadedView';
|
||||
import DashboardLayout, { columnConfig } from './DashboardLayout';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useIsDashboardCollapsed: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
describe('LoadedView', () => {
|
||||
describe('DashboardLayout', () => {
|
||||
const children = 'test-children';
|
||||
const props = {
|
||||
sidebar: 'test-sidebar-content',
|
||||
};
|
||||
const render = () => shallow(<DashboardLayout sidebar={props.sidebar}>{children}</DashboardLayout>);
|
||||
const testColumns = () => {
|
||||
it('loads courseList and sidebar column layout', () => {
|
||||
const columns = shallow(<LoadedView />).find(Row).find(Col);
|
||||
const columns = render().find(Row).find(Col);
|
||||
Object.keys(columnConfig.courseList).forEach(size => {
|
||||
expect(columns.at(0).props()[size]).toEqual(columnConfig.courseList[size]);
|
||||
});
|
||||
@@ -22,25 +24,25 @@ describe('LoadedView', () => {
|
||||
expect(columns.at(1).props()[size]).toEqual(columnConfig.sidebar[size]);
|
||||
});
|
||||
});
|
||||
it('displays CourseList in first column', () => {
|
||||
const columns = shallow(<LoadedView />).find(Row).find(Col);
|
||||
expect(columns.at(0).find(CourseList).length).toEqual(1);
|
||||
it('displays children in first column', () => {
|
||||
const columns = render().find(Row).find(Col);
|
||||
expect(columns.at(0).contains(children)).toEqual(true);
|
||||
});
|
||||
it('displays WidgetSidebar in second column', () => {
|
||||
const columns = shallow(<LoadedView />).find(Row).find(Col);
|
||||
expect(columns.at(1).find(WidgetSidebar).length).toEqual(1);
|
||||
it('displays sidebar prop in second column', () => {
|
||||
const columns = render().find(Row).find(Col);
|
||||
expect(columns.at(1).contains(props.sidebar)).toEqual(true);
|
||||
});
|
||||
};
|
||||
const testSnapshot = () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<LoadedView />)).toMatchSnapshot();
|
||||
expect(render()).toMatchSnapshot();
|
||||
});
|
||||
};
|
||||
describe('collapsed', () => {
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
it('does not show spacer component above widget sidebar', () => {
|
||||
const columns = shallow(<LoadedView />).find(Col);
|
||||
const columns = render().find(Col);
|
||||
expect(columns.at(1).find('h2').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -50,7 +52,7 @@ describe('LoadedView', () => {
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
it('shows a blank (nbsp) h2 spacer component above widget sidebar', () => {
|
||||
const columns = shallow(<LoadedView />).find(Col);
|
||||
const columns = render().find(Col);
|
||||
// nonbreaking space equivalent
|
||||
expect(columns.at(1).find('h2').text()).toEqual('\xA0');
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LoadedView collapsed snapshot 1`] = `
|
||||
exports[`DashboardLayout collapsed snapshot 1`] = `
|
||||
<Container
|
||||
fluid={true}
|
||||
size="xl"
|
||||
@@ -33,7 +33,7 @@ exports[`LoadedView collapsed snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<CourseList />
|
||||
test-children
|
||||
</Col>
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
@@ -62,13 +62,13 @@ exports[`LoadedView collapsed snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<WidgetSidebar />
|
||||
test-sidebar-content
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`LoadedView not collapsed snapshot 1`] = `
|
||||
exports[`DashboardLayout not collapsed snapshot 1`] = `
|
||||
<Container
|
||||
fluid={true}
|
||||
size="xl"
|
||||
@@ -101,7 +101,7 @@ exports[`LoadedView not collapsed snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<CourseList />
|
||||
test-children
|
||||
</Col>
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
@@ -135,7 +135,7 @@ exports[`LoadedView not collapsed snapshot 1`] = `
|
||||
>
|
||||
|
||||
</h2>
|
||||
<WidgetSidebar />
|
||||
test-sidebar-content
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`Dashboard snapshots courses loaded, show select session modal, no available dashboards snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex flex-column p-2 pt-3"
|
||||
className="d-flex flex-column p-2 pt-0"
|
||||
id="dashboard-container"
|
||||
>
|
||||
<h1
|
||||
@@ -14,14 +14,18 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
|
||||
<div
|
||||
id="dashboard-content"
|
||||
>
|
||||
<LoadedView />
|
||||
<DashboardLayout
|
||||
sidebar={<LoadedWidgetSidebar />}
|
||||
>
|
||||
<CourseList />
|
||||
</DashboardLayout>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Dashboard snapshots courses still loading snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex flex-column p-2 pt-3"
|
||||
className="d-flex flex-column p-2 pt-0"
|
||||
id="dashboard-container"
|
||||
>
|
||||
<h1
|
||||
@@ -39,7 +43,7 @@ exports[`Dashboard snapshots courses still loading snapshot 1`] = `
|
||||
|
||||
exports[`Dashboard snapshots there are no courses, there ARE available dashboards snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex flex-column p-2 pt-3"
|
||||
className="d-flex flex-column p-2 pt-0"
|
||||
id="dashboard-container"
|
||||
>
|
||||
<h1
|
||||
@@ -51,7 +55,11 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
|
||||
<div
|
||||
id="dashboard-content"
|
||||
>
|
||||
<EmptyCourse />
|
||||
<DashboardLayout
|
||||
sidebar={<NoCoursesWidgetSidebar />}
|
||||
>
|
||||
<CourseList />
|
||||
</DashboardLayout>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2,14 +2,16 @@ import React from 'react';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import EmptyCourse from 'containers/EmptyCourse';
|
||||
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
|
||||
import SelectSessionModal from 'containers/SelectSessionModal';
|
||||
import CourseList from 'containers/CourseList';
|
||||
|
||||
import LoadedSidebar from 'containers/WidgetContainers/LoadedSidebar';
|
||||
import NoCoursesSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
|
||||
|
||||
import LoadingView from './LoadingView';
|
||||
import LoadedView from './LoadedView';
|
||||
import DashboardLayout from './DashboardLayout';
|
||||
import hooks from './hooks';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const Dashboard = () => {
|
||||
@@ -20,7 +22,7 @@ export const Dashboard = () => {
|
||||
const initIsPending = appHooks.useRequestIsPending(RequestKeys.initialize);
|
||||
const showSelectSessionModal = appHooks.useShowSelectSessionModal();
|
||||
return (
|
||||
<div id="dashboard-container" className="d-flex flex-column p-2 pt-3">
|
||||
<div id="dashboard-container" className="d-flex flex-column p-2 pt-0">
|
||||
<h1 className="sr-only">{pageTitle}</h1>
|
||||
{!initIsPending && (
|
||||
<>
|
||||
@@ -29,9 +31,13 @@ export const Dashboard = () => {
|
||||
</>
|
||||
)}
|
||||
<div id="dashboard-content">
|
||||
{initIsPending && (<LoadingView />)}
|
||||
{(!initIsPending && hasCourses) && (<LoadedView />)}
|
||||
{(!initIsPending && !hasCourses) && (<EmptyCourse />)}
|
||||
{initIsPending
|
||||
? (<LoadingView />)
|
||||
: (
|
||||
<DashboardLayout sidebar={hasCourses ? <LoadedSidebar /> : <NoCoursesSidebar />}>
|
||||
<CourseList />
|
||||
</DashboardLayout>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,11 +2,14 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import EmptyCourse from 'containers/EmptyCourse';
|
||||
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
|
||||
import SelectSessionModal from 'containers/SelectSessionModal';
|
||||
import CourseList from 'containers/CourseList';
|
||||
|
||||
import LoadedView from './LoadedView';
|
||||
import LoadedWidgetSidebar from 'containers/WidgetContainers/LoadedSidebar';
|
||||
import NoCoursesWidgetSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
|
||||
|
||||
import DashboardLayout from './DashboardLayout';
|
||||
import LoadingView from './LoadingView';
|
||||
import hooks from './hooks';
|
||||
import Dashboard from '.';
|
||||
@@ -25,10 +28,12 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('containers/EmptyCourse', () => 'EmptyCourse');
|
||||
jest.mock('containers/EnterpriseDashboardModal', () => 'EnterpriseDashboardModal');
|
||||
jest.mock('containers/CourseList', () => 'CourseList');
|
||||
jest.mock('containers/WidgetContainers/LoadedSidebar', () => 'LoadedWidgetSidebar');
|
||||
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => 'NoCoursesWidgetSidebar');
|
||||
jest.mock('./LoadingView', () => 'LoadingView');
|
||||
jest.mock('./LoadedView', () => 'LoadedView');
|
||||
jest.mock('./DashboardLayout', () => 'DashboardLayout');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useInitializeDashboard: jest.fn(),
|
||||
@@ -115,7 +120,9 @@ describe('Dashboard', () => {
|
||||
initIsPending: false,
|
||||
showSelectSessionModal: true,
|
||||
},
|
||||
content: ['LoadedView', <LoadedView />],
|
||||
content: ['LoadedView', (
|
||||
<DashboardLayout sidebar={<LoadedWidgetSidebar />}><CourseList /></DashboardLayout>
|
||||
)],
|
||||
showEnterpriseModal: false,
|
||||
showSelectSessionModal: true,
|
||||
});
|
||||
@@ -129,7 +136,9 @@ describe('Dashboard', () => {
|
||||
initIsPending: false,
|
||||
showSelectSessionModal: false,
|
||||
},
|
||||
content: ['EmptyCourse', <EmptyCourse />],
|
||||
content: ['Dashboard layout with no courses sidebar and content', (
|
||||
<DashboardLayout sidebar={<NoCoursesWidgetSidebar />}><CourseList /></DashboardLayout>
|
||||
)],
|
||||
showEnterpriseModal: true,
|
||||
showSelectSessionModal: false,
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
myCourses: {
|
||||
id: 'dashboard.mycourses',
|
||||
defaultMessage: 'My Courses',
|
||||
description: 'Course list heading',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,94 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SuggestedCourses snapshot has 3 suggested courses 1`] = `
|
||||
<Container
|
||||
size="md"
|
||||
>
|
||||
<h3
|
||||
className="text-center"
|
||||
>
|
||||
Some courses you may be interested in
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<Card
|
||||
className="m-3"
|
||||
key="Suggested course 1"
|
||||
>
|
||||
<Card.ImageCap
|
||||
src="https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg"
|
||||
srcAlt="Course image banner"
|
||||
/>
|
||||
<Card.Header
|
||||
className="mb-2"
|
||||
title="Suggested course 1"
|
||||
/>
|
||||
<Card.Body
|
||||
className="h-100"
|
||||
/>
|
||||
<Card.Footer>
|
||||
<ActionRow>
|
||||
<Button
|
||||
as="a"
|
||||
>
|
||||
View Course
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
<Card
|
||||
className="m-3"
|
||||
key="Suggested course 2"
|
||||
>
|
||||
<Card.ImageCap
|
||||
src="https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg"
|
||||
srcAlt="Course image banner"
|
||||
/>
|
||||
<Card.Header
|
||||
className="mb-2"
|
||||
title="Suggested course 2"
|
||||
/>
|
||||
<Card.Body
|
||||
className="h-100"
|
||||
/>
|
||||
<Card.Footer>
|
||||
<ActionRow>
|
||||
<Button
|
||||
as="a"
|
||||
>
|
||||
View Course
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
<Card
|
||||
className="m-3"
|
||||
key="Suggested course 3"
|
||||
>
|
||||
<Card.ImageCap
|
||||
src="https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg"
|
||||
srcAlt="Course image banner"
|
||||
/>
|
||||
<Card.Header
|
||||
className="mb-2"
|
||||
title="Suggested course 3"
|
||||
/>
|
||||
<Card.Body
|
||||
className="h-100"
|
||||
/>
|
||||
<Card.Footer>
|
||||
<ActionRow>
|
||||
<Button
|
||||
as="a"
|
||||
>
|
||||
View Course
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
</div>
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`SuggestedCourses snapshot no suggested courses 1`] = `""`;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Card,
|
||||
Button,
|
||||
Container,
|
||||
} from '@edx/paragon';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const SuggestedCourses = () => {
|
||||
const suggestedCourses = appHooks.useSuggestedCoursesData();
|
||||
const { formatMessage } = useIntl();
|
||||
if (!suggestedCourses.length) { return null; }
|
||||
return (
|
||||
<Container size="md">
|
||||
<h3 className="text-center">{formatMessage(messages.header)}</h3>
|
||||
<div className="d-flex">
|
||||
{suggestedCourses.map((course) => (
|
||||
<Card key={course.courseName} className="m-3">
|
||||
<Card.ImageCap
|
||||
src={course.bannerImgSrc}
|
||||
srcAlt={formatMessage(messages.courseImageAlt)}
|
||||
/>
|
||||
<Card.Header title={course.courseName} className="mb-2" />
|
||||
<Card.Body className="h-100" />
|
||||
<Card.Footer>
|
||||
<ActionRow>
|
||||
<Button as="a">{formatMessage(messages.viewCourseButton)}</Button>
|
||||
</ActionRow>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestedCourses;
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import SuggestedCourses from '.';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useSuggestedCoursesData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const suggestedCourses = [
|
||||
{
|
||||
bannerImgSrc: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
|
||||
courseName: 'Suggested course 1',
|
||||
courseUrl: 'www.edx/suggested-course',
|
||||
},
|
||||
{
|
||||
bannerImgSrc: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
|
||||
courseName: 'Suggested course 2',
|
||||
courseUrl: 'www.edx/suggested-course',
|
||||
},
|
||||
{
|
||||
bannerImgSrc: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
|
||||
courseName: 'Suggested course 3',
|
||||
courseUrl: 'www.edx/suggested-course',
|
||||
},
|
||||
];
|
||||
|
||||
describe('SuggestedCourses snapshot', () => {
|
||||
test('no suggested courses', () => {
|
||||
appHooks.useSuggestedCoursesData.mockReturnValueOnce([]);
|
||||
expect(shallow(<SuggestedCourses />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('has 3 suggested courses', () => {
|
||||
appHooks.useSuggestedCoursesData.mockReturnValueOnce(suggestedCourses);
|
||||
expect(shallow(<SuggestedCourses />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
header: {
|
||||
id: 'leanerDashboard.emptyCourse.suggestedCourses.header',
|
||||
defaultMessage: 'Some courses you may be interested in',
|
||||
description: 'Header for suggesting courses',
|
||||
},
|
||||
viewCourseButton: {
|
||||
id: 'leanerDashboard.emptyCourse.suggestedCourses.viewCourseButton',
|
||||
defaultMessage: 'View Course',
|
||||
description: 'Actions button to visit the suggested course',
|
||||
},
|
||||
courseImageAlt: {
|
||||
id: 'leanerDashboard.emptyCourse.suggestedCourses.courseImageAlt',
|
||||
defaultMessage: 'Course image banner',
|
||||
description: 'alt for course image',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,40 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmptyCourse snapshot 1`] = `
|
||||
<div
|
||||
className="p-3"
|
||||
>
|
||||
<div
|
||||
className="empty-course-hero"
|
||||
>
|
||||
<Image
|
||||
alt="empty course banner"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
defaultMessage="Looking for a new challenge?"
|
||||
description="Prompt user for new challenge"
|
||||
id="EmptyCourse.lookingForChallengePrompt"
|
||||
/>
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Explore our courses to add them to your dashboard."
|
||||
description="Prompt user to explore more courses"
|
||||
id="EmptyCourse.exploreCoursesPrompt"
|
||||
/>
|
||||
</p>
|
||||
<Button
|
||||
variant="brand"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Explore courses"
|
||||
description="Button to explore more courses"
|
||||
id="EmptyCourse.exploreCoursesButton"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<SuggestedCourses />
|
||||
</div>
|
||||
`;
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button,
|
||||
Image,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import emptyCourseSVG from 'assets/empty-course.svg';
|
||||
|
||||
import SuggestedCourses from './SuggestedCourses';
|
||||
import messages from './messages';
|
||||
import './index.scss';
|
||||
|
||||
export const EmptyCourse = () => (
|
||||
<div className="p-3">
|
||||
<div className="empty-course-hero">
|
||||
<Image src={emptyCourseSVG} alt="empty course banner" />
|
||||
<h1>
|
||||
<FormattedMessage {...messages.lookingForChallengePrompt} />
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage {...messages.exploreCoursesPrompt} />
|
||||
</p>
|
||||
<Button variant="brand">
|
||||
<FormattedMessage {...messages.exploreCoursesButton} />
|
||||
</Button>
|
||||
</div>
|
||||
<SuggestedCourses />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default EmptyCourse;
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import EmptyCourse from '.';
|
||||
|
||||
jest.mock('./SuggestedCourses', () => 'SuggestedCourses');
|
||||
|
||||
describe('EmptyCourse', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<EmptyCourse />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ exports[`WidgetSidebar snapshots default 1`] = `
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<LookingForChallengeWidget />
|
||||
<RecommendationsPanel />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import LookingForChallengeWidget from './widgets/LookingForChallengeWidget';
|
||||
import RecommendationsPanel from 'widgets/RecommendationsPanel';
|
||||
|
||||
export const WidgetSidebar = () => (
|
||||
<div className="widget-sidebar">
|
||||
<div className="d-flex">
|
||||
<LookingForChallengeWidget />
|
||||
<RecommendationsPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import WidgetSidebar from '.';
|
||||
|
||||
jest.mock('./widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||
|
||||
describe('WidgetSidebar', () => {
|
||||
describe('snapshots', () => {
|
||||
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WidgetSidebar snapshots default 1`] = `
|
||||
<div
|
||||
className="widget-sidebar px-2"
|
||||
>
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<RecommendationsPanel />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
13
src/containers/WidgetContainers/NoCoursesSidebar/index.jsx
Normal file
13
src/containers/WidgetContainers/NoCoursesSidebar/index.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import RecommendationsPanel from 'widgets/RecommendationsPanel';
|
||||
|
||||
export const WidgetSidebar = () => (
|
||||
<div className="widget-sidebar px-2">
|
||||
<div className="d-flex">
|
||||
<RecommendationsPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default WidgetSidebar;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import WidgetSidebar from '.';
|
||||
|
||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||
|
||||
describe('WidgetSidebar', () => {
|
||||
describe('snapshots', () => {
|
||||
test('default', () => {
|
||||
const wrapper = shallow(<WidgetSidebar />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@ export const RequestKeys = StrictDict({
|
||||
enrollEntitlementSession: 'enrollEntitlementSession',
|
||||
leaveEntitlementSession: 'leaveEntitlementSession',
|
||||
masquerade: 'masquerade',
|
||||
recommendedCourses: 'recommendedCourses',
|
||||
});
|
||||
|
||||
export const ErrorCodes = StrictDict({
|
||||
|
||||
@@ -12,7 +12,7 @@ export const sortFn = (transform, { reverse }) => (v1, v2) => {
|
||||
|
||||
export const courseFilters = StrictDict({
|
||||
[FilterKeys.notEnrolled]: (course) => !course.enrollment.isEnrolled,
|
||||
[FilterKeys.done]: (course) => course.courseRun.isArchived,
|
||||
[FilterKeys.done]: (course) => course.courseRun !== null && course.courseRun.isArchived,
|
||||
[FilterKeys.upgraded]: (course) => course.enrollment.isVerified,
|
||||
[FilterKeys.inProgress]: (course) => course.enrollment.hasStarted,
|
||||
[FilterKeys.notStarted]: (course) => !course.enrollment.hasStarted,
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('courseList selector module', () => {
|
||||
});
|
||||
test('done returns true iff learner has finished course', () => {
|
||||
filterFn = courseFilters[FilterKeys.done];
|
||||
expect(filterFn({ courseRun: null })).toEqual(false);
|
||||
expect(filterFn({ courseRun: { isArchived: true } })).toEqual(true);
|
||||
expect(filterFn({ courseRun: { isArchived: false } })).toEqual(false);
|
||||
});
|
||||
|
||||
@@ -11,8 +11,6 @@ export const usePageNumber = () => useSelector(appSelectors.pageNumber);
|
||||
export const useEmailConfirmationData = () => useSelector(appSelectors.emailConfirmation);
|
||||
export const useEnterpriseDashboardData = () => useSelector(appSelectors.enterpriseDashboard);
|
||||
export const usePlatformSettingsData = () => useSelector(appSelectors.platformSettings);
|
||||
// suggested courses is max at 3 at the moment.
|
||||
export const useSuggestedCoursesData = () => useSelector(appSelectors.suggestedCourses).slice(0, 3);
|
||||
export const useSelectSessionModalData = () => useSelector(appSelectors.selectSessionModal);
|
||||
export const useSocialSettingsData = () => useSelector(appSelectors.socialSettingsData);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ jest.mock('data/redux', () => ({
|
||||
setPageNumber: jest.fn(v => ({ setPageNumber: v })),
|
||||
loadGlobalData: jest.fn(v => ({ loadGlobalData: v })),
|
||||
loadCourses: jest.fn(v => ({ loadCourses: v })),
|
||||
loadRecommendedCourses: jest.fn(v => ({ loadRecommendedCourses: v })),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
|
||||
@@ -38,6 +38,9 @@ jest.unmock('react-redux');
|
||||
jest.unmock('reselect');
|
||||
jest.unmock('hooks');
|
||||
|
||||
jest.mock('containers/WidgetContainers/LoadedSidebar', () => 'loaded-widget-sidebar');
|
||||
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => 'no-courses-widget-sidebar');
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: () => ({
|
||||
|
||||
51
src/widgets/RecommendationsPanel/LoadedView.jsx
Normal file
51
src/widgets/RecommendationsPanel/LoadedView.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Search } from '@edx/paragon/icons';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import CourseCard from './components/CourseCard';
|
||||
import messages from './messages';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const LoadedView = ({
|
||||
courses,
|
||||
}) => {
|
||||
const { courseSearchUrl } = hooks.usePlatformSettingsData();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="p-4 w-100 panel-background">
|
||||
<h3 className="pb-2">{formatMessage(messages.recommendationsHeading)}</h3>
|
||||
<div>
|
||||
{courses.map((course) => (
|
||||
<CourseCard key={course.courseKey} course={course} />
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center explore-courses-btn">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
iconBefore={Search}
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
>
|
||||
{formatMessage(messages.exploreCoursesButton)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LoadedView.propTypes = {
|
||||
courses: PropTypes.arrayOf(PropTypes.shape({
|
||||
courseKey: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
logoImageUrl: PropTypes.string,
|
||||
marketingUrl: PropTypes.string,
|
||||
})).isRequired,
|
||||
};
|
||||
|
||||
export default LoadedView;
|
||||
22
src/widgets/RecommendationsPanel/LoadedView.test.jsx
Normal file
22
src/widgets/RecommendationsPanel/LoadedView.test.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import LoadedView from './LoadedView';
|
||||
import mockData from './mockData';
|
||||
|
||||
jest.mock('./components/CourseCard', () => 'CourseCard');
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const courseSearchUrl = 'test-course-search-url';
|
||||
hooks.usePlatformSettingsData.mockReturnValue(courseSearchUrl);
|
||||
|
||||
describe('RecommendationsPanel LoadedView', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<LoadedView courses={mockData.courses} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
19
src/widgets/RecommendationsPanel/LoadingView.jsx
Normal file
19
src/widgets/RecommendationsPanel/LoadingView.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Spinner } from '@edx/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;
|
||||
19
src/widgets/RecommendationsPanel/LoadingView.test.jsx
Normal file
19
src/widgets/RecommendationsPanel/LoadingView.test.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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 />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RecommendationsPanel LoadedView snapshot 1`] = `
|
||||
<div
|
||||
className="p-4 w-100 panel-background"
|
||||
>
|
||||
<h3
|
||||
className="pb-2"
|
||||
>
|
||||
Recommendations for you
|
||||
</h3>
|
||||
<div>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-1",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 1",
|
||||
}
|
||||
}
|
||||
key="cs-1"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-2",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 2 with a really really really long name for some reason",
|
||||
}
|
||||
}
|
||||
key="cs-2"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-3",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 3",
|
||||
}
|
||||
}
|
||||
key="cs-3"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-4",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 4",
|
||||
}
|
||||
}
|
||||
key="cs-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-center explore-courses-btn"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
variant="tertiary"
|
||||
>
|
||||
Explore courses
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RecommendationsPanel LoadingView snapshot 1`] = `
|
||||
<div
|
||||
className="recommendations-loading w-100"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
variant="light"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
12
src/widgets/RecommendationsPanel/api.js
Normal file
12
src/widgets/RecommendationsPanel/api.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { get, stringifyUrl } from 'data/services/lms/utils';
|
||||
import urls from 'data/services/lms/urls';
|
||||
|
||||
export const fetchUrl = `${urls.api}/learner_home/recommendation/courses/`;
|
||||
export const apiKeys = StrictDict({ user: 'user' });
|
||||
|
||||
const fetchRecommendedCourses = () => get(stringifyUrl(fetchUrl));
|
||||
|
||||
export default {
|
||||
fetchRecommendedCourses,
|
||||
};
|
||||
17
src/widgets/RecommendationsPanel/api.test.js
Normal file
17
src/widgets/RecommendationsPanel/api.test.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { get, stringifyUrl } from 'data/services/lms/utils';
|
||||
import api, { fetchUrl } 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(fetchUrl)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
43
src/widgets/RecommendationsPanel/components/CourseCard.jsx
Normal file
43
src/widgets/RecommendationsPanel/components/CourseCard.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Card, Hyperlink, Truncate } from '@edx/paragon';
|
||||
|
||||
import { useIsCollapsed } from 'containers/CourseCard/hooks';
|
||||
import './index.scss';
|
||||
|
||||
export const CourseCard = ({ course }) => {
|
||||
const isCollapsed = useIsCollapsed();
|
||||
return (
|
||||
<Hyperlink destination={course?.marketingUrl} className="card-link">
|
||||
<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,
|
||||
};
|
||||
|
||||
export default CourseCard;
|
||||
33
src/widgets/RecommendationsPanel/components/index.scss
Normal file
33
src/widgets/RecommendationsPanel/components/index.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
@import "@edx/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;
|
||||
}
|
||||
margin-top: 0.313rem;
|
||||
}
|
||||
|
||||
.text-info-500 {
|
||||
margin: 0 !important;
|
||||
}
|
||||
47
src/widgets/RecommendationsPanel/hooks.js
Normal file
47
src/widgets/RecommendationsPanel/hooks.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { RequestStates } from 'data/constants/requests';
|
||||
|
||||
import * as module from './hooks';
|
||||
import api from './api';
|
||||
|
||||
export const state = StrictDict({
|
||||
requestState: (val) => React.useState(val), // eslint-disable-line
|
||||
data: (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; };
|
||||
});
|
||||
};
|
||||
|
||||
export const useRecommendationPanelData = () => {
|
||||
const [requestState, setRequestState] = module.state.requestState(RequestStates.pending);
|
||||
const [data, setData] = module.state.data({});
|
||||
module.useFetchCourses(setRequestState, setData);
|
||||
const courses = data.data?.courses || [];
|
||||
return {
|
||||
courses,
|
||||
isLoaded: requestState === RequestStates.completed && courses.length > 0,
|
||||
isFailed: requestState === RequestStates.failed
|
||||
|| (requestState === RequestStates.completed && courses.length === 0),
|
||||
isLoading: requestState === RequestStates.pending,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
useRecommendationPanelData,
|
||||
};
|
||||
178
src/widgets/RecommendationsPanel/hooks.test.js
Normal file
178
src/widgets/RecommendationsPanel/hooks.test.js
Normal file
@@ -0,0 +1,178 @@
|
||||
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(undefined);
|
||||
});
|
||||
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('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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
29
src/widgets/RecommendationsPanel/index.jsx
Normal file
29
src/widgets/RecommendationsPanel/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
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,
|
||||
isFailed,
|
||||
isLoaded,
|
||||
isLoading,
|
||||
} = hooks.useRecommendationPanelData();
|
||||
|
||||
if (isLoading) {
|
||||
return (<LoadingView />);
|
||||
}
|
||||
if (isLoaded) {
|
||||
return (<LoadedView courses={courses} />);
|
||||
}
|
||||
if (isFailed) {
|
||||
return (<LookingForChallengeWidget />);
|
||||
}
|
||||
// default fallback
|
||||
return (<LookingForChallengeWidget />);
|
||||
};
|
||||
|
||||
export default RecommendationsPanel;
|
||||
17
src/widgets/RecommendationsPanel/index.scss
Normal file
17
src/widgets/RecommendationsPanel/index.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
@import "@edx/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;
|
||||
}
|
||||
63
src/widgets/RecommendationsPanel/index.test.jsx
Normal file
63
src/widgets/RecommendationsPanel/index.test.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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', () => {
|
||||
it('displays LoadingView if request is loading', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
courses: [],
|
||||
isFailed: false,
|
||||
isLoaded: false,
|
||||
isLoading: true,
|
||||
});
|
||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(shallow(<LoadingView />));
|
||||
});
|
||||
it('displays LoadedView with courses if request is loaded', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
courses,
|
||||
isFailed: false,
|
||||
isLoaded: true,
|
||||
isLoading: false,
|
||||
});
|
||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(
|
||||
shallow(<LoadedView courses={courses} />),
|
||||
);
|
||||
});
|
||||
it('displays LookingForChallengeWidget if request is failed', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
courses: [],
|
||||
isFailed: true,
|
||||
isLoaded: false,
|
||||
isLoading: false,
|
||||
});
|
||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(
|
||||
shallow(<LookingForChallengeWidget />),
|
||||
);
|
||||
});
|
||||
it('defaults to LookingForChallengeWidget if no flags are true', () => {
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
courses: [],
|
||||
isFailed: false,
|
||||
isLoaded: false,
|
||||
isLoading: false,
|
||||
});
|
||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(
|
||||
shallow(<LookingForChallengeWidget />),
|
||||
);
|
||||
});
|
||||
});
|
||||
16
src/widgets/RecommendationsPanel/messages.js
Normal file
16
src/widgets/RecommendationsPanel/messages.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
recommendationsHeading: {
|
||||
id: 'RecommendationsPanel.recommendationsHeading',
|
||||
defaultMessage: 'Recommendations for you',
|
||||
description: 'Heading on recommendations panel',
|
||||
},
|
||||
exploreCoursesButton: {
|
||||
id: 'RecommendationsPanel.exploreCoursesButton',
|
||||
defaultMessage: 'Explore courses',
|
||||
description: 'Button to explore more courses on recommendations panel',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
31
src/widgets/RecommendationsPanel/mockData.js
Normal file
31
src/widgets/RecommendationsPanel/mockData.js
Normal file
@@ -0,0 +1,31 @@
|
||||
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',
|
||||
},
|
||||
],
|
||||
isPersonalizedRecommendation: true,
|
||||
};
|
||||
|
||||
export default recommendedCoursesData;
|
||||
Reference in New Issue
Block a user