feat: add social actions + descriptive names to mock data (#41)

This commit is contained in:
Ben Warzeski
2022-10-24 15:24:43 -04:00
committed by GitHub
parent 7174f8de1e
commit 57eac99b42
21 changed files with 546 additions and 146 deletions

70
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 (

View File

@@ -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 }) => {
</Banner>
);
}
if (hasFinished) {
if (isArchived) {
return (
<Banner variant="warning">
{formatMessage(messages.notEligibleForCert)}.

View File

@@ -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(<CertificateBanner {...props} />);
};
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();
});

View File

@@ -81,7 +81,7 @@ describe('CourseCardDetails hooks', () => {
};
const courseRunData = {
isStarted: true,
isFinished: false,
isArchived: false,
startDate: '10/10/1000',
endDate: '10/20/2000',
};

View File

@@ -4,7 +4,7 @@ exports[`CourseCardMenu snapshot 1`] = `
<Fragment>
<Dropdown>
<Dropdown.Toggle
alt="Actions dropdown"
alt="Course actions dropdown"
as="IconButton"
iconAs="Icon"
id="course-actions-dropdown-test-card-id"
@@ -13,24 +13,116 @@ exports[`CourseCardMenu snapshot 1`] = `
/>
<Dropdown.Menu>
<Dropdown.Item
data-testid="unenrollModalToggle"
disabled={false}
onClick={[MockFunction unenrollShow]}
>
Unenroll
</Dropdown.Item>
<Dropdown.Item
data-testid="emailSettingsModalToggle"
disabled={false}
onClick={[MockFunction emailSettingShow]}
>
Email Settings
Email settings
</Dropdown.Item>
<Dropdown.Item
href="#/action-3"
>
Share to Facebook
</Dropdown.Item>
<Dropdown.Item
href="#/action-3"
>
Share to Twitter
<Dropdown.Item>
<TwitterShareButton
title="I'm taking test-course-name online with facebook-social-brand. Check it out!"
url="facebook-share-url"
>
Share to Twitter
</TwitterShareButton>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<UnenrollConfirmModal
cardId="test-card-id"
closeModal={[MockFunction unenrollHide]}
show={false}
/>
<EmailSettingsModal
cardId="test-card-id"
closeModal={[MockFunction emailSettingHide]}
show={false}
/>
</Fragment>
`;
exports[`CourseCardMenu snapshot: masquerading 1`] = `
<Fragment>
<Dropdown>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
iconAs="Icon"
id="course-actions-dropdown-test-card-id"
src={[MockFunction icons.MoreVert]}
variant="primary"
/>
<Dropdown.Menu>
<Dropdown.Item
data-testid="unenrollModalToggle"
disabled={true}
onClick={[MockFunction unenrollShow]}
>
Unenroll
</Dropdown.Item>
<Dropdown.Item
data-testid="emailSettingsModalToggle"
disabled={true}
onClick={[MockFunction emailSettingShow]}
>
Email settings
</Dropdown.Item>
<Dropdown.Item>
<TwitterShareButton
title="I'm taking test-course-name online with facebook-social-brand. Check it out!"
url="facebook-share-url"
>
Share to Twitter
</TwitterShareButton>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<UnenrollConfirmModal
cardId="test-card-id"
closeModal={[MockFunction unenrollHide]}
show={false}
/>
<EmailSettingsModal
cardId="test-card-id"
closeModal={[MockFunction emailSettingHide]}
show={false}
/>
</Fragment>
`;
exports[`CourseCardMenu twitter share disabled 1`] = `
<Fragment>
<Dropdown>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
iconAs="Icon"
id="course-actions-dropdown-test-card-id"
src={[MockFunction icons.MoreVert]}
variant="primary"
/>
<Dropdown.Menu>
<Dropdown.Item
data-testid="unenrollModalToggle"
disabled={false}
onClick={[MockFunction unenrollShow]}
>
Unenroll
</Dropdown.Item>
<Dropdown.Item
data-testid="emailSettingsModalToggle"
disabled={false}
onClick={[MockFunction emailSettingShow]}
>
Email settings
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>

View File

@@ -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;

View File

@@ -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 (
<>
<Dropdown>
@@ -24,17 +33,51 @@ export const CourseCardMenu = ({ cardId }) => {
src={MoreVert}
iconAs={Icon}
variant="primary"
alt="Actions dropdown"
alt={formatMessage(messages.dropdownAlt)}
/>
<Dropdown.Menu>
<Dropdown.Item disabled={isMasquerading} onClick={unenrollModal.show}>
Unenroll
<Dropdown.Item
disabled={isMasquerading}
onClick={unenrollModal.show}
data-testid="unenrollModalToggle"
>
{formatMessage(messages.unenroll)}
</Dropdown.Item>
<Dropdown.Item disabled={isMasquerading} onClick={emailSettingsModal.show}>
Email Settings
<Dropdown.Item
disabled={isMasquerading}
onClick={emailSettingsModal.show}
data-testid="emailSettingsModalToggle"
>
{formatMessage(messages.emailSettings)}
</Dropdown.Item>
<Dropdown.Item href="#/action-3">Share to Facebook</Dropdown.Item>
<Dropdown.Item href="#/action-3">Share to Twitter</Dropdown.Item>
{/* Disabled pending PM decision on missing quote param in updated FB api.
{facebook.isEnabled && (
<Dropdown.Item>
<ReactShare.FacebookShareButton
url={facebook.shareUrl}
quote={formatMessage(messages.shareQuote, {
courseName,
socialBrand: facebook.socialBrand,
})}
>
{formatMessage(messages.shareToFacebook)}
</ReactShare.FacebookShareButton>
</Dropdown.Item>
)}
*/}
{twitter.isEnabled && (
<Dropdown.Item>
<ReactShare.TwitterShareButton
url={twitter.shareUrl}
title={formatMessage(messages.shareQuote, {
courseName,
socialBrand: twitter.socialBrand,
})}
>
{formatMessage(messages.shareToTwitter)}
</ReactShare.TwitterShareButton>
</Dropdown.Item>
)}
</Dropdown.Menu>
</Dropdown>
<UnenrollConfirmModal

View File

@@ -1,31 +1,102 @@
import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import { useEmailSettings, useUnenrollData } from './hooks';
import CourseCardMenu from '.';
import useCourseCardMenuData from './hooks';
jest.mock('./hooks', () => 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(<CourseCardMenu {...props} />);
wrapper = shallow(<CourseCardMenu {...props} />);
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(<CourseCardMenu {...props} />);
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(<CourseCardMenu {...props} />);
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(<CourseCardMenu {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('TwitterShareButton').length).toEqual(0);
});
});

View File

@@ -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;

View File

@@ -18,7 +18,7 @@ exports[`EnterpriseDashboard snapshot 1`] = `
You have access to the edX, Inc. dashboard
</h4>
<p>
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.
</p>
<ActionRow>
<Button

View File

@@ -8,7 +8,7 @@ const messages = defineMessages({
},
enterpriseDialogBody: {
id: 'leanerDashboard.enterpriseDialogBody',
defaultMessage: 'To access the coureses available to you through {label}, visit the {label} dashboard now.',
defaultMessage: 'To access the courses available to you through {label}, visit the {label} dashboard now.',
description: 'Body text for enterpise dashboard dialog',
},
enterpriseDialogDismissButton: {

View File

@@ -42,6 +42,7 @@ const app = createSlice({
enterpriseDashboard: payload.enterpriseDashboard,
platformSettings: payload.platformSettings,
suggestedCourses: payload.suggestedCourses,
socialShareSettings: payload.socialShareSettings,
}),
updateSelectSessionModal: (state, { payload }) => ({
...state,

View File

@@ -35,6 +35,7 @@ export const courseCard = StrictDict({
courseNumber: course.courseNumber,
courseName: course.courseName,
website: course.website,
socialShareUrl: course.socialShareUrl,
}),
),
courseProvider: mkCardSelector(

View File

@@ -108,6 +108,7 @@ describe('courseCard selectors module', () => {
courseNumber: 'test-course-number',
courseName: 'test-course-name',
website: 'test-website',
socialShareUrl: 'test-social-share-url',
});
});
it('returns a card selector based on course cardSimpleSelector', () => {
@@ -116,8 +117,11 @@ describe('courseCard selectors module', () => {
it('passes bannerImgSrc, converted to a baseAppUrl', () => {
expect(selected.bannerImgSrc).toEqual(baseAppUrl(testData.bannerImgSrc));
});
it('passes [courseNumber, courseName, website]', () => {
it('passes [courseNumber, courseName, website, socialShareUrl]', () => {
expect(selected.courseNumber).toEqual(testData.courseNumber);
expect(selected.courseName).toEqual(testData.courseName);
expect(selected.website).toEqual(testData.website);
expect(selected.socialShareUrl).toEqual(testData.socialShareUrl);
});
});
describe('courseProvider selector', () => {

View File

@@ -15,6 +15,7 @@ export const simpleSelectors = StrictDict({
enterpriseDashboard: mkSimpleSelector(app => app.enterpriseDashboard),
selectSessionModal: mkSimpleSelector(app => app.selectSessionModal),
pageNumber: mkSimpleSelector(app => app.pageNumber),
socialShareSettings: mkSimpleSelector(app => app.socialShareSettings),
});
export const cardSimpleSelectors = StrictDict({

View File

@@ -28,6 +28,7 @@ describe('app simple selectors', () => {
keys.enterpriseDashboard,
keys.selectSessionModal,
keys.pageNumber,
keys.socialShareSettings,
])('%s app simple selector forwards corresponding data from app store', (key) => {
testState = { app: { [key]: testString, otherField: 'fake string' } };
const { preSelectors, cb } = simpleSelectors[key];

View File

@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
import { actions as appActions } from './app/reducer';
import appSelectors from './app/selectors';
import requestSelectors from './requests/selectors';
import * as module from './hooks';
const { courseCard } = appSelectors;
@@ -13,6 +14,7 @@ export const usePlatformSettingsData = () => useSelector(appSelectors.platformSe
// suggested courses is max at 3 at the moment.
export const useSuggestedCoursesData = () => useSelector(appSelectors.suggestedCourses).slice(0, 3);
export const useSelectSessionModalData = () => useSelector(appSelectors.selectSessionModal);
export const useSocialSettingsData = () => useSelector(appSelectors.socialSettingsData);
export const useHasCourses = () => useSelector(appSelectors.hasCourses);
export const useHasAvailableDashboards = () => useSelector(appSelectors.hasAvailableDashboards);
@@ -35,6 +37,27 @@ export const useCardGradeData = useCourseCardData(courseCard.gradeData);
export const useCardProviderData = useCourseCardData(courseCard.courseProvider);
export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPrograms);
export const useCardSocialSettingsData = (cardId) => {
const { socialShareUrl } = module.useCardCourseData(cardId);
const socialShareSettings = useSelector(appSelectors.socialShareSettings);
if (!socialShareSettings) {
return {
facebook: { isEnabled: false, shareUrl: '' },
twitter: { isEnabled: false, shareUrl: '' },
};
}
return {
facebook: {
isEnabled: socialShareSettings.facebook.isEnabled,
shareUrl: `${socialShareUrl}?${socialShareSettings.facebook.utmParams}`,
},
twitter: {
isEnabled: socialShareSettings.twitter.isEnabled,
shareUrl: `${socialShareUrl}?${socialShareSettings.twitter.utmParams}`,
},
};
};
export const useUpdateSelectSessionModalCallback = (dispatch, cardId) => () => dispatch(
appActions.updateSelectSessionModal(cardId),
);

View File

@@ -49,7 +49,7 @@ export const relatedPrograms = [
export const genCardId = (index) => `card-id${index}`;
export const genCourseId = (index) => `course-number${index}-course-id${index}`;
export const genCourseNumber = (index) => `course-number${index}`;
export const genCourseTitle = (index) => `Course Name ${index}`;
export const genCourseShareUrl = (index) => `home.edx.org?social-share-url/${index}`;
export const genEntitlementUUID = (index) => `entitlement-course-uuid-${index}`;
const bannerImgSrc = '/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg';
@@ -95,6 +95,18 @@ export const globalData = {
courseUrl: 'www.edx/suggested-course',
},
],
socialShareSettings: {
facebook: {
isEnabled: true,
socialBrand: 'edx.org',
utmParams: 'utm_campaign=social-sharing-db&utm_medium=social&utm_source=facebook',
},
twitter: {
isEnabled: true,
socialBrand: 'edx.org',
utmParams: 'utm_campaign=social-sharing-db&utm_medium=social&utm_source=twitter',
},
},
};
export const genCourseRunData = (data = {}) => ({
@@ -169,9 +181,12 @@ export const availableSessions = [
export const courseRuns = [
// audit, course run not started
{},
{
courseName: 'Audit Course, Course run not started',
},
// audit, course run not started, too early to view
{
courseName: 'Audit Course, Course run not started, Too early to view',
enrollment: {
coursewareAccess: {
isTooEarly: true,
@@ -182,6 +197,7 @@ export const courseRuns = [
},
// audit, course run not started, too early to view and unmet prereqs
{
courseName: 'Audit Course, Course run not started, Too early to view, Has unmet prereqs.',
enrollment: {
coursewareAccess: {
isTooEarly: true,
@@ -192,10 +208,12 @@ export const courseRuns = [
},
// audit, course run started
{
courseName: 'Audit Course, Course run not started',
courseRun: { isStarted: true },
},
// audit, course run started, unmet prereqs
{
courseName: 'Audit Course, Course run not started, Has unmet prereqs',
enrollment: {
coursewareAccess: {
isTooEarly: true,
@@ -207,6 +225,7 @@ export const courseRuns = [
},
// audit, course run started, access expired, learner not started
{
courseName: 'Audit Course, Course run started, Audit ccess expired, Learner not started',
courseRun: { isStarted: true },
enrollment: {
accessExpirationDate: pastDate,
@@ -215,6 +234,7 @@ export const courseRuns = [
},
// audit, course run started, access expired, cannot upgrade, learner not started
{
courseName: 'Audit course, Course run not started, Audit access expired, Cannot upgrade, Learner not started',
courseRun: { isStarted: true },
enrollment: {
accessExpirationDate: pastDate,
@@ -224,6 +244,7 @@ export const courseRuns = [
},
// audit, course run ended, access expired, cannot upgrade, learner not started
{
courseName: 'Audit Course, Course run ended, Audit access expired, Cannot upgrade, Learner not started',
courseRun: {
endDate: pastDate,
isStarted: true,
@@ -235,6 +256,7 @@ export const courseRuns = [
},
// audit, course run archived, access expired, cannot upgrade, learner not started
{
courseName: 'Audit Course, Course run archived, Audit access expired, Cannot upgrade, Learner not started',
courseRun: {
endDate: pastDate,
isArchived: true,
@@ -247,11 +269,13 @@ export const courseRuns = [
},
// audit, course run and learner started, passing
{
courseName: 'Audit Course, Course run and learner started, Passing',
courseRun: { isStarted: true },
enrollment: { hasStarted: true },
},
// audit, course run and learner started, access expired
{
courseName: 'Audit Course, Course run and learner started, Audit access expired',
courseRun: { isStarted: true },
enrollment: {
accessExpirationDate: pastDate,
@@ -261,6 +285,7 @@ export const courseRuns = [
},
// audit, course run and learner started, access expired, cannot upgrade
{
courseName: 'Audit Course, Course run and learner started, Audit access expired, Cannot upgrade',
courseRun: { isStarted: true },
enrollment: {
accessExpirationDate: pastDate,
@@ -271,6 +296,7 @@ export const courseRuns = [
},
// audit, course run ended, learner started, expired, cannot upgraded, not passing
{
courseName: 'Audit Course, Course run ended, Learner started, Access expired, Cannot upgrade, Not passing',
courseRun: {
isStarted: true,
endDate: pastDate,
@@ -285,6 +311,7 @@ export const courseRuns = [
},
// audit, course run archived, learner started, expired, cannot upgrade, not passing
{
courseName: 'Audit Course, Course run archived, Learner started, Access expired, Cannot upgrade, Not passing',
courseRun: {
isStarted: true,
isArchived: true,
@@ -299,25 +326,32 @@ export const courseRuns = [
grade: { isPassing: false },
},
// verified, course not started, learner not started
{ enrollment: { isVerified: true } },
{
courseName: 'Verified Course, Course and learner not started',
enrollment: { isVerified: true },
},
// verified, course started, learner not started
{
courseName: 'Verified Course, Course started, Learner not started',
courseRun: { isStarted: true },
enrollment: { isVerified: true },
},
// verified, course started, learner started, passing
{
courseName: 'Verified Course, Course and learner started, Passing',
courseRun: { isStarted: true },
enrollment: { hasStarted: true, isVerified: true },
},
// verified, course started, learner started, not passing
{
courseName: 'Verified Course, Course and learner started, not passing',
courseRun: { isStarted: true },
gradeData: { isPassing: false },
enrollment: { hasStarted: true, isVerified: true },
},
// verified, learner finished, not passing, cert not earned
{
courseName: 'Verified Course, Learner finished, cert not earned',
enrollment: {
hasFinished: true,
hasStarted: true,
@@ -331,6 +365,7 @@ export const courseRuns = [
},
// verified, learner finished, passing, cert earned but not available
{
courseName: 'Verified Course, Learner finished, Cert earned but not available',
enrollment: {
hasFinished: true,
hasStarted: true,
@@ -344,6 +379,7 @@ export const courseRuns = [
},
// verified, learner finished, passing, restricted
{
courseName: 'Verified Course, Learner finished, Passing, Certificate restricted',
enrollment: {
hasFinished: true,
hasStarted: true,
@@ -354,6 +390,7 @@ export const courseRuns = [
},
// verified, learner finished, cert earned, downloadable (web + link)
{
courseName: 'Verified Course, Learner finished, Passing, Certificate downloadable and viewable',
enrollment: {
hasFinished: true,
hasStarted: true,
@@ -369,6 +406,7 @@ export const courseRuns = [
},
// verified, course ended, learner finished, cert earned, downloadable (link only),
{
courseName: 'Verified Course, Course ended, Learner finished, Passing, Certificate downloadable',
enrollment: {
hasFinished: true,
hasStarted: true,
@@ -386,6 +424,7 @@ export const courseRuns = [
},
// verified, course archived, learner finished, cert earned, downloadable (web + link)
{
courseName: 'Verified Course, Course archived, Learner finished, Passing, Certificate downloadable and viewable',
enrollment: {
hasFinished: true,
hasStarted: true,
@@ -405,6 +444,7 @@ export const courseRuns = [
},
// Entitlement - not started
{
courseName: 'Entitlement Course, not started',
enrollment: {
isVerified: true,
coursewareAccess: {
@@ -426,6 +466,7 @@ export const courseRuns = [
},
// Entitlement - Course run started, learner not started, unmet prereqs
{
courseName: 'Entitlement Course, Course run started, Learner not started, Has unmet prereqs',
enrollment: {
isVerified: true,
coursewareAccess: {
@@ -444,6 +485,7 @@ export const courseRuns = [
},
// Entitlement - Course run started, learner started, not passing
{
courseName: 'Entitlement Course, Course run started, Learner started, Not passing',
enrollment: {
isVerified: true,
hasStarted: true,
@@ -462,6 +504,7 @@ export const courseRuns = [
},
// Entitlement - Course run started, learner started, passing, cannot change
{
courseName: 'Entitlement Course, Course run and learner started, Passing, Cannot change sessions',
enrollment: {
isVerified: true,
hasStarted: true,
@@ -479,6 +522,7 @@ export const courseRuns = [
},
// Entitlement - Learner finished, but did not pass
{
courseName: 'Entitlement Course, Learner finished but did not pass',
enrollment: {
isVerified: true,
hasFinished: false,
@@ -497,6 +541,7 @@ export const courseRuns = [
},
// Entitlement - Learner finished, and passed. cannot refund. previewable cert.
{
courseName: 'Entitlement course, Learner finished and passed, Cannot refund, Previewable Cert',
enrollment: {
isVerified: true,
hasFinished: false,
@@ -520,6 +565,7 @@ export const courseRuns = [
},
// Entitlement - Learner finished and failed. cannot refund. course ended.
{
courseName: 'Entitlement Course, Learner finished and failed, Cannot refund, Course ended',
enrollment: {
isVerified: true,
hasFinished: false,
@@ -541,6 +587,7 @@ export const courseRuns = [
},
// Entitlement - Learner finished and passed. cannot refund. course archived. cert downloadable
{
courseName: 'Entitlement Course, Learner finished and passed, Cannot refund, Course archived, Cert downloadable',
enrollment: {
isVerified: true,
hasFinished: false,
@@ -574,6 +621,7 @@ export const courseRuns = [
// unfulfilled entitlement select session pass deadline without available session
export const entitlementCourses = [
{
courseName: 'Unfulfilled Entitlement select session',
entitlement: {
uuid: genEntitlementUUID(10),
availableSessions,
@@ -584,6 +632,7 @@ export const entitlementCourses = [
isRefundable: true,
},
}, {
courseName: 'Unfulfilled Entitlement select session with upcoming deadline',
entitlement: {
uuid: genEntitlementUUID(11),
availableSessions,
@@ -594,6 +643,7 @@ export const entitlementCourses = [
isRefundable: true,
},
}, {
courseName: 'Unfulfilled Entitlement select session past deadline, With available session',
entitlement: {
uuid: genEntitlementUUID(12),
availableSessions,
@@ -604,6 +654,7 @@ export const entitlementCourses = [
isRefundable: true,
},
}, {
courseName: 'Unfulfilled Entitlement select session past deadline, With available no session',
entitlement: {
uuid: genEntitlementUUID(13),
availableSessions: [],
@@ -616,34 +667,33 @@ export const entitlementCourses = [
},
];
export const compileCourseRunData = (data, index) => {
const courseName = genCourseTitle(index);
const providerOptions = [
providers.edx,
providers.mit,
null,
];
const emailOptions = [
{ isEmailEnabled: false, hasOptedOutOfEmail: false },
{ isEmailEnabled: true, hasOptedOutOfEmail: false },
{ isEmailEnabled: true, hasOptedOutOfEmail: true },
];
const programsOptions = [
{ relatedPrograms },
{ relatedPrograms: [relatedPrograms[0]] },
{ relatedPrograms: [] },
];
const getOption = (options, index) => options[index % options.length];
export const compileCourseRunData = ({ courseName, ...data }, index) => {
const courseId = genCourseId(index);
const courseNumber = genCourseNumber(index);
const providerIndex = index % 3;
const socialShareUrl = genCourseShareUrl(index);
const lastEnrolledDate = new Date();
lastEnrolledDate.setDate(lastEnrolledDate.getDate() - index);
lastEnrolledDate.setDate(lastEnrolledDate.getDate() - index - 1);
const lastEnrolled = lastEnrolledDate.toISOString();
const iteratedData = [
{
course: { courseName, bannerImgSrc, courseNumber },
emailSettings: { isEmailEnabled: false, hasOptedOutOfEmail: false },
programs: { relatedPrograms },
courseProvider: providers.edx,
},
{
course: { courseName, bannerImgSrc, courseNumber },
emailSettings: { isEmailEnabled: true, hasOptedOutOfEmail: false },
courseProvider: providers.mit,
programs: { relatedPrograms: [relatedPrograms[0]] },
},
{
course: { courseName, bannerImgSrc, courseNumber },
emailSettings: { isEmailEnabled: true, hasOptedOutOfEmail: true },
courseProvider: null,
programs: { relatedPrograms: [] },
},
];
const out = {
gradeData: { isPassing: true },
entitlement: null,
@@ -652,12 +702,17 @@ export const compileCourseRunData = (data, index) => {
enrollment: genEnrollmentData({ lastEnrolled, ...data.enrollment }),
courseRun: genCourseRunData({
...data.courseRun,
...iteratedData.emailSettings,
...getOption(emailOptions, index),
courseId,
}),
courseProvider: iteratedData[providerIndex].courseProvider,
course: iteratedData[providerIndex].course,
programs: iteratedData[providerIndex].programs,
course: {
courseName,
bannerImgSrc,
courseNumber,
socialShareUrl,
},
courseProvider: getOption(providerOptions, index),
programs: getOption(programsOptions, index),
};
if (out.enrollment.canUpgrade) {
out.courseRun.upgradeUrl = 'test-upgrade-url';
@@ -665,27 +720,9 @@ export const compileCourseRunData = (data, index) => {
return out;
};
export const compileEntitlementData = (data, index) => {
const courseName = genCourseTitle(100 + index);
export const compileEntitlementData = ({ courseName, ...data }, index) => {
const courseNumber = genCourseNumber(100 + index);
const providerIndex = index % 3;
const iteratedData = [
{
courseProvider: providers.edx,
course: { courseNumber, courseName, bannerImgSrc },
programs: { relatedPrograms },
},
{
courseProvider: providers.mit,
course: { courseNumber, courseName, bannerImgSrc },
programs: { relatedPrograms: [relatedPrograms[0]] },
},
{
courseProvider: null,
course: { courseNumber, courseName, bannerImgSrc },
programs: { relatedPrograms: [] },
},
];
const socialShareUrl = genCourseShareUrl(100 + index);
return {
enrollment: genEnrollmentData({
isEnrolled: false,
@@ -703,7 +740,14 @@ export const compileEntitlementData = (data, index) => {
certificate: null,
courseRun: null,
...data,
...iteratedData[providerIndex],
course: {
courseName,
courseNumber,
bannerImgSrc,
socialShareUrl,
},
courseProvider: getOption(providerOptions, index),
programs: getOption(programsOptions, index),
};
};

View File

@@ -19,7 +19,7 @@ import * as fakeData from 'data/services/lms/fakeData/courses';
import { RequestKeys, RequestStates } from 'data/constants/requests';
import reducers from 'data/redux';
import messages from 'i18n';
import { selectors } from 'data/redux';
import { selectors, thunkActions } from 'data/redux';
import { cardId as genCardId } from 'data/redux/app/reducer';
import App from 'App';
@@ -134,46 +134,63 @@ const waitForRequestStatus = (key, status) => waitForEqual(
);
const loadApp = async (courses) => {
initCourses.mockReturnValue(courses);
initCourses.mockReturnValue(courses.map(compileCourseRunData));
await renderEl();
inspector = new Inspector(el);
await waitForRequestStatus(RequestKeys.initialize, RequestStates.pending);
resolveFns.init.success();
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
}
const courseNames = [
'course-name-1',
'course-name-2',
'course-name-3',
];
describe('ESG app integration tests', () => {
beforeEach(() => {
mockApi();
});
test('initialization', async () => {
await loadApp([compileCourseRunData({}, 0)]);
await waitForRequestStatus(RequestKeys.initialize, RequestStates.pending);
resolveFns.init.success();
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
await loadApp([{ courseName: courseNames[0] }]);
});
describe('course cards', () => {
const loadCourses = async (courses, tests) => {
await loadApp(courses);
resolveFns.init.success();
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
const courseNames = [
'course-name-0',
'course-name-1',
'course-name-2',
];
const testCourse = async (tests) => {
await getState();
const cards = inspector.get.courseCards;
courses.forEach((course, index) => {
const card = cards.at(index);
const cardId = genCardId(index);
const cardDetails = inspector.get.card.details(card);
const { courseName } = selectors.app.courseCard.course(state, cardId);
inspector.verifyText(inspector.get.card.header(card), courseName);
if (tests.length > index) {
tests[index]({ cardId, cardDetails });
}
});
const index = 0;
const card = cards.at(index);
const cardId = genCardId(index);
const cardDetails = inspector.get.card.details(card);
const courseData = selectors.app.courseCard.course(state, cardId);
const { courseName } = selectors.app.courseCard.course(state, cardId);
inspector.verifyText(inspector.get.card.header(card), courseName);
if (tests.length > index) {
tests[index]({ cardId, cardDetails });
}
}
const loadCourse = async (course) => {
initCourses.mockReturnValue([course].map(compileCourseRunData));
store.dispatch(thunkActions.app.initialize());
await waitForRequestStatus(RequestKeys.initialize, RequestStates.pending);
resolveFns.init.success();
await waitForRequestStatus(RequestKeys.initialize, RequestStates.completed);
};
test('audit', async () => {
const courses = [
{}, // audit, course run not started
{ courseName: courseNames[0] }, // audit, course run not started
{
courseName: courseNames[1],
enrollment: {
coursewareAccess: {
isTooEarly: true,
@@ -183,6 +200,7 @@ describe('ESG app integration tests', () => {
},
}, // audit, course run not started, is too early
{
courseName: courseNames[2],
courseRun: {
courseRun: { isStarted: true },
},
@@ -195,7 +213,8 @@ describe('ESG app integration tests', () => {
}, // audit, course run and learner started, access expired, cannot upgrade
];
const { formatDate } = useIntl();
await loadCourses(courses.map(compileCourseRunData), [
await loadApp([courses[0]]);
await testCourse([
({ cardId, cardDetails }) => {
const enrollment = selectors.app.courseCard.enrollment(state, cardId);
const courseRun = selectors.app.courseCard.courseRun(state, cardId);
@@ -212,6 +231,9 @@ describe('ESG app integration tests', () => {
}),
].forEach(value => inspector.verifyTextIncludes(cardDetails, value));
},
]);
await loadCourse(courses[1]);
await testCourse([
({ cardId, cardDetails }) => {
const enrollment = selectors.app.courseCard.enrollment(state, cardId);
const courseRun = selectors.app.courseCard.courseRun(state, cardId);