diff --git a/src/App.jsx b/src/App.jsx index 0d08d32..a6ce7d7 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -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 = () => { {formatMessage(messages.pageTitle)}
- +
{hasNetworkFailure ? ( diff --git a/src/App.scss b/src/App.scss index 14948b8..c78c30b 100755 --- a/src/App.scss +++ b/src/App.scss @@ -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; } diff --git a/src/App.test.jsx b/src/App.test.jsx index bc57ea7..6734293 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -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); diff --git a/src/__snapshots__/App.test.jsx.snap b/src/__snapshots__/App.test.jsx.snap index d819557..443fdba 100644 --- a/src/__snapshots__/App.test.jsx.snap +++ b/src/__snapshots__/App.test.jsx.snap @@ -11,7 +11,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
- +
- +
@@ -63,7 +63,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
- +
( - +export const Banner = ({ + children, variant, icon, className, +}) => ( + {children} ); 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; diff --git a/src/components/Banner.test.jsx b/src/components/Banner.test.jsx index c78cf2f..eca59dd 100644 --- a/src/components/Banner.test.jsx +++ b/src/components/Banner.test.jsx @@ -19,5 +19,9 @@ describe('Banner', () => { expect(wrapper.find(Alert).prop('variant')).toEqual('success'); }); + test('renders with custom class', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); }); }); diff --git a/src/components/EmailLink.test.jsx b/src/components/EmailLink.test.jsx new file mode 100644 index 0000000..2e1ab16 --- /dev/null +++ b/src/components/EmailLink.test.jsx @@ -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(); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.isEmptyRender()).toEqual(true); + }); + it('renders a MailtoLink when an address is provided', () => { + const wrapper = shallow(); + expect(wrapper.find('MailtoLink').length).toEqual(1); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/components/__snapshots__/Banner.test.jsx.snap b/src/components/__snapshots__/Banner.test.jsx.snap index 8139341..5f70d5b 100644 --- a/src/components/__snapshots__/Banner.test.jsx.snap +++ b/src/components/__snapshots__/Banner.test.jsx.snap @@ -10,6 +10,16 @@ exports[`Banner snapshot renders default banner 1`] = ` `; +exports[`Banner snapshot renders with custom class 1`] = ` + + Hello, world! + +`; + exports[`Banner snapshot renders with variants 1`] = ` + test@email.com + +`; + +exports[`EmailLink renders null when no address is provided 1`] = `""`; diff --git a/src/containers/CourseCard/CourseCard.scss b/src/containers/CourseCard/CourseCard.scss index f87c67e..244b30c 100644 --- a/src/containers/CourseCard/CourseCard.scss +++ b/src/containers/CourseCard/CourseCard.scss @@ -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; + } + } } } } diff --git a/src/containers/CourseCard/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/__snapshots__/index.test.jsx.snap index b9cbe1d..08f36c4 100644 --- a/src/containers/CourseCard/__snapshots__/index.test.jsx.snap +++ b/src/containers/CourseCard/__snapshots__/index.test.jsx.snap @@ -40,9 +40,6 @@ exports[`CourseCard component snapshot: collapsed 1`] = ` - @@ -99,9 +96,6 @@ exports[`CourseCard component snapshot: not collapsed 1`] = ` - diff --git a/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/ProgramsList.jsx b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/ProgramsList.jsx new file mode 100644 index 0000000..9f2b382 --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/ProgramsList.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const ProgramsList = ({ programs }) => ( + +); + +ProgramsList.propTypes = { + programs: PropTypes.arrayOf( + PropTypes.shape({ + programUrl: PropTypes.string, + title: PropTypes.string, + }), + ).isRequired, +}; + +export default ProgramsList; diff --git a/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/ProgramsList.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/ProgramsList.test.jsx new file mode 100644 index 0000000..5b2a3ab --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/ProgramsList.test.jsx @@ -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(); + expect(wrapper).toMatchSnapshot(); + + expect(wrapper.find('li').length).toEqual(programs.length); + }); +}); diff --git a/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/__snapshots__/ProgramsList.test.jsx.snap b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/__snapshots__/ProgramsList.test.jsx.snap new file mode 100644 index 0000000..6d50e8f --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/__snapshots__/ProgramsList.test.jsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProgramsList renders correctly 1`] = ` + +`; diff --git a/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..725a98e --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/__snapshots__/index.test.jsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RelatedProgramsBanner render with programs 1`] = ` + + Related Programs: + + +`; diff --git a/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/index.jsx b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/index.jsx new file mode 100644 index 0000000..96d1154 --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/index.jsx @@ -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 && ( + + {formatMessage(messages.relatedPrograms)} + + + ) + ); +}; +RelatedProgramsBanner.propTypes = { + cardId: PropTypes.string.isRequired, +}; + +export default RelatedProgramsBanner; diff --git a/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/index.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/index.test.jsx new file mode 100644 index 0000000..9b89a2b --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/index.test.jsx @@ -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(); + 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(); + expect(el).toMatchSnapshot(); + }); +}); diff --git a/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/messages.js b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/messages.js new file mode 100644 index 0000000..ce0307d --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardBanners/RelatedProgramsBanner/messages.js @@ -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; diff --git a/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..3da6b26 --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardBanners/__snapshots__/index.test.jsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CourseCardBanners render with isEnrolled false 1`] = ` +
+ + + +
+`; + +exports[`CourseCardBanners renders default CourseCardBanners 1`] = ` +
+ + + + + +
+`; diff --git a/src/containers/CourseCard/components/CourseCardBanners/index.jsx b/src/containers/CourseCard/components/CourseCardBanners/index.jsx index ee77e1b..b648690 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/index.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/index.jsx @@ -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 (
+ {isEnrolled && } diff --git a/src/containers/CourseCard/components/CourseCardBanners/index.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/index.test.jsx new file mode 100644 index 0000000..3f85b8c --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardBanners/index.test.jsx @@ -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(); + expect(wrapper).toMatchSnapshot(); + }); + test('render with isEnrolled false', () => { + reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false }); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/containers/CourseCard/index.jsx b/src/containers/CourseCard/index.jsx index e266a80..ce96cec 100644 --- a/src/containers/CourseCard/index.jsx +++ b/src/containers/CourseCard/index.jsx @@ -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 = ({ - diff --git a/src/containers/CourseCard/index.test.jsx b/src/containers/CourseCard/index.test.jsx index 5f09d5e..f4aa821 100644 --- a/src/containers/CourseCard/index.test.jsx +++ b/src/containers/CourseCard/index.test.jsx @@ -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'; diff --git a/src/containers/CourseList/NoCoursesView/__snapshots__/index.test.jsx.snap b/src/containers/CourseList/NoCoursesView/__snapshots__/index.test.jsx.snap index 5d60310..894f016 100644 --- a/src/containers/CourseList/NoCoursesView/__snapshots__/index.test.jsx.snap +++ b/src/containers/CourseList/NoCoursesView/__snapshots__/index.test.jsx.snap @@ -18,6 +18,7 @@ exports[`NoCoursesView snapshot 1`] = ` + + + + {authenticatedUser && ( + <> + {dashboard && ( + + )} + + + {getConfig().ORDER_HISTORY_URL && ( + + )} + + + )} +
+ ) + ); +}; + +CollapseMenuBody.propTypes = { + isOpen: PropTypes.bool.isRequired, +}; + +export default CollapseMenuBody; diff --git a/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/CollapseMenuBody.test.jsx b/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/CollapseMenuBody.test.jsx new file mode 100644 index 0000000..f2150c6 --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/CollapseMenuBody.test.jsx @@ -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(); + expect(wrapper).toMatchSnapshot(); + }); + + test('render empty if not open', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + test('render unauthenticated', () => { + const { authenticatedUser } = AppContext; + AppContext.authenticatedUser = null; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + AppContext.authenticatedUser = authenticatedUser; + }); +}); diff --git a/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/__snapshots__/CollapseMenuBody.test.jsx.snap b/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/__snapshots__/CollapseMenuBody.test.jsx.snap new file mode 100644 index 0000000..be663bc --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/__snapshots__/CollapseMenuBody.test.jsx.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollapseMenuBody render 1`] = ` +
+ + + + + + + + +
+`; + +exports[`CollapseMenuBody render empty if not open 1`] = `""`; + +exports[`CollapseMenuBody render unauthenticated 1`] = ` +
+ + + + +
+`; diff --git a/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/__snapshots__/index.test.jsx.snap b/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..c1dd843 --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/__snapshots__/index.test.jsx.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CollapsedHeader render nothing if not collapsed 1`] = `""`; + +exports[`CollapsedHeader renders 1`] = ` + +
+ + +
+ +
+`; + +exports[`CollapsedHeader renders with isOpen true 1`] = ` + +
+ + +
+ +
+`; diff --git a/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/index.jsx b/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/index.jsx new file mode 100644 index 0000000..0f2ce4a --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/index.jsx @@ -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 && ( + <> +
+ + +
+ + + ) + ); +}; + +CollapsedHeader.propTypes = {}; + +export default CollapsedHeader; diff --git a/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/index.test.jsx b/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/index.test.jsx new file mode 100644 index 0000000..7576b7c --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/CollapsedHeader/index.test.jsx @@ -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(); + expect(wrapper).toMatchSnapshot(); + }); + + it('render nothing if not collapsed', () => { + useIsCollapsed.mockReturnValueOnce(false); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders with isOpen true', () => { + useLearnerDashboardHeaderVariantData.mockReturnValueOnce({ + isOpen: true, + toggleIsOpen: jest.fn().mockName('toggleIsOpen'), + }); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/AuthenticatedUserDropdown.jsx b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/AuthenticatedUserDropdown.jsx new file mode 100644 index 0000000..ca661ab --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/AuthenticatedUserDropdown.jsx @@ -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 && ( + + + + {authenticatedUser.username} + + + + SWITCH DASHBOARD + + Personal + + {!!dashboard && ( + + {dashboard.label} {formatMessage(messages.dashboard)} + + )} + + + {formatMessage(messages.profile)} + + + {formatMessage(messages.account)} + + {getConfig().ORDER_HISTORY_URL && ( + + {formatMessage(messages.orderHistory)} + + )} + + + {formatMessage(messages.signOut)} + + + + ) + ); +}; + +AuthenticatedUserDropdown.propTypes = {}; + +export default AuthenticatedUserDropdown; diff --git a/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/AuthenticatedUserDropdown.test.jsx b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/AuthenticatedUserDropdown.test.jsx new file mode 100644 index 0000000..49f9c6d --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/AuthenticatedUserDropdown.test.jsx @@ -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(); + 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(); + expect(wrapper).toMatchSnapshot(); + }); + test('without enterprise dashboard and expanded', () => { + reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null); + useIsCollapsed.mockReturnValueOnce(false); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/__snapshots__/AuthenticatedUserDropdown.test.jsx.snap b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/__snapshots__/AuthenticatedUserDropdown.test.jsx.snap new file mode 100644 index 0000000..1664f7c --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/__snapshots__/AuthenticatedUserDropdown.test.jsx.snap @@ -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`] = ` + + + + username + + + + + SWITCH DASHBOARD + + + Personal + + + label + + Dashboard + + + + Profile + + + Account + + + + Sign Out + + + +`; + +exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and expanded 1`] = ` + + + + username + + + + + SWITCH DASHBOARD + + + Personal + + + + Profile + + + Account + + + + Sign Out + + + +`; diff --git a/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/__snapshots__/index.test.jsx.snap b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..591360e --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/__snapshots__/index.test.jsx.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExpandedHeader render 1`] = ` +
+
+ + + + + + +
+ +
+`; + +exports[`ExpandedHeader render empty if collapsed 1`] = `""`; diff --git a/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/index.jsx b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/index.jsx new file mode 100644 index 0000000..128caae --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/index.jsx @@ -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 && ( +
+
+ + + + + + + +
+ + +
+ ) + ); +}; + +ExpandedHeader.propTypes = {}; + +export default ExpandedHeader; diff --git a/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/index.test.jsx b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/index.test.jsx new file mode 100644 index 0000000..de153f9 --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/ExpandedHeader/index.test.jsx @@ -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(); + expect(wrapper).toMatchSnapshot(); + }); + + test('render empty if collapsed', () => { + useIsCollapsed.mockReturnValueOnce(true); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/src/containers/LearnerDashboardHeaderVariant/__snapshots__/BrandLogo.test.jsx.snap b/src/containers/LearnerDashboardHeaderVariant/__snapshots__/BrandLogo.test.jsx.snap new file mode 100644 index 0000000..96ecd4e --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/__snapshots__/BrandLogo.test.jsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BrandLogo dashboard defined 1`] = ` + + edX, Inc. Dashboard + +`; + +exports[`BrandLogo dashboard undefined 1`] = ` + + edX, Inc. Dashboard + +`; diff --git a/src/containers/LearnerDashboardHeaderVariant/__snapshots__/index.test.jsx.snap b/src/containers/LearnerDashboardHeaderVariant/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..3d86b7b --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/__snapshots__/index.test.jsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LearnerDashboardHeaderVariant render 1`] = ` + + + + + + +`; diff --git a/src/containers/LearnerDashboardHeaderVariant/hooks.js b/src/containers/LearnerDashboardHeaderVariant/hooks.js new file mode 100644 index 0000000..a3f8592 --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/hooks.js @@ -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, +}; diff --git a/src/containers/LearnerDashboardHeaderVariant/hooks.test.js b/src/containers/LearnerDashboardHeaderVariant/hooks.test.js new file mode 100644 index 0000000..a426dce --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/hooks.test.js @@ -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); + }); + }); +}); diff --git a/src/containers/LearnerDashboardHeaderVariant/index.jsx b/src/containers/LearnerDashboardHeaderVariant/index.jsx new file mode 100644 index 0000000..b8ed23e --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/index.jsx @@ -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 = () => ( + <> + + + + + +); + +LearnerDashboardHeaderVariant.propTypes = {}; + +export default LearnerDashboardHeaderVariant; diff --git a/src/containers/LearnerDashboardHeaderVariant/index.scss b/src/containers/LearnerDashboardHeaderVariant/index.scss new file mode 100644 index 0000000..d8bd8da --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/index.scss @@ -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; +} diff --git a/src/containers/LearnerDashboardHeaderVariant/index.test.jsx b/src/containers/LearnerDashboardHeaderVariant/index.test.jsx new file mode 100644 index 0000000..ff97e3b --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/index.test.jsx @@ -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(); + 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); + }); +}); diff --git a/src/containers/LearnerDashboardHeaderVariant/messages.js b/src/containers/LearnerDashboardHeaderVariant/messages.js new file mode 100644 index 0000000..70f3bbd --- /dev/null +++ b/src/containers/LearnerDashboardHeaderVariant/messages.js @@ -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; diff --git a/src/setupTest.jsx b/src/setupTest.jsx index fa5e8f8..d1ea6eb 100755 --- a/src/setupTest.jsx +++ b/src/setupTest.jsx @@ -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', () => ({ diff --git a/src/widgets/RecommendationsPanel/LoadedView.jsx b/src/widgets/RecommendationsPanel/LoadedView.jsx index d286fc2..8b3824c 100644 --- a/src/widgets/RecommendationsPanel/LoadedView.jsx +++ b/src/widgets/RecommendationsPanel/LoadedView.jsx @@ -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; diff --git a/src/widgets/RecommendationsPanel/__snapshots__/LoadedView.test.jsx.snap b/src/widgets/RecommendationsPanel/__snapshots__/LoadedView.test.jsx.snap index 07e6640..c555423 100644 --- a/src/widgets/RecommendationsPanel/__snapshots__/LoadedView.test.jsx.snap +++ b/src/widgets/RecommendationsPanel/__snapshots__/LoadedView.test.jsx.snap @@ -65,6 +65,7 @@ exports[`RecommendationsPanel LoadedView snapshot with personalize recommendatio