feat: update related program location (#104)

This commit is contained in:
leangseu-edx
2023-02-13 14:12:27 -05:00
committed by GitHub
parent a13085a6a1
commit 0933d185af
51 changed files with 1507 additions and 32 deletions

View File

@@ -17,13 +17,13 @@ import {
actions,
} from 'data/redux';
import { reduxHooks } from 'hooks';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import Dashboard from 'containers/Dashboard';
import ZendeskFab from 'components/ZendeskFab';
import track from 'tracking';
import fakeData from 'data/services/lms/fakeData/courses';
import LearnerDashboardHeaderVariant from './containers/LearnerDashboardHeaderVariant';
import messages from './messages';
@@ -77,7 +77,7 @@ export const App = () => {
<title>{formatMessage(messages.pageTitle)}</title>
</Helmet>
<div>
<LearnerDashboardHeader />
<LearnerDashboardHeaderVariant />
<main>
{hasNetworkFailure
? (

View File

@@ -11,6 +11,12 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
@import "~@edx/frontend-component-footer/dist/_footer";
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.alert.alert-info .alert-icon {
color: black;
}

View File

@@ -12,14 +12,14 @@ import { Alert } from '@edx/paragon';
import { RequestKeys } from 'data/constants/requests';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import LearnerDashboardHeaderVariant from 'containers/LearnerDashboardHeaderVariant';
import { App } from './App';
import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => 'Footer');
jest.mock('containers/Dashboard', () => 'Dashboard');
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
jest.mock('containers/LearnerDashboardHeaderVariant', () => 'LearnerDashboardHeaderVariant');
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
@@ -54,7 +54,7 @@ describe('App router component', () => {
expect(el.find(Helmet).find('title').text()).toEqual(useIntl().formatMessage(messages.pageTitle));
});
it('displays learner dashboard header', () => {
expect(el.find(LearnerDashboardHeader).length).toEqual(1);
expect(el.find(LearnerDashboardHeaderVariant).length).toEqual(1);
});
it('wraps the page in a browser router', () => {
expect(el.find(Router)).toMatchObject(el);

View File

@@ -11,7 +11,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
</title>
</HelmetWrapper>
<div>
<LearnerDashboardHeader />
<LearnerDashboardHeaderVariant />
<main>
<Alert
variant="danger"
@@ -40,7 +40,7 @@ exports[`App router component component no network failure snapshot 1`] = `
</title>
</HelmetWrapper>
<div>
<LearnerDashboardHeader />
<LearnerDashboardHeaderVariant />
<main>
<Dashboard />
</main>
@@ -63,7 +63,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
</title>
</HelmetWrapper>
<div>
<LearnerDashboardHeader />
<LearnerDashboardHeaderVariant />
<main>
<Alert
variant="danger"

View File

@@ -4,19 +4,23 @@ import PropTypes from 'prop-types';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
export const Banner = ({ children, variant, icon }) => (
<Alert variant={variant} className="mb-0" icon={icon}>
export const Banner = ({
children, variant, icon, className,
}) => (
<Alert variant={variant} className={className} icon={icon}>
{children}
</Alert>
);
Banner.defaultProps = {
icon: Info,
variant: 'info',
className: 'mb-0',
};
Banner.propTypes = {
variant: PropTypes.string,
icon: PropTypes.func,
children: PropTypes.node.isRequired,
className: PropTypes.string,
};
export default Banner;

View File

@@ -19,5 +19,9 @@ describe('Banner', () => {
expect(wrapper.find(Alert).prop('variant')).toEqual('success');
});
test('renders with custom class', () => {
const wrapper = shallow(<Banner {...props} className="custom-class" />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,16 @@
import { shallow } from 'enzyme';
import EmailLink from './EmailLink';
describe('EmailLink', () => {
it('renders null when no address is provided', () => {
const wrapper = shallow(<EmailLink />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toEqual(true);
});
it('renders a MailtoLink when an address is provided', () => {
const wrapper = shallow(<EmailLink address="test@email.com" />);
expect(wrapper.find('MailtoLink').length).toEqual(1);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -10,6 +10,16 @@ exports[`Banner snapshot renders default banner 1`] = `
</Alert>
`;
exports[`Banner snapshot renders with custom class 1`] = `
<Alert
className="custom-class"
icon={[MockFunction icons.Info]}
variant="info"
>
Hello, world!
</Alert>
`;
exports[`Banner snapshot renders with variants 1`] = `
<Alert
className="mb-0"

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmailLink renders a MailtoLink when an address is provided 1`] = `
<MailtoLink
to="test@email.com"
>
test@email.com
</MailtoLink>
`;
exports[`EmailLink renders null when no address is provided 1`] = `""`;

View File

@@ -51,10 +51,25 @@
.course-card-banners {
> .alert {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-radius: 0;
box-shadow: none;
padding: map-get($spacers, 3) map-get($spacers, 4);
&:last-of-type {
border-bottom-left-radius: $alert-border-radius;
border-bottom-right-radius: $alert-border-radius;
}
}
.related-programs-banner {
.related-programs-list-container {
list-style: none;
display: inline;
> li {
line-height: 1rem;
}
}
}
}
}

View File

@@ -40,9 +40,6 @@ exports[`CourseCard component snapshot: collapsed 1`] = `
<Card.Footer
orientation="vertical"
>
<RelatedProgramsBadge
cardId="test-card-id"
/>
<CourseCardActions
cardId="test-card-id"
/>
@@ -99,9 +96,6 @@ exports[`CourseCard component snapshot: not collapsed 1`] = `
<Card.Footer
orientation="horizontal"
>
<RelatedProgramsBadge
cardId="test-card-id"
/>
<CourseCardActions
cardId="test-card-id"
/>

View File

@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
export const ProgramsList = ({ programs }) => (
<ul className="related-programs-list-container">
{programs.map((program) => (
<li key={program.programUrl} className="my-2">
<a href={program.programUrl}>{program.title}</a>
</li>
))}
</ul>
);
ProgramsList.propTypes = {
programs: PropTypes.arrayOf(
PropTypes.shape({
programUrl: PropTypes.string,
title: PropTypes.string,
}),
).isRequired,
};
export default ProgramsList;

View File

@@ -0,0 +1,23 @@
import { shallow } from 'enzyme';
import { ProgramsList } from './ProgramsList';
describe('ProgramsList', () => {
const programs = [
{
programUrl: 'http://example.com',
title: 'Example Program 1',
},
{
programUrl: 'http://example.com',
title: 'Example Program 2',
},
];
it('renders correctly', () => {
const wrapper = shallow(<ProgramsList programs={programs} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('li').length).toEqual(programs.length);
});
});

View File

@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProgramsList renders correctly 1`] = `
<ul
className="related-programs-list-container"
>
<li
className="my-2"
key="http://example.com"
>
<a
href="http://example.com"
>
Example Program 1
</a>
</li>
<li
className="my-2"
key="http://example.com"
>
<a
href="http://example.com"
>
Example Program 2
</a>
</li>
</ul>
`;

View File

@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RelatedProgramsBanner render with programs 1`] = `
<Banner
className="bg-white border-top border-bottom mb-0 related-programs-banner"
icon={[MockFunction icons.Program]}
variant="info"
>
Related Programs:
<ProgramsList
programs={
Array [
Object {
"title": "Program 1",
"url": "http://example.com/program1",
},
Object {
"title": "Program 2",
"url": "http://example.com/program2",
},
]
}
/>
</Banner>
`;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Program } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import Banner from 'components/Banner';
import ProgramList from './ProgramsList';
import messages from './messages';
export const RelatedProgramsBanner = ({ cardId }) => {
const { formatMessage } = useIntl();
const programData = reduxHooks.useCardRelatedProgramsData(cardId);
return (
programData?.length > 0 && (
<Banner
icon={Program}
className="bg-white border-top border-bottom mb-0 related-programs-banner"
>
{formatMessage(messages.relatedPrograms)}
<ProgramList programs={programData.list} />
</Banner>
)
);
};
RelatedProgramsBanner.propTypes = {
cardId: PropTypes.string.isRequired,
};
export default RelatedProgramsBanner;

View File

@@ -0,0 +1,42 @@
import { shallow } from 'enzyme';
import { reduxHooks } from 'hooks';
import RelatedProgramsBanner from '.';
jest.mock('./ProgramsList', () => 'ProgramsList');
jest.mock('hooks', () => ({
reduxHooks: {
useCardRelatedProgramsData: jest.fn(),
},
}));
const cardId = 'test-card-id';
describe('RelatedProgramsBanner', () => {
test('render empty', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue({
length: 0,
});
const el = shallow(<RelatedProgramsBanner cardId={cardId} />);
expect(el.isEmptyRender()).toEqual(true);
});
test('render with programs', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue({
list: [
{
title: 'Program 1',
url: 'http://example.com/program1',
},
{
title: 'Program 2',
url: 'http://example.com/program2',
},
],
length: 2,
});
const el = shallow(<RelatedProgramsBanner cardId={cardId} />);
expect(el).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,31 @@
import { StrictDict } from 'utils';
export const messages = StrictDict({
relatedPrograms: {
id: 'learner-dash.courseCard.banners.relatedPrograms',
description: 'title for related programs banner',
defaultMessage: 'Related Programs: ',
},
expandBanner: {
id: 'learner-dash.courseCard.banners.expandBanner',
description: 'expand banner button text',
defaultMessage: 'More',
},
expandBannerAlt: {
id: 'learner-dash.courseCard.banners.expandBannerAlt',
description: 'expand banner button alt text',
defaultMessage: 'Expand banner icon',
},
collapseBanner: {
id: 'learner-dash.courseCard.banners.collapseBanner',
description: 'collapse banner button text',
defaultMessage: 'Less',
},
collapseBannerAlt: {
id: 'learner-dash.courseCard.banners.collapseBannerAlt',
description: 'collapse banner button alt text',
defaultMessage: 'Collapse banner icon',
},
});
export default messages;

View File

@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardBanners render with isEnrolled false 1`] = `
<div
className="course-card-banners"
data-testid="CourseCardBanners"
>
<RelatedProgramsBanner
cardId="test-card-id"
/>
<CourseBanner
cardId="test-card-id"
/>
<EntitlementBanner
cardId="test-card-id"
/>
</div>
`;
exports[`CourseCardBanners renders default CourseCardBanners 1`] = `
<div
className="course-card-banners"
data-testid="CourseCardBanners"
>
<RelatedProgramsBanner
cardId="test-card-id"
/>
<CourseBanner
cardId="test-card-id"
/>
<EntitlementBanner
cardId="test-card-id"
/>
<CertificateBanner
cardId="test-card-id"
/>
<CreditBanner
cardId="test-card-id"
/>
</div>
`;

View File

@@ -7,11 +7,13 @@ import CourseBanner from './CourseBanner';
import CertificateBanner from './CertificateBanner';
import CreditBanner from './CreditBanner';
import EntitlementBanner from './EntitlementBanner';
import RelatedProgramsBanner from './RelatedProgramsBanner';
export const CourseCardBanners = ({ cardId }) => {
const { isEnrolled } = reduxHooks.useCardEnrollmentData(cardId);
return (
<div className="course-card-banners" data-testid="CourseCardBanners">
<RelatedProgramsBanner cardId={cardId} />
<CourseBanner cardId={cardId} />
<EntitlementBanner cardId={cardId} />
{isEnrolled && <CertificateBanner cardId={cardId} />}

View File

@@ -0,0 +1,32 @@
import { shallow } from 'enzyme';
import { reduxHooks } from 'hooks';
import CourseCardBanners from '.';
jest.mock('./CourseBanner', () => 'CourseBanner');
jest.mock('./CertificateBanner', () => 'CertificateBanner');
jest.mock('./CreditBanner', () => 'CreditBanner');
jest.mock('./EntitlementBanner', () => 'EntitlementBanner');
jest.mock('./RelatedProgramsBanner', () => 'RelatedProgramsBanner');
jest.mock('hooks', () => ({
reduxHooks: {
useCardEnrollmentData: jest.fn(() => ({ isEnrolled: true })),
},
}));
describe('CourseCardBanners', () => {
const props = {
cardId: 'test-card-id',
};
test('renders default CourseCardBanners', () => {
const wrapper = shallow(<CourseCardBanners {...props} />);
expect(wrapper).toMatchSnapshot();
});
test('render with isEnrolled false', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
const wrapper = shallow(<CourseCardBanners {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -10,7 +10,6 @@ import CourseCardMenu from './components/CourseCardMenu';
import CourseCardActions from './components/CourseCardActions';
import CourseCardDetails from './components/CourseCardDetails';
import CourseCardTitle from './components/CourseCardTitle';
import RelatedProgramsBadge from './components/RelatedProgramsBadge';
import './CourseCard.scss';
@@ -34,7 +33,6 @@ export const CourseCard = ({
<CourseCardDetails cardId={cardId} />
</Card.Section>
<Card.Footer orientation={orientation}>
<RelatedProgramsBadge cardId={cardId} />
<CourseCardActions cardId={cardId} />
</Card.Footer>
</Card.Body>

View File

@@ -14,7 +14,6 @@ jest.mock('./components/CourseCardMenu', () => 'CourseCardMenu');
jest.mock('./components/CourseCardActions', () => 'CourseCardActions');
jest.mock('./components/CourseCardDetails', () => 'CourseCardDetails');
jest.mock('./components/CourseCardTitle', () => 'CourseCardTitle');
jest.mock('./components/RelatedProgramsBadge', () => 'RelatedProgramsBadge');
const cardId = 'test-card-id';

View File

@@ -18,6 +18,7 @@ exports[`NoCoursesView snapshot 1`] = `
<Button
as="a"
href="course-search-url"
iconBefore={[MockFunction icons.Search]}
variant="brand"
>
Explore courses

View File

@@ -3,20 +3,14 @@ import { shallow } from 'enzyme';
import EmptyCourse from '.';
jest.mock('data/redux', () => ({
hooks: {
useRecommendedCoursesData: jest.fn(() => ({ courses: [], isControl: false })),
useRequestIsPending: jest.fn(),
usePlatformSettingsData: () => ({
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: 'course-search-url',
}),
})),
},
}));
jest.mock('containers/Dashboard/hooks', () => ({
useIsDashboardCollapsed: jest.fn(() => true),
}));
describe('NoCoursesView', () => {
test('snapshot', () => {
expect(shallow(<EmptyCourse />)).toMatchSnapshot();

View File

@@ -37,6 +37,7 @@ exports[`LearnerDashboardHeader snapshots with collapsed 1`] = `
iconAs="Icon"
invertColors={true}
onClick={[MockFunction findCoursesNavClicked('test-course-search-url')]}
src={[MockFunction icons.Search]}
variant="primary"
/>
<UserMenu />
@@ -78,6 +79,7 @@ exports[`LearnerDashboardHeader snapshots without collapsed 1`] = `
<Button
as="a"
href="test-course-search-url"
iconBefore={[MockFunction icons.Search]}
onClick={[MockFunction findCoursesNavClicked('test-course-search-url')]}
variant="inverse-tertiary"
>

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import messages from './messages';
export const BrandLogo = () => {
const { formatMessage } = useIntl();
const dashboard = reduxHooks.useEnterpriseDashboardData();
return (
<a href={dashboard?.url || '/'} className="mx-auto">
<img
className="logo py-3"
src="https://edx-cdn.org/v3/prod/logo.svg"
alt={formatMessage(messages.logoAltText)}
/>
</a>
);
};
BrandLogo.propTypes = {};
export default BrandLogo;

View File

@@ -0,0 +1,28 @@
import { shallow } from 'enzyme';
import { reduxHooks } from 'hooks';
import BrandLogo from './BrandLogo';
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: jest.fn(),
},
}));
describe('BrandLogo', () => {
test('dashboard defined', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce({
url: 'url',
});
const wrapper = shallow(<BrandLogo />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('a').prop('href')).toEqual('url');
});
test('dashboard undefined', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
const wrapper = shallow(<BrandLogo />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('a').prop('href')).toEqual('/');
});
});

View File

@@ -0,0 +1,96 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Button } from '@edx/paragon';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import { findCoursesNavDropdownClicked } from '../hooks';
import messages from '../messages';
export const CollapseMenuBody = ({ isOpen }) => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const dashboard = reduxHooks.useEnterpriseDashboardData();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const exploreCoursesClick = findCoursesNavDropdownClicked(courseSearchUrl);
return (
isOpen && (
<div className="d-flex flex-column shadow-sm nav-small-menu">
<Button as="a" href="/" variant="inverse-primary">
{formatMessage(messages.course)}
</Button>
<Button as="a" href={urls.programsUrl} variant="inverse-primary">
{formatMessage(messages.program)}
</Button>
<Button
as="a"
href={courseSearchUrl}
variant="inverse-primary"
onClick={exploreCoursesClick}
>
{formatMessage(messages.discoverNew)}
</Button>
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
{formatMessage(messages.help)}
</Button>
{authenticatedUser && (
<>
{dashboard && (
<Button as="a" href={dashboard.url} variant="inverse-primary">
{formatMessage(messages.dashboard)}
</Button>
)}
<Button
as="a"
href={`${getConfig().LMS_BASE_URL}/u/${
authenticatedUser.username
}`}
variant="inverse-primary"
>
{formatMessage(messages.profile)}
</Button>
<Button
as="a"
href={`${getConfig().LMS_BASE_URL}/account/settings`}
variant="inverse-primary"
>
{formatMessage(messages.account)}
</Button>
{getConfig().ORDER_HISTORY_URL && (
<Button
as="a"
variant="inverse-primary"
href={getConfig().ORDER_HISTORY_URL}
>
{formatMessage(messages.orderHistory)}
</Button>
)}
<Button
as="a"
href={getConfig().LOGOUT_URL}
variant="inverse-primary"
>
{formatMessage(messages.signOut)}
</Button>
</>
)}
</div>
)
);
};
CollapseMenuBody.propTypes = {
isOpen: PropTypes.bool.isRequired,
};
export default CollapseMenuBody;

View File

@@ -0,0 +1,48 @@
import { shallow } from 'enzyme';
import { AppContext } from '@edx/frontend-platform/react';
import CollapseMenuBody from './CollapseMenuBody';
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
username: 'username',
},
},
}));
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: () => ({
url: 'url',
}),
usePlatformSettingsData: () => ({
courseSearchUrl: 'courseSearchUrl',
}),
},
}));
jest.mock('../hooks', () => ({
findCoursesNavDropdownClicked: (url) => jest.fn().mockName(`findCoursesNavDropdownClicked("${url}")`),
}));
describe('CollapseMenuBody', () => {
test('render', () => {
const wrapper = shallow(<CollapseMenuBody isOpen />);
expect(wrapper).toMatchSnapshot();
});
test('render empty if not open', () => {
const wrapper = shallow(<CollapseMenuBody isOpen={false} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
});
test('render unauthenticated', () => {
const { authenticatedUser } = AppContext;
AppContext.authenticatedUser = null;
const wrapper = shallow(<CollapseMenuBody isOpen />);
expect(wrapper).toMatchSnapshot();
AppContext.authenticatedUser = authenticatedUser;
});
});

View File

@@ -0,0 +1,103 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollapseMenuBody render 1`] = `
<div
className="d-flex flex-column shadow-sm nav-small-menu"
>
<Button
as="a"
href="/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
href="courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
<Button
as="a"
href="url"
variant="inverse-primary"
>
Dashboard
</Button>
<Button
as="a"
href="http://localhost:18000/u/username"
variant="inverse-primary"
>
Profile
</Button>
<Button
as="a"
href="http://localhost:18000/account/settings"
variant="inverse-primary"
>
Account
</Button>
<Button
as="a"
href="http://localhost:18000/logout"
variant="inverse-primary"
>
Sign Out
</Button>
</div>
`;
exports[`CollapseMenuBody render empty if not open 1`] = `""`;
exports[`CollapseMenuBody render unauthenticated 1`] = `
<div
className="d-flex flex-column shadow-sm nav-small-menu"
>
<Button
as="a"
href="/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
href="courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
</div>
`;

View File

@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollapsedHeader render nothing if not collapsed 1`] = `""`;
exports[`CollapsedHeader renders 1`] = `
<Fragment>
<header
className="d-flex shadow-sm align-items-center learner-variant-header"
>
<IconButton
alt="Close"
className="p-4"
iconAs="Icon"
invertColors={true}
isActive={true}
onClick={[MockFunction toggleIsOpen]}
variant="primary"
/>
<mockConstructor />
</header>
<mockConstructor
isOpen={false}
/>
</Fragment>
`;
exports[`CollapsedHeader renders with isOpen true 1`] = `
<Fragment>
<header
className="d-flex shadow-sm align-items-center learner-variant-header"
>
<IconButton
alt="Menu"
className="p-4"
iconAs="Icon"
invertColors={true}
isActive={true}
onClick={[MockFunction toggleIsOpen]}
src={[MockFunction icons.Close]}
variant="primary"
/>
<mockConstructor />
</header>
<mockConstructor
isOpen={true}
/>
</Fragment>
`;

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Menu, Close } from '@edx/paragon/icons';
import { IconButton, Icon } from '@edx/paragon';
import { useLearnerDashboardHeaderVariantData, useIsCollapsed } from '../hooks';
import CollapseMenuBody from './CollapseMenuBody';
import BrandLogo from '../BrandLogo';
import messages from '../messages';
export const CollapsedHeader = () => {
const { formatMessage } = useIntl();
const isCollapsed = useIsCollapsed();
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderVariantData();
return (
isCollapsed && (
<>
<header className="d-flex shadow-sm align-items-center learner-variant-header">
<IconButton
invertColors
isActive
src={isOpen ? Close : Menu}
iconAs={Icon}
alt={
isOpen
? formatMessage(messages.collapseMenuOpenAltText)
: formatMessage(messages.collapseMenuClosedAltText)
}
onClick={toggleIsOpen}
variant="primary"
className="p-4"
/>
<BrandLogo />
</header>
<CollapseMenuBody isOpen={isOpen} />
</>
)
);
};
CollapsedHeader.propTypes = {};
export default CollapsedHeader;

View File

@@ -0,0 +1,38 @@
import { shallow } from 'enzyme';
import CollapsedHeader from '.';
import { useLearnerDashboardHeaderVariantData, useIsCollapsed } from '../hooks';
jest.mock('../BrandLogo', () => jest.fn(() => 'BrandLogo'));
jest.mock('./CollapseMenuBody', () => jest.fn(() => 'CollapseMenuBody'));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(() => true),
useLearnerDashboardHeaderVariantData: jest.fn(() => ({
isOpen: false,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
})),
}));
describe('CollapsedHeader', () => {
it('renders', () => {
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper).toMatchSnapshot();
});
it('render nothing if not collapsed', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper).toMatchSnapshot();
});
it('renders with isOpen true', () => {
useLearnerDashboardHeaderVariantData.mockReturnValueOnce({
isOpen: true,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
});
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,65 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { AvatarButton, Dropdown } from '@edx/paragon';
import { reduxHooks } from 'hooks';
import messages from '../messages';
export const AuthenticatedUserDropdown = () => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const dashboard = reduxHooks.useEnterpriseDashboardData();
return (
authenticatedUser && (
<Dropdown className="user-dropdown pr4">
<Dropdown.Toggle
as={AvatarButton}
src={authenticatedUser.profileImage}
id="user"
variant="light"
className="p-4"
>
<span data-hj-suppress className="d-md-inline">
{authenticatedUser.username}
</span>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
<Dropdown.Header>SWITCH DASHBOARD</Dropdown.Header>
<Dropdown.Item as="a" href="/edx-dashboard" className="active">
Personal
</Dropdown.Item>
{!!dashboard && (
<Dropdown.Item as="a" href={dashboard.url} key={dashboard.label}>
{dashboard.label} {formatMessage(messages.dashboard)}
</Dropdown.Item>
)}
<Dropdown.Divider />
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${authenticatedUser.username}`}>
{formatMessage(messages.profile)}
</Dropdown.Item>
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
{formatMessage(messages.account)}
</Dropdown.Item>
{getConfig().ORDER_HISTORY_URL && (
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
{formatMessage(messages.orderHistory)}
</Dropdown.Item>
)}
<Dropdown.Divider />
<Dropdown.Item href={getConfig().LOGOUT_URL}>
{formatMessage(messages.signOut)}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
);
};
AuthenticatedUserDropdown.propTypes = {};
export default AuthenticatedUserDropdown;

View File

@@ -0,0 +1,57 @@
import { shallow } from 'enzyme';
import { reduxHooks } from 'hooks';
import { AppContext } from '@edx/frontend-platform/react';
import { AuthenticatedUserDropdown } from './AuthenticatedUserDropdown';
import { useIsCollapsed } from '../hooks';
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
profileImage: 'profileImage',
username: 'username',
},
},
}));
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: jest.fn(),
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: 'test-course-search-url',
})),
},
}));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavDropdownClicked: (href) => jest.fn().mockName(`findCoursesNavDropdownClicked('${href}')`),
}));
describe('AuthenticatedUserDropdown', () => {
const defaultDashboardData = {
label: 'label',
url: 'url',
};
describe('snapshots', () => {
test('no auth render empty', () => {
const { authenticatedUser } = AppContext;
AppContext.authenticatedUser = null;
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
AppContext.authenticatedUser = authenticatedUser;
});
test('with enterprise dashboard', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(defaultDashboardData);
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper).toMatchSnapshot();
});
test('without enterprise dashboard and expanded', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,114 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AuthenticatedUserDropdown snapshots no auth render empty 1`] = `""`;
exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
<Dropdown
className="user-dropdown pr4"
>
<Dropdown.Toggle
className="p-4"
id="user"
src="profileImage"
variant="light"
>
<span
className="d-md-inline"
data-hj-suppress={true}
>
username
</span>
</Dropdown.Toggle>
<Dropdown.Menu
className="dropdown-menu-right"
>
<Dropdown.Header>
SWITCH DASHBOARD
</Dropdown.Header>
<Dropdown.Item
as="a"
className="active"
href="/edx-dashboard"
>
Personal
</Dropdown.Item>
<Dropdown.Item
as="a"
href="url"
key="label"
>
label
Dashboard
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://localhost:18000/u/username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="http://localhost:18000/account/settings"
>
Account
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://localhost:18000/logout"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
`;
exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and expanded 1`] = `
<Dropdown
className="user-dropdown pr4"
>
<Dropdown.Toggle
className="p-4"
id="user"
src="profileImage"
variant="light"
>
<span
className="d-md-inline"
data-hj-suppress={true}
>
username
</span>
</Dropdown.Toggle>
<Dropdown.Menu
className="dropdown-menu-right"
>
<Dropdown.Header>
SWITCH DASHBOARD
</Dropdown.Header>
<Dropdown.Item
as="a"
className="active"
href="/edx-dashboard"
>
Personal
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://localhost:18000/u/username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="http://localhost:18000/account/settings"
>
Account
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://localhost:18000/logout"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
`;

View File

@@ -0,0 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExpandedHeader render 1`] = `
<header
className="d-flex shadow-sm align-items-center learner-variant-header pl-4"
>
<div
className="flex-grow-1 d-flex align-items-center"
>
<BrandLogo />
<Button
as="a"
className="p-4 course-link"
href="/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
className="p-4"
href="programsUrl"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
className="p-4"
href="courseSearchUrl"
onClick={[MockFunction findCoursesNavClicked("courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<span
className="flex-grow-1"
/>
<Button
as="a"
className="p-4"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
</div>
<AuthenticatedUserDropdown />
</header>
`;
exports[`ExpandedHeader render empty if collapsed 1`] = `""`;

View File

@@ -0,0 +1,73 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
import messages from '../messages';
import BrandLogo from '../BrandLogo';
export const ExpandedHeader = () => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const isCollapsed = useIsCollapsed();
const exploreCoursesClick = findCoursesNavClicked(courseSearchUrl);
return (
!isCollapsed && (
<header className="d-flex shadow-sm align-items-center learner-variant-header pl-4">
<div className="flex-grow-1 d-flex align-items-center">
<BrandLogo />
<Button
as="a"
href="/"
variant="inverse-primary"
className="p-4 course-link"
>
{formatMessage(messages.course)}
</Button>
<Button
as="a"
href={urls.programsUrl}
variant="inverse-primary"
className="p-4"
>
{formatMessage(messages.program)}
</Button>
<Button
as="a"
href={courseSearchUrl}
variant="inverse-primary"
className="p-4"
onClick={exploreCoursesClick}
>
{formatMessage(messages.discoverNew)}
</Button>
<span className="flex-grow-1" />
<Button
as="a"
href={getConfig().SUPPORT_URL}
variant="inverse-primary"
className="p-4"
>
{formatMessage(messages.help)}
</Button>
</div>
<AuthenticatedUserDropdown />
</header>
)
);
};
ExpandedHeader.propTypes = {};
export default ExpandedHeader;

View File

@@ -0,0 +1,40 @@
import { shallow } from 'enzyme';
import ExpandedHeader from '.';
import { useIsCollapsed } from '../hooks';
jest.mock('data/services/lms/urls', () => ({
programsUrl: 'programsUrl',
}));
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: () => ({
courseSearchUrl: 'courseSearchUrl',
}),
},
}));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
}));
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
jest.mock('../BrandLogo', () => 'BrandLogo');
describe('ExpandedHeader', () => {
test('render', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper).toMatchSnapshot();
});
test('render empty if collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
});
});

View File

@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BrandLogo dashboard defined 1`] = `
<a
className="mx-auto"
href="url"
>
<img
alt="edX, Inc. Dashboard"
className="logo py-3"
src="https://edx-cdn.org/v3/prod/logo.svg"
/>
</a>
`;
exports[`BrandLogo dashboard undefined 1`] = `
<a
className="mx-auto"
href="/"
>
<img
alt="edX, Inc. Dashboard"
className="logo py-3"
src="https://edx-cdn.org/v3/prod/logo.svg"
/>
</a>
`;

View File

@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LearnerDashboardHeaderVariant render 1`] = `
<Fragment>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</Fragment>
`;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { useWindowSize, breakpoints } from '@edx/paragon';
import track from 'tracking';
import { StrictDict } from 'utils';
import { linkNames } from 'tracking/constants';
import * as module from './hooks';
export const state = StrictDict({
isOpen: (val) => React.useState(val), // eslint-disable-line
});
export const useIsCollapsed = () => {
const { width } = useWindowSize();
const isCollapsed = React.useMemo(() => (width <= breakpoints.large.minWidth), [width]);
return isCollapsed;
};
export const findCoursesNavClicked = (href) => track.findCourses.findCoursesClicked(href, {
linkName: linkNames.learnerHomeNavExplore,
});
export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCoursesClicked(href, {
linkName: linkNames.learnerHomeNavDropdownExplore,
});
export const useLearnerDashboardHeaderVariantData = () => {
const [isOpen, setIsOpen] = module.state.isOpen(false);
const toggleIsOpen = () => setIsOpen(!isOpen);
return {
isOpen,
toggleIsOpen,
};
};
export default {
useIsCollapsed,
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderVariantData,
};

View File

@@ -0,0 +1,69 @@
import { useWindowSize, breakpoints } from '@edx/paragon';
import track from 'tracking';
import { linkNames } from 'tracking/constants';
import { MockUseState } from 'testUtils';
import * as hooks from './hooks';
const state = new MockUseState(hooks);
const {
useIsCollapsed,
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderVariantData,
} = hooks;
jest.mock('tracking', () => ({
findCourses: {
findCoursesClicked: jest.fn(),
},
}));
const url = 'http://example.com';
describe('LearnerDashboardHeaderVariant hooks', () => {
describe('state values', () => {
state.testGetter(state.keys.isOpen);
});
describe('useIsCollapsed', () => {
test('large screen is not collapsed', () => {
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth + 1 });
expect(useIsCollapsed()).toEqual(false);
});
test('small screen is collapsed', () => {
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth - 1 });
expect(useIsCollapsed()).toEqual(true);
});
});
describe('findCoursesNavClicked', () => {
test('calls tracking with nav link name', () => {
findCoursesNavClicked(url);
expect(track.findCourses.findCoursesClicked).toHaveBeenCalledWith(url, {
linkName: linkNames.learnerHomeNavExplore,
});
});
});
describe('findCoursesNavDropdownClicked', () => {
test('calls tracking with dropdown link name', () => {
findCoursesNavDropdownClicked(url);
expect(track.findCourses.findCoursesClicked).toHaveBeenCalledWith(url, {
linkName: linkNames.learnerHomeNavDropdownExplore,
});
});
});
describe('useLearnerDashboardHeaderVariantData', () => {
test('default state', () => {
state.mock();
const out = useLearnerDashboardHeaderVariantData();
state.expectInitializedWith(state.keys.isOpen, false);
out.toggleIsOpen();
expect(state.values.isOpen).toEqual(true);
});
});
});

View File

@@ -0,0 +1,22 @@
import React from 'react';
import MasqueradeBar from 'containers/MasqueradeBar';
import ConfirmEmailBanner from 'containers/LearnerDashboardHeader/ConfirmEmailBanner';
import CollapsedHeader from './CollapsedHeader';
import ExpandedHeader from './ExpandedHeader';
import './index.scss';
export const LearnerDashboardHeaderVariant = () => (
<>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</>
);
LearnerDashboardHeaderVariant.propTypes = {};
export default LearnerDashboardHeaderVariant;

View File

@@ -0,0 +1,38 @@
.dropdown-menu-collapse {
width: 100vw;
position: absolute;
left: 0;
}
.learner-variant-header {
a {
// needed to make the link not resize the header
border-bottom: 2px solid transparent;
}
.course-link {
border-bottom: 2px solid !important;
}
.course-link:hover {
border-bottom: inherit !important;
}
}
.nav-small-menu {
> * {
justify-content: flex-start !important;
border-radius: 0 !important;
border-top: 1px solid #ddd !important;
&::after {
content: '\00BB';
padding-left: 10px;
}
}
}
.logo {
// copy from legacy dashboard
height: 40px;
}

View File

@@ -0,0 +1,18 @@
import { shallow } from 'enzyme';
import LearnerDashboardHeaderVariant from '.';
jest.mock('containers/LearnerDashboardHeader/ConfirmEmailBanner', () => 'ConfirmEmailBanner');
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');
jest.mock('./CollapsedHeader', () => 'CollapsedHeader');
jest.mock('./ExpandedHeader', () => 'ExpandedHeader');
describe('LearnerDashboardHeaderVariant', () => {
test('render', () => {
const wrapper = shallow(<LearnerDashboardHeaderVariant />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ConfirmEmailBanner')).toHaveLength(1);
expect(wrapper.find('MasqueradeBar')).toHaveLength(1);
expect(wrapper.find('CollapsedHeader')).toHaveLength(1);
expect(wrapper.find('ExpandedHeader')).toHaveLength(1);
});
});

View File

@@ -0,0 +1,71 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
dashboard: {
id: 'learnerVariantDashboard.menu.dashboard.label',
defaultMessage: 'Dashboard',
description: 'The text for the user menu Dashboard navigation link.',
},
help: {
id: 'learnerVariantDashboard.help.label',
defaultMessage: 'Help',
description: 'The text for the link to the Help Center',
},
profile: {
id: 'learnerVariantDashboard.menu.profile.label',
defaultMessage: 'Profile',
description: 'The text for the user menu Profile navigation link.',
},
viewPrograms: {
id: 'learnerVariantDashboard.menu.viewPrograms.label',
defaultMessage: 'View Programs',
description: 'The text for the user menu View Programs navigation link.',
},
account: {
id: 'learnerVariantDashboard.menu.account.label',
defaultMessage: 'Account',
description: 'The text for the user menu Account navigation link.',
},
orderHistory: {
id: 'learnerVariantDashboard.menu.orderHistory.label',
defaultMessage: 'Order History',
description: 'The text for the user menu Order History navigation link.',
},
signOut: {
id: 'learnerVariantDashboard.menu.signOut.label',
defaultMessage: 'Sign Out',
description: 'The label for the user menu Sign Out action.',
},
course: {
id: 'learnerVariantDashboard.course',
defaultMessage: 'Courses',
description: 'Header link for switching to dashboard page.',
},
program: {
id: 'learnerVariantDashboard.program',
defaultMessage: 'Programs',
description: 'Header link for switching to program page.',
},
discoverNew: {
id: 'learnerVariantDashboard.discoverNew',
defaultMessage: 'Discover New',
description: 'Header link for switching to discover page.',
},
logoAltText: {
id: 'learnerVariantDashboard.logoAltText',
defaultMessage: 'edX, Inc. Dashboard',
description: 'Alt text for the edX logo.',
},
collapseMenuOpenAltText: {
id: 'learnerVariantDashboard.collapseMenuOpenAltText',
defaultMessage: 'Menu',
description: 'Alt text for the collapse menu icon when the menu is open.',
},
collapseMenuClosedAltText: {
id: 'learnerVariantDashboard.collapseMenuClosedAltText',
defaultMessage: 'Close',
description: 'Alt text for the collapse menu icon when the menu is closed.',
},
});
export default messages;

View File

@@ -194,6 +194,7 @@ jest.mock('@edx/paragon/icons', () => ({
ArrowBack: jest.fn().mockName('icons.ArrowBack'),
ArrowDropDown: jest.fn().mockName('icons.ArrowDropDown'),
ArrowDropUp: jest.fn().mockName('icons.ArrowDropUp'),
Book: jest.fn().mockName('icons.Book'),
Cancel: jest.fn().mockName('icons.Cancel'),
Close: jest.fn().mockName('icons.Close'),
CheckCircle: jest.fn().mockName('icons.CheckCircle'),
@@ -208,6 +209,7 @@ jest.mock('@edx/paragon/icons', () => ({
Tune: jest.fn().mockName('icons.Tune'),
PersonSearch: jest.fn().mockName('icons.PersonSearch'),
Program: jest.fn().mockName('icons.Program'),
Search: jest.fn().mockName('icons.Search'),
}));
jest.mock('data/constants/app', () => ({

View File

@@ -48,6 +48,10 @@ export const LoadedView = ({
);
};
LoadedView.defaultProps = {
isControl: true,
};
LoadedView.propTypes = {
courses: PropTypes.arrayOf(PropTypes.shape({
courseKey: PropTypes.string,
@@ -55,7 +59,7 @@ LoadedView.propTypes = {
logoImageUrl: PropTypes.string,
marketingUrl: PropTypes.string,
})).isRequired,
isControl: PropTypes.oneOf([true, false, null]).isRequired,
isControl: PropTypes.oneOf([true, false, null]),
};
export default LoadedView;

View File

@@ -65,6 +65,7 @@ exports[`RecommendationsPanel LoadedView snapshot with personalize recommendatio
<Button
as="a"
href="course-search-url"
iconBefore={[MockFunction icons.Search]}
onClick={[MockFunction track.findCoursesWidgetClicked('course-search-url')]}
variant="tertiary"
>
@@ -139,6 +140,7 @@ exports[`RecommendationsPanel LoadedView snapshot without personalize recommenda
<Button
as="a"
href="course-search-url"
iconBefore={[MockFunction icons.Search]}
onClick={[MockFunction track.findCoursesWidgetClicked('course-search-url')]}
variant="tertiary"
>