diff --git a/src/containers/CourseCard/components/CourseCardMenu/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/components/CourseCardMenu/__snapshots__/index.test.jsx.snap index 09cfc876..8f30919b 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/__snapshots__/index.test.jsx.snap +++ b/src/containers/CourseCard/components/CourseCardMenu/__snapshots__/index.test.jsx.snap @@ -1,8 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1`] = ` +exports[`CourseCardMenu default snapshot 1`] = ` - + - + `; -exports[`CourseCardMenu not enrolled, share disabled, email setting disabled snapshot 1`] = ` - - - - - - - -`; +exports[`CourseCardMenu renders null if showDropdown is false 1`] = `""`; diff --git a/src/containers/CourseCard/components/CourseCardMenu/hooks.js b/src/containers/CourseCard/components/CourseCardMenu/hooks.js index 5d646106..2380f87f 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/hooks.js +++ b/src/containers/CourseCard/components/CourseCardMenu/hooks.js @@ -36,3 +36,36 @@ export const useHandleToggleDropdown = (cardId) => { if (isOpen) { trackCourseEvent(); } }; }; + +export const useCourseCardMenu = (cardId) => { + const { courseName } = reduxHooks.useCardCourseData(cardId); + const { isEnrolled, isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId); + const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId); + const { isMasquerading } = reduxHooks.useMasqueradeData(); + const { isEarned } = reduxHooks.useCardCertificateData(cardId); + const handleTwitterShare = reduxHooks.useTrackCourseEvent( + track.socialShare, + cardId, + 'twitter', + ); + const handleFacebookShare = reduxHooks.useTrackCourseEvent( + track.socialShare, + cardId, + 'facebook', + ); + + const showUnenrollItem = isEnrolled && !isEarned; + const showDropdown = showUnenrollItem || isEmailEnabled || facebook.isEnabled || twitter.isEnabled; + + return { + courseName, + isMasquerading, + isEmailEnabled, + showUnenrollItem, + showDropdown, + facebook, + twitter, + handleTwitterShare, + handleFacebookShare, + }; +}; diff --git a/src/containers/CourseCard/components/CourseCardMenu/hooks.test.js b/src/containers/CourseCard/components/CourseCardMenu/hooks.test.js index fb5eea5c..a0073a38 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/hooks.test.js +++ b/src/containers/CourseCard/components/CourseCardMenu/hooks.test.js @@ -7,6 +7,11 @@ import * as hooks from './hooks'; jest.mock('hooks', () => ({ reduxHooks: { useTrackCourseEvent: jest.fn(), + useCardCourseData: jest.fn(), + useCardEnrollmentData: jest.fn(), + useCardSocialSettingsData: jest.fn(), + useMasqueradeData: jest.fn(), + useCardCertificateData: jest.fn(), }, })); @@ -14,6 +19,18 @@ const trackCourseEvent = jest.fn(); reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent); const state = new MockUseState(hooks); +const defaultSocialShare = { + facebook: { + isEnabled: true, + shareUrl: 'facebook-share-url', + socialBrand: 'facebook-social-brand', + }, + twitter: { + isEnabled: true, + shareUrl: 'twitter-share-url', + socialBrand: 'twitter-social-brand', + }, +}; const cardId = 'test-card-id'; let out; @@ -88,4 +105,116 @@ describe('CourseCardMenu hooks', () => { }); }); }); + + describe('useCourseCardMenu', () => { + const mockUseCourseCardMenu = ({ + courseName, + isEnrolled, + isEmailEnabled, + isMasquerading, + facebook, + twitter, + isEarned, + } = {}) => { + reduxHooks.useCardCourseData.mockReturnValueOnce({ courseName }); + reduxHooks.useCardSocialSettingsData.mockReturnValueOnce({ + facebook: { + ...defaultSocialShare.facebook, + ...facebook, + }, + twitter: { + ...defaultSocialShare.twitter, + ...twitter, + }, + }); + reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ + isEnrolled, + isEmailEnabled, + }); + reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading }); + reduxHooks.useCardCertificateData.mockReturnValueOnce({ isEarned }); + }; + afterEach(() => jest.resetAllMocks()); + describe('showUnenrollItem', () => { + test('return true', () => { + mockUseCourseCardMenu({ isEnrolled: true, isEarned: false }); + out = hooks.useCourseCardMenu(cardId); + expect(out.showUnenrollItem).toBeTruthy(); + }); + + test('return false', () => { + mockUseCourseCardMenu({ isEnrolled: true, isEarned: true }); + out = hooks.useCourseCardMenu(cardId); + expect(out.showUnenrollItem).toBeFalsy(); + + mockUseCourseCardMenu({ isEnrolled: false, isEarned: false }); + out = hooks.useCourseCardMenu(cardId); + expect(out.showUnenrollItem).toBeFalsy(); + + mockUseCourseCardMenu({ isEnrolled: false, isEarned: true }); + out = hooks.useCourseCardMenu(cardId); + expect(out.showUnenrollItem).toBeFalsy(); + }); + }); + + describe('showDropdown', () => { + test('return false iif everything is false', () => { + mockUseCourseCardMenu({ + isEnrolled: false, + isEarned: false, + isEmailEnabled: false, + facebook: { isEnabled: false }, + twitter: { isEnabled: false }, + }); + out = hooks.useCourseCardMenu(cardId); + expect(out.showDropdown).toBeFalsy(); + }); + + test('return true iif at least one is true', () => { + mockUseCourseCardMenu({ + isEnrolled: true, + isEarned: false, + isEmailEnabled: false, + facebook: { isEnabled: false }, + twitter: { isEnabled: false }, + }); + out = hooks.useCourseCardMenu(cardId); + expect(out.showDropdown).toBeTruthy(); + }); + }); + + test('return correct values', () => { + const expected = { + courseName: 'abitrary-course-name', + isMasquerading: 'abitrary-masquerading-value', + isEmailEnabled: 'abitrary-email-enabled-value', + facebook: { isEnabled: 'abitrary-facebook-value' }, + twitter: { isEnabled: 'abitrary-twitter-value' }, + }; + mockUseCourseCardMenu(expected); + out = hooks.useCourseCardMenu(cardId); + expect(out.courseName).toEqual(expected.courseName); + expect(out.isMasquerading).toEqual(expected.isMasquerading); + expect(out.isEmailEnabled).toEqual(expected.isEmailEnabled); + expect(out.facebook.isEnabled).toEqual(expected.facebook.isEnabled); + expect(out.twitter.isEnabled).toEqual(expected.twitter.isEnabled); + }); + + test('handleSocialShareClick', () => { + mockUseCourseCardMenu(); + + out = hooks.useCourseCardMenu(cardId); + expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledTimes(2); + expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith( + track.socialShare, + cardId, + 'facebook', + ); + expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith( + track.socialShare, + cardId, + 'twitter', + ); + }); + }); }); diff --git a/src/containers/CourseCard/components/CourseCardMenu/index.jsx b/src/containers/CourseCard/components/CourseCardMenu/index.jsx index f2c45242..915e996a 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/index.jsx +++ b/src/containers/CourseCard/components/CourseCardMenu/index.jsx @@ -6,14 +6,13 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Dropdown, Icon, IconButton } from '@edx/paragon'; import { MoreVert } from '@edx/paragon/icons'; -import track from 'tracking'; -import { reduxHooks } from 'hooks'; import EmailSettingsModal from 'containers/EmailSettingsModal'; import UnenrollConfirmModal from 'containers/UnenrollConfirmModal'; import { useEmailSettings, useUnenrollData, useHandleToggleDropdown, + useCourseCardMenu, } from './hooks'; import messages from './messages'; @@ -21,25 +20,26 @@ import messages from './messages'; export const CourseCardMenu = ({ cardId }) => { const { formatMessage } = useIntl(); - const { courseName } = reduxHooks.useCardCourseData(cardId); - const { isEnrolled, isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId); - const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId); - const { isMasquerading } = reduxHooks.useMasqueradeData(); - const handleTwitterShare = reduxHooks.useTrackCourseEvent( - track.socialShare, - cardId, - 'twitter', - ); - const handleFacebookShare = reduxHooks.useTrackCourseEvent( - track.socialShare, - cardId, - 'facebook', - ); - const emailSettingsModal = useEmailSettings(); const unenrollModal = useUnenrollData(); const handleToggleDropdown = useHandleToggleDropdown(cardId); + const { + courseName, + isMasquerading, + isEmailEnabled, + showUnenrollItem, + showDropdown, + facebook, + twitter, + handleTwitterShare, + handleFacebookShare, + } = useCourseCardMenu(cardId); + + if (!showDropdown) { + return null; + } + return ( <> @@ -52,7 +52,7 @@ export const CourseCardMenu = ({ cardId }) => { alt={formatMessage(messages.dropdownAlt)} /> - {isEnrolled && ( + {showUnenrollItem && ( ({ FacebookShareButton: () => 'FacebookShareButton', TwitterShareButton: () => 'TwitterShareButton', })); -jest.mock('hooks', () => ({ - reduxHooks: { - useCardCourseData: jest.fn(), - useCardEnrollmentData: jest.fn(), - useCardSocialSettingsData: jest.fn(), - useMasqueradeData: jest.fn(), - useTrackCourseEvent: (_, __, site) => jest.fn().mockName(`${site}ShareClick`), - }, -})); jest.mock('./hooks', () => ({ useEmailSettings: jest.fn(), useUnenrollData: jest.fn(), - useHandleToggleDropdown: jest.fn(), + useCourseCardMenu: jest.fn(), + useHandleToggleDropdown: () => jest.fn().mockName('mockHandleToggleDropdown'), })); const props = { @@ -48,77 +41,105 @@ const defaultSocialShare = { socialBrand: 'twitter-social-brand', }, }; -const courseName = 'test-course-name'; +const defaultUseCourseCardMenu = { + courseName: 'test-course-name', + isMasquerading: false, + isEmailEnabled: true, + showUnenrollItem: true, + showDropdown: true, + handleTwitterShare: jest.fn().mockName('handleTwitterShare'), + handleFacebookShare: jest.fn().mockName('handleFacebookShare'), +}; let wrapper; let el; describe('CourseCardMenu', () => { - beforeEach(() => { - useEmailSettings.mockReturnValue(defaultEmailSettingsModal); - useUnenrollData.mockReturnValue(defaultUnenrollModal); - reduxHooks.useCardSocialSettingsData.mockReturnValue(defaultSocialShare); - reduxHooks.useCardCourseData.mockReturnValue({ courseName }); - reduxHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: true, isEmailEnabled: true }); - reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false }); + useEmailSettings.mockReturnValue(defaultEmailSettingsModal); + useUnenrollData.mockReturnValue(defaultUnenrollModal); + + const mockUseCourseCardMenu = ({ + isMasquerading, + isEmailEnabled, + showUnenrollItem, + showDropdown, + facebook, + twitter, + }) => { + useCourseCardMenu.mockReturnValueOnce({ + ...defaultUseCourseCardMenu, + isMasquerading, + isEmailEnabled, + showUnenrollItem, + showDropdown, + facebook, + twitter, + }); + return shallow(); + }; + test('default snapshot', () => { + wrapper = mockUseCourseCardMenu({ + isMasquerading: false, + isEmailEnabled: true, + showUnenrollItem: true, + showDropdown: true, + ...defaultSocialShare, + }); + expect(wrapper).toMatchSnapshot(); }); - describe('enrolled, share enabled, email setting enable', () => { - beforeEach(() => { - wrapper = shallow(); - }); - test('snapshot', () => { - expect(wrapper).toMatchSnapshot(); - }); - it('renders share buttons', () => { - el = wrapper.find('FacebookShareButton'); - expect(el.length).toEqual(1); - expect(el.prop('url')).toEqual('facebook-share-url'); - el = wrapper.find('TwitterShareButton'); - expect(el.length).toEqual(1); - expect(el.prop('url')).toEqual('twitter-share-url'); - }); - it('renders enabled unenroll modal toggle', () => { - el = wrapper.find({ 'data-testid': 'unenrollModalToggle' }); - expect(el.props().disabled).toEqual(false); - }); - it('renders enabled email settings modal toggle', () => { - el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' }); - expect(el.props().disabled).toEqual(false); - }); - it('renders enabled email settings modal toggle', () => { - el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' }); - expect(el.props().disabled).toEqual(false); - }); + test('renders null if showDropdown is false', () => { + wrapper = mockUseCourseCardMenu({ + isMasquerading: true, + isEmailEnabled: true, + showUnenrollItem: true, + showDropdown: false, + ...defaultSocialShare, + }); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.isEmptyRender()).toEqual(true); }); - describe('not enrolled, share disabled, email setting disabled', () => { - beforeEach(() => { - reduxHooks.useCardSocialSettingsData.mockReturnValueOnce({ - ...defaultSocialShare, - twitter: { ...defaultSocialShare.twitter, isEnabled: false }, - facebook: { ...defaultSocialShare.facebook, isEnabled: false }, + + describe('disable state options', () => { + beforeAll(() => { + wrapper = mockUseCourseCardMenu({ + isMasquerading: false, + isEmailEnabled: false, + showUnenrollItem: false, + showDropdown: true, // set to true for testing + facebook: { + isEnabled: false, + }, + twitter: { + isEnabled: false, + }, }); - reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false, isEmailEnabled: false }); - wrapper = shallow(); }); - test('snapshot', () => { - expect(wrapper).toMatchSnapshot(); + // to make sure it try to render the dropdown + it('render dropdown base on showDropdown', () => { + expect(wrapper.isEmptyRender()).toEqual(false); + expect(wrapper.find('Dropdown').length).toEqual(1); }); - it('does not renders share buttons', () => { - expect(wrapper.find('FacebookShareButton').length).toEqual(0); - expect(wrapper.find('TwitterShareButton').length).toEqual(0); + it('not renders email settings modal toggle', () => { + el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' }); + expect(el.length).toEqual(0); }); - it('does not render unenroll modal toggle', () => { + it('not renders unenroll modal toggle', () => { el = wrapper.find({ 'data-testid': 'unenrollModalToggle' }); expect(el.length).toEqual(0); }); - it('does not render email settings modal toggle', () => { - el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' }); - expect(el.length).toEqual(0); + it('not renders share buttons', () => { + expect(wrapper.find('FacebookShareButton').length).toEqual(0); + expect(wrapper.find('TwitterShareButton').length).toEqual(0); }); }); describe('masquerading', () => { beforeEach(() => { - reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true }); - wrapper = shallow(); + wrapper = mockUseCourseCardMenu({ + isMasquerading: true, + isEmailEnabled: true, + showUnenrollItem: true, + showDropdown: true, + ...defaultSocialShare, + }); }); test('snapshot', () => { expect(wrapper).toMatchSnapshot(); diff --git a/src/data/redux/app/selectors/courseCard.js b/src/data/redux/app/selectors/courseCard.js index 01faacab..9d5ebf58 100644 --- a/src/data/redux/app/selectors/courseCard.js +++ b/src/data/redux/app/selectors/courseCard.js @@ -15,17 +15,14 @@ export const loadDateVal = (date) => (date ? new Date(date) : null); export const courseCard = StrictDict({ certificate: mkCardSelector( cardSimpleSelectors.certificate, - (certificate) => { - const availableDate = new Date(certificate.availableDate); - const isAvailable = availableDate <= new Date(); - return { - availableDate, - certPreviewUrl: baseAppUrl(certificate.certPreviewUrl), - isDownloadable: certificate.isDownloadable, - isEarnedButUnavailable: certificate.isEarned && !isAvailable, - isRestricted: certificate.isRestricted, - }; - }, + (certificate) => (certificate === null ? {} : ({ + availableDate: new Date(certificate.availableDate), + certPreviewUrl: baseAppUrl(certificate.certPreviewUrl), + isDownloadable: certificate.isDownloadable, + isEarnedButUnavailable: certificate.isEarned && new Date(certificate.availableDate) > new Date(), + isRestricted: certificate.isRestricted, + isEarned: certificate.isEarned, + })), ), course: mkCardSelector( cardSimpleSelectors.course, diff --git a/src/data/redux/app/selectors/courseCard.test.js b/src/data/redux/app/selectors/courseCard.test.js index 9331f855..f3ad11b3 100644 --- a/src/data/redux/app/selectors/courseCard.test.js +++ b/src/data/redux/app/selectors/courseCard.test.js @@ -79,6 +79,9 @@ describe('courseCard selectors module', () => { it('returns a card selector based on certificate cardSimpleSelector', () => { expect(simpleSelector).toEqual(cardSimpleSelectors.certificate); }); + it('returns {} object if null certificate received', () => { + expect(selector(null)).toEqual({}); + }); it('passes availableDate, converted to a date', () => { expect(selected.availableDate).toMatchObject(new Date(testData.availableDate)); }); @@ -162,6 +165,9 @@ describe('courseCard selectors module', () => { it('returns a card selector based on courseRun cardSimpleSelector', () => { expect(simpleSelector).toEqual(cardSimpleSelectors.courseRun); }); + it('returns {} object if null courseRun received', () => { + expect(selector(null)).toEqual({}); + }); it('passes [endDate, startDate], converted to dates', () => { expect(selected.endDate).toEqual(new Date(testData.endDate)); expect(selected.startDate).toEqual(new Date(testData.startDate));