From 57eac99b4281805e6ecb32c576058044d37db6db Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Mon, 24 Oct 2022 15:24:43 -0400 Subject: [PATCH] feat: add social actions + descriptive names to mock data (#41) --- package-lock.json | 70 +++++++++ package.json | 1 + src/App.jsx | 7 +- .../CourseCardBanners/CertificateBanner.jsx | 4 +- .../CertificateBanner.test.jsx | 18 +-- .../CourseCardDetails/hooks.test.js | 2 +- .../__snapshots__/index.test.jsx.snap | 114 ++++++++++++-- .../components/CourseCardMenu/hooks.js | 11 -- .../components/CourseCardMenu/index.jsx | 65 ++++++-- .../components/CourseCardMenu/index.test.jsx | 113 ++++++++++--- .../components/CourseCardMenu/messages.js | 36 +++++ .../__snapshots__/index.test.jsx.snap | 2 +- .../EnterpriseDashboardModal/messages.js | 2 +- src/data/redux/app/reducer.js | 1 + src/data/redux/app/selectors/courseCard.js | 1 + .../redux/app/selectors/courseCard.test.js | 6 +- .../redux/app/selectors/simpleSelectors.js | 1 + .../app/selectors/simpleSelectors.test.js | 1 + src/data/redux/hooks.js | 23 +++ src/data/services/lms/fakeData/courses.js | 148 ++++++++++++------ src/test/app.test.jsx | 66 +++++--- 21 files changed, 546 insertions(+), 146 deletions(-) create mode 100644 src/containers/CourseCard/components/CourseCardMenu/messages.js diff --git a/package-lock.json b/package-lock.json index 56a750f..4b571a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "react-router": "5.2.0", "react-router-dom": "5.2.0", "react-router-redux": "^5.0.0-alpha.9", + "react-share": "^4.4.0", "redux": "4.1.1", "redux-beacon": "^2.1.0", "redux-devtools-extension": "2.13.9", @@ -17637,6 +17638,27 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", + "integrity": "sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==", + "dependencies": { + "debug": "^2.1.3" + } + }, + "node_modules/jsonp/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/jsonp/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -24885,6 +24907,22 @@ "isarray": "0.0.1" } }, + "node_modules/react-share": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/react-share/-/react-share-4.4.0.tgz", + "integrity": "sha512-POe8Ge/JT9Ew9iyW7CiYsCCWCb8uMJWqFl9S7W0fJ/oH5gBJNzukH0bL5vSr17KKG5h15d3GfKaoviI22BKeYA==", + "dependencies": { + "classnames": "^2.2.5", + "jsonp": "^0.2.1" + }, + "engines": { + "node": ">=6.9.0", + "npm": ">=5.0.0" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17" + } + }, "node_modules/react-side-effect": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", @@ -43636,6 +43674,29 @@ "universalify": "^2.0.0" } }, + "jsonp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", + "integrity": "sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==", + "requires": { + "debug": "^2.1.3" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -48922,6 +48983,15 @@ } } }, + "react-share": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/react-share/-/react-share-4.4.0.tgz", + "integrity": "sha512-POe8Ge/JT9Ew9iyW7CiYsCCWCb8uMJWqFl9S7W0fJ/oH5gBJNzukH0bL5vSr17KKG5h15d3GfKaoviI22BKeYA==", + "requires": { + "classnames": "^2.2.5", + "jsonp": "^0.2.1" + } + }, "react-side-effect": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", diff --git a/package.json b/package.json index de05468..45f613e 100755 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "react-router": "5.2.0", "react-router-dom": "5.2.0", "react-router-redux": "^5.0.0-alpha.9", + "react-share": "^4.4.0", "redux": "4.1.1", "redux-beacon": "^2.1.0", "redux-devtools-extension": "2.13.9", diff --git a/src/App.jsx b/src/App.jsx index 2ea4fa5..f10ea78 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,7 +7,8 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { AppContext } from '@edx/frontend-platform/react'; import Footer from '@edx/frontend-component-footer'; -import { thunkActions } from 'data/redux'; +import store from 'data/store'; +import { selectors, actions, thunkActions } from 'data/redux'; import fakeData from 'data/services/lms/fakeData/courses'; import LearnerDashboardHeader from 'containers/LearnerDashboardHeader'; import Dashboard from 'containers/Dashboard'; @@ -34,6 +35,10 @@ export const App = () => { ], })); }; + window.store = store; + window.selectors = selectors; + window.actions = actions; + window.thunkActions = thunkActions; } }); return ( diff --git a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx index d07c3a6..215b9bf 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.jsx @@ -16,9 +16,9 @@ export const CertificateBanner = ({ cardId }) => { const { isAudit, isVerified, - hasFinished, } = appHooks.useCardEnrollmentData(cardId); const { isPassing } = appHooks.useCardGradeData(cardId); + const { isArchived } = appHooks.useCardCourseRunData(cardId); const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(cardId); const { supportEmail, billingEmail } = appHooks.usePlatformSettingsData(); const { formatMessage, formatDate } = useIntl(); @@ -45,7 +45,7 @@ export const CertificateBanner = ({ cardId }) => { ); } - if (hasFinished) { + if (isArchived) { return ( {formatMessage(messages.notEligibleForCert)}. diff --git a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx index b163196..d819a17 100644 --- a/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx +++ b/src/containers/CourseCard/components/CourseCardBanners/CertificateBanner.test.jsx @@ -7,9 +7,9 @@ import messages from './messages'; jest.mock('data/redux', () => ({ hooks: { useCardCertificateData: jest.fn(), + useCardCourseRunData: jest.fn(), useCardEnrollmentData: jest.fn(), useCardGradeData: jest.fn(), - useCardCourseRunData: jest.fn(), usePlatformSettingsData: jest.fn(), }, })); @@ -17,9 +17,7 @@ jest.mock('data/redux', () => ({ jest.mock('Components/Banner', () => 'Banner'); describe('CertificateBanner', () => { - const props = { - cardId: 'cardId', - }; + const props = { cardId: 'cardId' }; hooks.usePlatformSettingsData.mockReturnValue({ supportEmail: 'suport@email', billingEmail: 'billing@email', @@ -37,19 +35,19 @@ describe('CertificateBanner', () => { const defaultEnrollment = { isAudit: false, isVerified: false, - hasFinished: false, - }; - const defaultGrade = { - isPassing: false, }; + const defaultCourseRun = { isArchived: false }; + const defaultGrade = { isPassing: false }; const createWrapper = ({ certificate = {}, enrollment = {}, grade = {}, + courseRun = {}, }) => { hooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade }); hooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate }); hooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment }); + hooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun }); return shallow(); }; describe('snapshot', () => { @@ -82,9 +80,7 @@ describe('CertificateBanner', () => { }); test('not passing and has finished', () => { const wrapper = createWrapper({ - enrollment: { - hasFinished: true, - }, + courseRun: { isArchived: true }, }); expect(wrapper).toMatchSnapshot(); }); diff --git a/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js b/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js index 9041ba3..5c813bf 100644 --- a/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js +++ b/src/containers/CourseCard/components/CourseCardDetails/hooks.test.js @@ -81,7 +81,7 @@ describe('CourseCardDetails hooks', () => { }; const courseRunData = { isStarted: true, - isFinished: false, + isArchived: false, startDate: '10/10/1000', endDate: '10/20/2000', }; 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 6b4593a..aac6d5d 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/__snapshots__/index.test.jsx.snap +++ b/src/containers/CourseCard/components/CourseCardMenu/__snapshots__/index.test.jsx.snap @@ -4,7 +4,7 @@ exports[`CourseCardMenu snapshot 1`] = ` Unenroll - Email Settings + Email settings - - Share to Facebook - - - Share to Twitter + + + Share to Twitter + + + + + + + +`; + +exports[`CourseCardMenu snapshot: masquerading 1`] = ` + + + + + + Unenroll + + + Email settings + + + + Share to Twitter + + + + + + + +`; + +exports[`CourseCardMenu twitter share disabled 1`] = ` + + + + + + Unenroll + + + Email settings diff --git a/src/containers/CourseCard/components/CourseCardMenu/hooks.js b/src/containers/CourseCard/components/CourseCardMenu/hooks.js index bcbb012..0d03418 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/hooks.js +++ b/src/containers/CourseCard/components/CourseCardMenu/hooks.js @@ -24,14 +24,3 @@ export const useEmailSettings = () => { isVisible, }; }; - -export const useCardMenuData = () => { - const unenrollModal = module.useUnenrollData(); - const emailSettingsModal = module.useEmailSettings(); - return { - emailSettingsModal, - unenrollModal, - }; -}; - -export default useCardMenuData; diff --git a/src/containers/CourseCard/components/CourseCardMenu/index.jsx b/src/containers/CourseCard/components/CourseCardMenu/index.jsx index 4d9f03a..3261a27 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/index.jsx +++ b/src/containers/CourseCard/components/CourseCardMenu/index.jsx @@ -1,20 +1,29 @@ import React from 'react'; import PropTypes from 'prop-types'; +import * as ReactShare from 'react-share'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { Dropdown, Icon, IconButton } from '@edx/paragon'; import { MoreVert } from '@edx/paragon/icons'; import { hooks as appHooks } from 'data/redux'; import EmailSettingsModal from 'containers/EmailSettingsModal'; import UnenrollConfirmModal from 'containers/UnenrollConfirmModal'; -import useCourseCardMenuData from './hooks'; +import { useEmailSettings, useUnenrollData } from './hooks'; + +import messages from './messages'; export const CourseCardMenu = ({ cardId }) => { + const emailSettingsModal = useEmailSettings(); + const unenrollModal = useUnenrollData(); + const { courseName } = appHooks.useCardCourseData(cardId); const { - emailSettingsModal, - unenrollModal, - } = useCourseCardMenuData(); + // facebook, + twitter, + } = appHooks.useCardSocialSettingsData(cardId); const { isMasquerading } = appHooks.useMasqueradeData(); + const { formatMessage } = useIntl(); + return ( <> @@ -24,17 +33,51 @@ export const CourseCardMenu = ({ cardId }) => { src={MoreVert} iconAs={Icon} variant="primary" - alt="Actions dropdown" + alt={formatMessage(messages.dropdownAlt)} /> - - Unenroll + + {formatMessage(messages.unenroll)} - - Email Settings + + {formatMessage(messages.emailSettings)} - Share to Facebook - Share to Twitter + {/* Disabled pending PM decision on missing quote param in updated FB api. + {facebook.isEnabled && ( + + + {formatMessage(messages.shareToFacebook)} + + + )} + */} + {twitter.isEnabled && ( + + + {formatMessage(messages.shareToTwitter)} + + + )} jest.fn()); +jest.mock('react-share', () => ({ + FacebookShareButton: () => 'FacebookShareButton', + TwitterShareButton: () => 'TwitterShareButton', +})); +jest.mock('data/redux', () => ({ + hooks: { + useCardCourseData: jest.fn(), + useCardSocialSettingsData: jest.fn(), + useMasqueradeData: jest.fn(), + }, +})); +jest.mock('./hooks', () => ({ + useEmailSettings: jest.fn(), + useUnenrollData: jest.fn(), +})); + +const props = { + cardId: 'test-card-id', +}; +const defaultEmailSettingsModal = { + isVisible: false, + show: jest.fn().mockName('emailSettingShow'), + hide: jest.fn().mockName('emailSettingHide'), +}; +const defaultUnenrollModal = { + isVisible: false, + show: jest.fn().mockName('unenrollShow'), + hide: jest.fn().mockName('unenrollHide'), +}; +const defaultSocialShare = { + facebook: { + isEnabled: true, + shareUrl: 'facebook-share-url', + socialBrand: 'facebook-social-brand', + }, + twitter: { + isEnabled: true, + shareUrl: 'facebook-share-url', + socialBrand: 'facebook-social-brand', + }, +}; +const courseName = 'test-course-name'; +let wrapper; describe('CourseCardMenu', () => { - const props = { - cardId: 'test-card-id', - }; - const defaultEmailSettingsModal = { - isVisible: false, - show: jest.fn().mockName('emailSettingShow'), - hide: jest.fn().mockName('emailSettingHide'), - }; - const defaultUnenrollModal = { - isVisible: false, - show: jest.fn().mockName('unenrollShow'), - hide: jest.fn().mockName('unenrollHide'), - }; - + beforeEach(() => { + useEmailSettings.mockReturnValue(defaultEmailSettingsModal); + useUnenrollData.mockReturnValue(defaultUnenrollModal); + appHooks.useCardSocialSettingsData.mockReturnValue(defaultSocialShare); + appHooks.useCardCourseData.mockReturnValue({ courseName }); + appHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false }); + }); test('snapshot', () => { - useCourseCardMenuData.mockReturnValue({ - emailSettingsModal: defaultEmailSettingsModal, - unenrollModal: defaultUnenrollModal, - }); - const wrapper = shallow(); + wrapper = shallow(); expect(wrapper).toMatchSnapshot(); + // expect(wrapper.find('FacebookShareButton').length).toEqual(1); + expect(wrapper.find('TwitterShareButton').length).toEqual(1); + expect(wrapper.find({ + 'data-testid': 'unenrollModalToggle', + }).props().disabled).toEqual(false); + expect(wrapper.find({ + 'data-testid': 'emailSettingsModalToggle', + }).props().disabled).toEqual(false); + }); + test('snapshot: masquerading', () => { + appHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true }); + wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find({ + 'data-testid': 'unenrollModalToggle', + }).props().disabled).toEqual(true); + expect(wrapper.find({ + 'data-testid': 'emailSettingsModalToggle', + }).props().disabled).toEqual(true); + }); + /* + test('facebook share disabled', () => { + appHooks.useCardSocialSettingsData.mockReturnValueOnce({ + ...defaultSocialShare, + facebook: { ...defaultSocialShare.facebook, isEnabled: false }, + }); + wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('FacebookShareButton').length).toEqual(0); + }); + */ + test('twitter share disabled', () => { + appHooks.useCardSocialSettingsData.mockReturnValueOnce({ + ...defaultSocialShare, + twitter: { ...defaultSocialShare.twitter, isEnabled: false }, + }); + wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('TwitterShareButton').length).toEqual(0); }); }); diff --git a/src/containers/CourseCard/components/CourseCardMenu/messages.js b/src/containers/CourseCard/components/CourseCardMenu/messages.js new file mode 100644 index 0000000..023b38b --- /dev/null +++ b/src/containers/CourseCard/components/CourseCardMenu/messages.js @@ -0,0 +1,36 @@ +import { StrictDict } from 'utils'; + +export const messages = StrictDict({ + unenroll: { + id: 'learner-dash.courseCardMenu.unenroll', + description: 'Course unenroll menu button', + defaultMessage: 'Unenroll', + }, + dropdownAlt: { + id: 'learner-dash.courseCardMenu.dropdownAlt', + description: 'Course action menu alt-text', + defaultMessage: 'Course actions dropdown', + }, + emailSettings: { + id: 'learner-dash.courseCardMenu.emailSettings', + description: 'Course email settings menu button', + defaultMessage: 'Email settings', + }, + shareToFacebook: { + id: 'learner-dash.courseCardMenu.shareToFacebook', + description: 'Course Facebook Sharing button', + defaultMessage: 'Share to Facebook', + }, + shareToTwitter: { + id: 'learner-dash.courseCardMenu.shareToTwitter', + description: 'Course Twitter Sharing button', + defaultMessage: 'Share to Twitter', + }, + shareQuote: { + id: 'learner-dash.courseCardMenu.shareQuote', + description: 'Social sharing quote', + defaultMessage: 'I\'m taking {courseName} online with {socialBrand}. Check it out!', + }, +}); + +export default messages; diff --git a/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap b/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap index 068c3c3..fae0a28 100644 --- a/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap +++ b/src/containers/EnterpriseDashboardModal/__snapshots__/index.test.jsx.snap @@ -18,7 +18,7 @@ exports[`EnterpriseDashboard snapshot 1`] = ` You have access to the edX, Inc. dashboard

- To access the coureses available to you through edX, Inc., visit the edX, Inc. dashboard now. + To access the courses available to you through edX, Inc., visit the edX, Inc. dashboard now.