Compare commits

...

24 Commits

Author SHA1 Message Date
Simon Chen
cdb0f48f08 feat!: upsell through a course cert preview pop up
Even if the learner is enrolled in the course as a audit learner, each course cards would show a link that, once clicked upon, will pop a modal with verified cert preview and upgrade button
2023-10-25 13:37:58 -04:00
Syed Ali Abbas Zaidi
3cdcc1fe61 chore: bump frontend-platform (#216) 2023-10-16 13:09:55 +05:00
Shahbaz Shabbir
82ff0d7ddb fix: get brand logo file path from env (#205)
Co-authored-by: Brian Smith <112954497+brian-smith-tcril@users.noreply.github.com>
2023-10-12 14:06:01 -04:00
Jenkins
1478956e34 chore(i18n): update translations 2023-10-08 12:47:23 -04:00
Syed Ali Abbas Zaidi
f049712430 feat: upgrade react router to v6 (#126)
Co-authored-by: Matthew Carter <mcarter@edx.org>
2023-10-03 15:10:27 -04:00
Juliana Kang
c977de2df9 feat: Update header user dropdown back to Order History (#208)
REV-3693
2023-10-02 15:20:43 -04:00
Juliana Kang
4b20c5bbdd Revert "Revert "feat: Update header user dropdown back to Order History"" 2023-09-18 14:14:21 -04:00
Juliana Kang
0c1fa2f030 Revert "feat: Update header user dropdown back to Order History" (#207)
REV-3693
2023-09-18 14:13:47 -04:00
Juliana Kang
91117cce6a Revert "feat: Update header user dropdown back to Order History" 2023-09-18 13:52:13 -04:00
Juliana Kang
e6dba8bdc2 feat: Update header user dropdown back to Order History (#206)
REV-3693
2023-09-18 10:34:49 -04:00
julianajlk
1d67ac5f24 test: Update snapshot for Order History 2023-09-15 12:27:05 -04:00
julianajlk
60d2f22c50 feat: Update header user dropdown back to Order History 2023-09-15 12:12:07 -04:00
Syed Sajjad Hussain Shah
5dc89d7404 fix: collapsed navbar icon fix (#204)
Co-authored-by: Matthew Carter <mcarter@edx.org>
2023-09-07 17:11:15 -04:00
Zainab Amir
0f24d3a52d fix: recommendations card design and painted door eventing (#203) 2023-09-06 19:26:21 +05:00
Syed Sajjad Hussain Shah
fc885d02dc fix: recommendations card design and painted door eventing 2023-09-06 17:22:27 +05:00
Syed Sajjad Hussain Shah
2e09d3632e feat: add painted door button for no recommendations (#198) 2023-09-01 10:50:56 +05:00
Syed Sajjad Hussain Shah
d8cb46da60 Merge branch 'master' into sajjad/VAN-1618 2023-09-01 10:32:16 +05:00
Mashal Malik
199d6e7c60 feat: update react & react-dom to v17 (#161) 2023-08-28 10:31:57 -04:00
mubbsharanwar
64563d58f9 fix: update dashboard recommendations url
Point cross product recommendations url from learner_recommendations to edx-recommendations plugin.

VAN-1596
2023-08-28 11:50:41 +05:00
Jenkins
1e9a0a87b6 chore(i18n): update translations 2023-08-27 12:47:12 -04:00
Syed Sajjad Hussain Shah
d42d0cdc59 feat: add painted door button for no recommendations
VAN-1618
2023-08-24 13:30:39 +05:00
Mubbshar Anwar
8fef92d94d fix: update dashboard recommendations url (#195)
Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
Co-authored-by: Matthew Carter <mcarter@edx.org>
2023-08-24 11:12:39 +05:00
Ben Warzeski
b41eee47c9 Bw/recommendations painted door exp (#197)
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2023-08-23 11:53:19 -04:00
Ben Warzeski
909f3f1f47 Bw/fix email modal (#193) 2023-08-22 15:12:54 -04:00
77 changed files with 5116 additions and 3934 deletions

1
.env
View File

@@ -41,3 +41,4 @@ ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
EXPERIMENT_08_23_VAN_PAINTED_DOOR='true'

View File

@@ -48,3 +48,4 @@ ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true

View File

@@ -47,3 +47,4 @@ ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY='SDK Key'
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true

7153
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,10 +28,10 @@
"dependencies": {
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-component-footer": "^12.0.0",
"@edx/frontend-enterprise-hotjar": "^1.2.0",
"@edx/frontend-platform": "^4.2.0",
"@edx/paragon": "^20.32.0",
"@edx/frontend-component-footer": "^12.2.1",
"@edx/frontend-enterprise-hotjar": "^2.0.0",
"@edx/frontend-platform": "^5.5.4",
"@edx/paragon": "^20.44.0",
"@edx/react-unit-test-utils": "^1.7.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
@@ -59,13 +59,13 @@
"moment": "^2.29.4",
"prop-types": "15.7.2",
"query-string": "7.0.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-intl": "^5.20.9",
"react-pdf": "^5.5.0",
"react-redux": "^7.2.4",
"react-router-dom": "5.3.3",
"react-router-dom": "6.15.0",
"react-share": "^4.4.0",
"react-zendesk": "^0.1.13",
"redux": "4.1.1",
@@ -84,15 +84,15 @@
"@edx/reactifex": "^2.1.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"axios-mock-adapter": "^1.20.0",
"copy-webpack-plugin": "^11.0.0",
"enzyme-adapter-react-16": "^1.15.6",
"fetch-mock": "^9.11.0",
"husky": "^7.0.0",
"identity-obj-proxy": "^3.0.0",
"jest-expect-message": "^1.0.2",
"react-dev-utils": "^11.0.4",
"react-test-renderer": "^16.14.0",
"react-test-renderer": "^17.0.2",
"redux-mock-store": "^1.5.4",
"semantic-release": "^20.1.3"
}

View File

@@ -5,6 +5,15 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
<% if (process.env.OPTIMIZELY_URL) { %>
<script
src="<%= process.env.OPTIMIZELY_URL %>"
></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
></script>
<% } %>
</head>
<body>
<div id="root"></div>

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -24,10 +23,11 @@ import { ExperimentProvider } from 'ExperimentContext';
import track from 'tracking';
import fakeData from 'data/services/lms/fakeData/courses';
import LearnerDashboardHeader from './containers/LearnerDashboardHeader';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import messages from './messages';
import './App.scss';
export const App = () => {
@@ -73,28 +73,30 @@ export const App = () => {
}
}, [authenticatedUser, loadData]);
return (
<Router>
<>
<Helmet>
<title>{formatMessage(messages.pageTitle)}</title>
</Helmet>
<div>
<LearnerDashboardHeader />
<main>
{hasNetworkFailure
? (
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
)}
</main>
<AppWrapper>
<LearnerDashboardHeader />
<main>
{hasNetworkFailure
? (
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
)}
</main>
</AppWrapper>
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
<ZendeskFab />
</div>
</Router>
</>
);
};

View File

@@ -1,18 +1,15 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Helmet } from 'react-helmet';
import { ErrorPage } from '@edx/frontend-platform/react';
import { BrowserRouter as Router } from 'react-router-dom';
import { shallow } from '@edx/react-unit-test-utils';
import Footer from '@edx/frontend-component-footer';
import { useIntl } from '@edx/frontend-platform/i18n';
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 AppWrapper from 'containers/WidgetContainers/AppWrapper';
import { ExperimentProvider } from 'ExperimentContext';
import { App } from './App';
import messages from './messages';
@@ -25,6 +22,7 @@ jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('ExperimentContext', () => ({
ExperimentProvider: 'ExperimentProvider',
}));
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
actions: 'redux.actions',
@@ -53,18 +51,23 @@ describe('App router component', () => {
const { formatMessage } = useIntl();
describe('component', () => {
const runBasicTests = () => {
test('snapshot', () => { expect(el).toMatchSnapshot(); });
test('snapshot', () => { expect(el.snapshot).toMatchSnapshot(); });
it('displays title in helmet component', () => {
expect(el.find(Helmet).find('title').text()).toEqual(useIntl().formatMessage(messages.pageTitle));
const control = el.instance
.findByType(Helmet)[0]
.findByType('title')[0];
expect(control.children[0].el).toEqual(formatMessage(messages.pageTitle));
});
it('displays learner dashboard header', () => {
expect(el.find(LearnerDashboardHeader).length).toEqual(1);
});
it('wraps the page in a browser router', () => {
expect(el.find(Router)).toMatchObject(el);
expect(el.instance.findByType(LearnerDashboardHeader).length).toEqual(1);
});
test('Footer logo drawn from env variable', () => {
expect(el.find(Footer).props().logo).toEqual(logo);
expect(el.instance.findByType(Footer)[0].props.logo).toEqual(logo);
});
it('wraps the header and main components in an AppWrapper widget container', () => {
const container = el.instance.findByType(AppWrapper)[0];
expect(container.children[0].type).toEqual('LearnerDashboardHeader');
expect(container.children[1].type).toEqual('main');
});
};
describe('no network failure', () => {
@@ -74,9 +77,14 @@ describe('App router component', () => {
});
runBasicTests();
it('loads dashboard', () => {
expect(el.find('main')).toMatchObject(shallow(
<main><ExperimentProvider><Dashboard /></ExperimentProvider></main>,
));
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
).toEqual(true);
});
});
describe('initialize failure', () => {
@@ -86,13 +94,14 @@ describe('App router component', () => {
});
runBasicTests();
it('loads error page', () => {
expect(el.find('main')).toEqual(shallow(
<main>
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
</main>,
));
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const alert = main.children[0];
expect(alert.type).toEqual('Alert');
expect(alert.children.length).toEqual(1);
const errorPage = alert.children[0];
expect(errorPage.type).toEqual('ErrorPage');
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
});
});
describe('refresh failure', () => {
@@ -102,13 +111,14 @@ describe('App router component', () => {
});
runBasicTests();
it('loads error page', () => {
expect(el.find('main')).toEqual(shallow(
<main>
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
</main>,
));
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const alert = main.children[0];
expect(alert.type).toEqual('Alert');
expect(alert.children.length).toEqual(1);
const errorPage = alert.children[0];
expect(errorPage.type).toEqual('ErrorPage');
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
});
});
});

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App router component component initialize failure snapshot 1`] = `
<BrowserRouter>
<Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -11,26 +11,28 @@ exports[`App router component component initialize failure snapshot 1`] = `
</title>
</HelmetWrapper>
<div>
<LearnerDashboardHeader />
<main>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
</div>
</BrowserRouter>
</Fragment>
`;
exports[`App router component component no network failure snapshot 1`] = `
<BrowserRouter>
<Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -40,22 +42,24 @@ exports[`App router component component no network failure snapshot 1`] = `
</title>
</HelmetWrapper>
<div>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</main>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
</div>
</BrowserRouter>
</Fragment>
`;
exports[`App router component component refresh failure snapshot 1`] = `
<BrowserRouter>
<Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -65,20 +69,22 @@ exports[`App router component component refresh failure snapshot 1`] = `
</title>
</HelmetWrapper>
<div>
<LearnerDashboardHeader />
<main>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
</div>
</BrowserRouter>
</Fragment>
`;

View File

@@ -13,18 +13,28 @@ exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
"redux": "store",
}
}
wrapWithRouter={true}
>
<NoticesWrapper>
<Switch>
<PageRoute
<Routes>
<Route
element={
<PageWrap>
<App />
</PageWrap>
}
path="/"
>
<App />
</PageRoute>
<Redirect
to="/"
/>
</Switch>
<Route
element={
<Navigate
replace={true}
to="/"
/>
}
path="*"
/>
</Routes>
</NoticesWrapper>
</AppProvider>
`;

View File

@@ -16,6 +16,7 @@ const configuration = {
SUPPORT_URL: process.env.SUPPORT_URL || null,
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
LOGO_URL: process.env.LOGO_URL,
};
const features = {};

View File

@@ -0,0 +1,29 @@
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import messages from './messages';
export const useCertificatePreviewData = () => {
const { formatMessage } = useIntl();
const selectedCardId = reduxHooks.useCertificatePreviewData().cardId;
const { courseId } = reduxHooks.useCardCourseRunData(selectedCardId) || {};
const courseTitle = courseId;
const header = formatMessage(messages.previewTitle, { courseTitle });
const closePreviewModal = reduxHooks.useUpdateCertificatePreviewModalCallback(null);
const getCertificatePreviewUrl = () => `${getConfig().LMS_BASE_URL}/certificates/upsell/course/${courseId}`;
return {
showModal: selectedCardId != null,
header,
closePreviewModal,
getCertificatePreviewUrl,
};
};
export default useCertificatePreviewData;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
ModalDialog,
} from '@edx/paragon';
import { UpgradeButton } from '../CourseCard/components/CourseCardActions/UpgradeButton';
import useCertificatePreviewData from './hooks';
export const CertificatePreviewModal = ({
cardId,
}) => {
const {
showModal,
header,
closePreviewModal,
getCertificatePreviewUrl,
} = useCertificatePreviewData();
return (
<ModalDialog
isOpen={showModal}
onClose={closePreviewModal}
hasCloseButton
isFullscreenOnMobile
size="lg"
className="p-4 px-4.5"
title={header}
>
<h3>{header}</h3>
<div>
<iframe
title={header}
src={getCertificatePreviewUrl()}
width={725}
height={400}
/>
</div>
<UpgradeButton
cardId={cardId}
/>
</ModalDialog>
);
};
CertificatePreviewModal.propTypes = {
cardId: PropTypes.string.isRequired,
};
export default CertificatePreviewModal;

View File

@@ -0,0 +1,12 @@
/* eslint-disable quotes */
import { StrictDict } from 'utils';
export const messages = StrictDict({
previewTitle: {
id: 'learner-dash.certificatePreview.title',
description: 'The title of the email settings modal',
defaultMessage: 'Your certificate preview for {courseTitle} ',
},
});
export default messages;

View File

@@ -47,6 +47,7 @@ export const useCardDetailsData = ({ cardId }) => {
} = reduxHooks.useCardEntitlementData(cardId);
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
const openCertificatePreview = reduxHooks.useUpdateCertificatePreviewModalCallback(cardId);
return {
providerName: providerName || formatMessage(messages.unknownProviderName),
@@ -57,6 +58,7 @@ export const useCardDetailsData = ({ cardId }) => {
openSessionModal,
courseNumber,
changeOrLeaveSessionMessage: formatMessage(messages.changeOrLeaveSessionButton),
openCertificatePreview,
};
};

View File

@@ -16,23 +16,35 @@ export const CourseCardDetails = ({ cardId }) => {
openSessionModal,
courseNumber,
changeOrLeaveSessionMessage,
openCertificatePreview,
} = useCardDetailsData({ cardId });
return (
<span className="small" data-testid="CourseCardDetails">
{providerName} {courseNumber}
{!(isEntitlement && !isFulfilled) && accessMessage && (
`${accessMessage}`
)}
{isEntitlement && isFulfilled && canChange ? (
<>
{' • '}
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
{changeOrLeaveSessionMessage}
</Button>
</>
) : null}
</span>
<div>
<span className="small" data-testid="CourseCardDetails">
{providerName} {courseNumber}
{!(isEntitlement && !isFulfilled) && accessMessage && (
`${accessMessage}`
)}
{isEntitlement && isFulfilled && canChange ? (
<>
{' • '}
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
{changeOrLeaveSessionMessage}
</Button>
</>
) : null}
</span>
<Button
variant="link"
size="inline"
className="float-right"
data-testid="certificate-preview"
onClick={openCertificatePreview}
>
Preview Your Certificate
</Button>
</div>
);
};

View File

@@ -8,7 +8,6 @@ import { Dropdown } from '@edx/paragon';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import { useEmailSettings } from './hooks';
import messages from './messages';
@@ -16,11 +15,9 @@ export const testIds = StrictDict({
emailSettingsModalToggle: 'emailSettingsModalToggle',
});
export const SocialShareMenu = ({ cardId }) => {
export const SocialShareMenu = ({ cardId, emailSettings }) => {
const { formatMessage } = useIntl();
const emailSettingsModal = useEmailSettings();
const { courseName } = reduxHooks.useCardCourseData(cardId);
const { isEmailEnabled, isExecEd2UCourse } = reduxHooks.useCardEnrollmentData(cardId);
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
@@ -38,7 +35,7 @@ export const SocialShareMenu = ({ cardId }) => {
{isEmailEnabled && (
<Dropdown.Item
disabled={isMasquerading}
onClick={emailSettingsModal.show}
onClick={emailSettings.show}
data-testid={testIds.emailSettingsModalToggle}
>
{formatMessage(messages.emailSettings)}
@@ -77,6 +74,9 @@ export const SocialShareMenu = ({ cardId }) => {
};
SocialShareMenu.propTypes = {
cardId: PropTypes.string.isRequired,
emailSettings: PropTypes.shape({
show: PropTypes.func,
}).isRequired,
};
export default SocialShareMenu;

View File

@@ -39,14 +39,11 @@ jest.mock('./hooks', () => ({
useEmailSettings: jest.fn(),
}));
const emailSettings = {
isVisible: false,
show: jest.fn().mockName('emailSettingShow'),
hide: jest.fn().mockName('emailSettingHide'),
const props = {
cardId: 'test-card-id',
emailSettings: { show: jest.fn() },
};
const props = { cardId: 'test-card-id' };
const mockHook = (fn, returnValue, options = {}) => {
if (options.isCardHook) {
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue);
@@ -71,7 +68,6 @@ const socialShare = {
};
const mockHooks = (returnVals = {}) => {
mockHook(useEmailSettings, emailSettings);
mockHook(
reduxHooks.useCardEnrollmentData,
{
@@ -140,7 +136,7 @@ describe('SocialShareMenu', () => {
});
}
test('show email settings modal on click', () => {
expect(loadToggle().props.onClick).toEqual(emailSettings.show);
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
});
});
};
@@ -188,7 +184,7 @@ describe('SocialShareMenu', () => {
expect(loadToggle().props.disabled).toEqual(false);
});
test('show email settings modal on click', () => {
expect(loadToggle().props.onClick).toEqual(emailSettings.show);
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
});
});
testEmailSettingsDropdown();

View File

@@ -16,6 +16,13 @@ exports[`CourseCardMenu render show dropdown hide unenroll item and disable emai
<Dropdown.Menu>
<SocialShareMenu
cardId="test-card-id"
emailSettings={
Object {
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
}
}
/>
</Dropdown.Menu>
</Dropdown>
@@ -50,6 +57,13 @@ exports[`CourseCardMenu render show dropdown show unenroll and enable email snap
</Dropdown.Item>
<SocialShareMenu
cardId="test-card-id"
emailSettings={
Object {
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
}
}
/>
</Dropdown.Menu>
</Dropdown>

View File

@@ -7,6 +7,7 @@ import { MoreVert } from '@edx/paragon/icons';
import { StrictDict } from '@edx/react-unit-test-utils';
import EmailSettingsModal from 'containers/EmailSettingsModal';
import CertificatePreviewModal from 'containers/CertificatePreviewModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import { reduxHooks } from 'hooks';
import SocialShareMenu from './SocialShareMenu';
@@ -26,12 +27,13 @@ export const testIds = StrictDict({
export const CourseCardMenu = ({ cardId }) => {
const { formatMessage } = useIntl();
const emailSettingsModal = useEmailSettings();
const emailSettings = useEmailSettings();
const unenrollModal = useUnenrollData();
const handleToggleDropdown = useHandleToggleDropdown(cardId);
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
const showCertificatePreviewModal = reduxHooks.useShowCertificatePreviewModal(cardId);
if (!shouldShowDropdown) {
return null;
@@ -58,7 +60,7 @@ export const CourseCardMenu = ({ cardId }) => {
{formatMessage(messages.unenroll)}
</Dropdown.Item>
)}
<SocialShareMenu cardId={cardId} />
<SocialShareMenu cardId={cardId} emailSettings={emailSettings} />
</Dropdown.Menu>
</Dropdown>
<UnenrollConfirmModal
@@ -68,8 +70,13 @@ export const CourseCardMenu = ({ cardId }) => {
/>
{isEmailEnabled && (
<EmailSettingsModal
show={emailSettingsModal.isVisible}
closeModal={emailSettingsModal.hide}
show={emailSettings.isVisible}
closeModal={emailSettings.hide}
cardId={cardId}
/>
)}
{showCertificatePreviewModal && (
<CertificatePreviewModal
cardId={cardId}
/>
)}

View File

@@ -7,6 +7,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import EmailSettingsModal from 'containers/EmailSettingsModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import { reduxHooks } from 'hooks';
import SocialShareMenu from './SocialShareMenu';
import * as hooks from './hooks';
import CourseCardMenu, { testIds } from '.';
@@ -18,6 +19,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
jest.mock('hooks', () => ({
reduxHooks: { useMasqueradeData: jest.fn(), useCardEnrollmentData: jest.fn() },
}));
jest.mock('./SocialShareMenu', () => 'SocialShareMenu');
jest.mock('./hooks', () => ({
useEmailSettings: jest.fn(),
useUnenrollData: jest.fn(),
@@ -122,6 +124,13 @@ describe('CourseCardMenu', () => {
expect(modal.props.cardId).toEqual(props.cardId);
});
};
const testSocialShareMenu = () => {
it('displays SocialShareMenu with cardID and emailSettings', () => {
const menu = el.instance.findByType(SocialShareMenu)[0];
expect(menu.props.cardId).toEqual(props.cardId);
expect(menu.props.emailSettings).toEqual(emailSettings);
});
};
describe('show dropdown', () => {
describe('hide unenroll item and disable email', () => {
beforeEach(() => {
@@ -132,6 +141,7 @@ describe('CourseCardMenu', () => {
expect(el.snapshot).toMatchSnapshot();
});
testHandleToggle();
testSocialShareMenu();
it('does not render unenroll modal toggle', () => {
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(0);
});
@@ -154,6 +164,7 @@ describe('CourseCardMenu', () => {
expect(el.snapshot).toMatchSnapshot();
});
testHandleToggle();
testSocialShareMenu();
describe('unenroll modal toggle', () => {
let toggle;
describe('not masquerading', () => {

View File

@@ -3,6 +3,7 @@ import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import { configuration } from '../../config';
import messages from './messages';
@@ -14,7 +15,7 @@ export const BrandLogo = () => {
<a href={dashboard?.url || '/'} className="mx-auto">
<img
className="logo py-3"
src="https://edx-cdn.org/v3/prod/logo.svg"
src={configuration.LOGO_URL}
alt={formatMessage(messages.logoAltText)}
/>
</a>

View File

@@ -4,14 +4,14 @@ import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Button, Badge } from '@edx/paragon';
import WidgetNavbar from 'containers/WidgetContainers/WidgetNavbar';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import { COLLAPSED_NAVBAR } from 'widgets/RecommendationsPaintedDoorBtn/constants';
import { findCoursesNavDropdownClicked } from '../hooks';
import messages from '../messages';
export const CollapseMenuBody = ({ isOpen }) => {
@@ -40,6 +40,7 @@ export const CollapseMenuBody = ({ isOpen }) => {
>
{formatMessage(messages.discoverNew)}
</Button>
<WidgetNavbar placement={COLLAPSED_NAVBAR} />
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
{formatMessage(messages.help)}
</Button>
@@ -80,7 +81,7 @@ export const CollapseMenuBody = ({ isOpen }) => {
variant="inverse-primary"
href={getConfig().ORDER_HISTORY_URL}
>
{formatMessage(messages.ordersAndSubscriptions)}
{formatMessage(messages.orderHistory)}
</Button>
)}
<Button

View File

@@ -26,6 +26,9 @@ exports[`CollapseMenuBody render 1`] = `
>
Discover New
</Button>
<WidgetNavbar
placement="collapsedNavbar"
/>
<Button
as="a"
href="http://localhost:18000/support"
@@ -92,6 +95,9 @@ exports[`CollapseMenuBody render unauthenticated 1`] = `
>
Discover New
</Button>
<WidgetNavbar
placement="collapsedNavbar"
/>
<Button
as="a"
href="http://localhost:18000/support"

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Menu, Close } from '@edx/paragon/icons';
import { MenuIcon, Close } from '@edx/paragon/icons';
import { IconButton, Icon } from '@edx/paragon';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
@@ -23,7 +23,7 @@ export const CollapsedHeader = () => {
<IconButton
invertColors
isActive
src={isOpen ? Close : Menu}
src={isOpen ? Close : MenuIcon}
iconAs={Icon}
alt={
isOpen

View File

@@ -55,7 +55,7 @@ export const AuthenticatedUserDropdown = () => {
</Dropdown.Item>
{getConfig().ORDER_HISTORY_URL && (
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
{formatMessage(messages.ordersAndSubscriptions)}
{formatMessage(messages.orderHistory)}
</Dropdown.Item>
)}
<Dropdown.Divider />

View File

@@ -55,7 +55,7 @@ exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
<Dropdown.Item
href="http://order-history-url.test"
>
Orders & Subscriptions
Order History
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
@@ -122,7 +122,7 @@ exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and ex
<Dropdown.Item
href="http://order-history-url.test"
>
Orders & Subscriptions
Order History
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item

View File

@@ -33,6 +33,9 @@ exports[`ExpandedHeader render 1`] = `
>
Discover New
</Button>
<WidgetNavbar
placement="expendedNavbar"
/>
<span
className="flex-grow-1"
/>

View File

@@ -4,11 +4,12 @@ import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import WidgetNavbar from 'containers/WidgetContainers/WidgetNavbar';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import { EXPANDED_NAVBAR } from 'widgets/RecommendationsPaintedDoorBtn/constants';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
import messages from '../messages';
import BrandLogo from '../BrandLogo';
@@ -51,6 +52,7 @@ export const ExpandedHeader = () => {
>
{formatMessage(messages.discoverNew)}
</Button>
<WidgetNavbar placement={EXPANDED_NAVBAR} />
<span className="flex-grow-1" />
<Button
as="a"

View File

@@ -8,7 +8,7 @@ exports[`BrandLogo dashboard defined 1`] = `
<img
alt="edX, Inc. Dashboard"
className="logo py-3"
src="https://edx-cdn.org/v3/prod/logo.svg"
src="https://edx-cdn.org/v3/default/logo.svg"
/>
</a>
`;
@@ -21,7 +21,7 @@ exports[`BrandLogo dashboard undefined 1`] = `
<img
alt="edX, Inc. Dashboard"
className="logo py-3"
src="https://edx-cdn.org/v3/prod/logo.svg"
src="https://edx-cdn.org/v3/default/logo.svg"
/>
</a>
`;

View File

@@ -26,10 +26,10 @@ const messages = defineMessages({
defaultMessage: 'Account',
description: 'The text for the user menu Account navigation link.',
},
ordersAndSubscriptions: {
id: 'learnerVariantDashboard.menu.ordersAndSubscriptions.label',
defaultMessage: 'Orders & Subscriptions',
description: 'The text for the user menu Orders & Subscriptions 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',

View File

@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AppWrapper WidgetContainer component output no experiments are active snapshot 1`] = `
<div>
This is some
<b>
test
</b>
<i>
content
</i>
</div>
`;
exports[`AppWrapper WidgetContainer component output painted door experiment is active (08/23) snapshot 1`] = `
<PaintedDoorExperimentProvider>
<div>
This is some
<b>
test
</b>
<i>
content
</i>
</div>
</PaintedDoorExperimentProvider>
`;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import PaintedDoorExperimentProvider from 'widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
export const AppWrapper = ({
children,
}) => {
console.log(`process.env.EXPERIMENT_08_23_VAN_PAINTED_DOOR = ${Boolean(process.env.EXPERIMENT_08_23_VAN_PAINTED_DOOR)}`);
return (
<PaintedDoorExperimentProvider>
{children}
</PaintedDoorExperimentProvider>
);
};
AppWrapper.propTypes = {
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]).isRequired,
};
export default AppWrapper;

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import PaintedDoorExperimentProvider from 'widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
import AppWrapper from '.';
jest.mock(
'widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext',
() => 'PaintedDoorExperimentProvider',
);
let el;
const children = (<div>This is some <b>test</b> <i>content</i></div>);
const render = () => {
el = shallow(<AppWrapper>{children}</AppWrapper>);
};
const mockAndRenderForBlock = (newVal) => {
const oldVal = process.env;
beforeEach(() => {
process.env = { ...oldVal, ...newVal };
render();
});
afterEach(() => {
process.env = oldVal;
render();
});
};
describe('AppWrapper WidgetContainer component', () => {
describe('output', () => {
describe('painted door experiment is active (08/23)', () => {
mockAndRenderForBlock({ EXPERIMENT_08_23_VAN_PAINTED_DOOR: true });
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('renders children wrapped in PaintedDoorExperimentProvider', () => {
const control = el.instance.findByType(PaintedDoorExperimentProvider)[0];
expect(el.instance).toEqual(control);
});
});
describe('no experiments are active', () => {
mockAndRenderForBlock({ EXPERIMENT_08_23_VAN_PAINTED_DOOR: false });
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('renders children wrapped in PaintedDoorExperimentProvider', () => {
expect(el.instance.matches(shallow(children))).toEqual(true);
});
});
});
});

View File

@@ -5,7 +5,7 @@ exports[`WidgetSidebar snapshots default 1`] = `
className="widget-sidebar"
>
<div
className="d-flex"
className="d-flex flex-column"
>
<RecommendationsPanel />
</div>

View File

@@ -12,7 +12,7 @@ export const WidgetSidebar = ({ setSidebarShowing }) => {
return (
<div className="widget-sidebar">
<div className="d-flex">
<div className="d-flex flex-column">
<RecommendationsPanel />
</div>
</div>

View File

@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetNavbar snapshots default 1`] = `
<RecommendationsPaintedDoorBtn
experimentVariation=""
placement="expendedNavbar"
/>
`;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import RecommendationsPaintedDoorBtn from 'widgets/RecommendationsPaintedDoorBtn';
import { COLLAPSED_NAVBAR, EXPANDED_NAVBAR } from 'widgets/RecommendationsPaintedDoorBtn/constants';
import {
usePaintedDoorExperimentContext,
} from 'widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
export const WidgetNavbar = ({ placement }) => {
const {
experimentVariation,
isPaintedDoorNavbarBtnVariation,
experimentLoading,
} = usePaintedDoorExperimentContext();
if (!experimentLoading && isPaintedDoorNavbarBtnVariation) {
return (
<RecommendationsPaintedDoorBtn placement={placement} experimentVariation={experimentVariation} />
);
}
return null;
};
WidgetNavbar.propTypes = {
placement: PropTypes.oneOf([COLLAPSED_NAVBAR, EXPANDED_NAVBAR]).isRequired,
};
export default WidgetNavbar;

View File

@@ -0,0 +1,65 @@
import { shallow } from 'enzyme';
import {
usePaintedDoorExperimentContext,
} from '../../../widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
import WidgetNavbar from './index';
import { EXPANDED_NAVBAR } from '../../../widgets/RecommendationsPaintedDoorBtn/constants';
import RecommendationsPaintedDoorBtn from '../../../widgets/RecommendationsPaintedDoorBtn';
jest.mock('widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext', () => ({
usePaintedDoorExperimentContext: jest.fn(),
}));
describe('WidgetNavbar', () => {
let mockExperimentContext = {
experimentVariation: '',
isPaintedDoorNavbarBtnVariation: true,
experimentLoading: false,
};
const props = {
placement: EXPANDED_NAVBAR,
};
describe('snapshots', () => {
test('default', () => {
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
const wrapper = shallow(<WidgetNavbar {...props} />);
expect(usePaintedDoorExperimentContext).toHaveBeenCalled();
expect(wrapper).toMatchSnapshot();
});
});
test('renders button if user in navbar variation', () => {
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
const wrapper = shallow(<WidgetNavbar {...props} />);
expect(usePaintedDoorExperimentContext).toHaveBeenCalled();
expect(wrapper.type()).toBe(RecommendationsPaintedDoorBtn);
});
test('renders nothing if user in not in navbar variation', () => {
mockExperimentContext = {
...mockExperimentContext,
isPaintedDoorNavbarBtnVariation: false,
};
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
const wrapper = shallow(<WidgetNavbar {...props} />);
expect(usePaintedDoorExperimentContext).toHaveBeenCalled();
expect(wrapper.type()).toBeNull();
});
test('renders nothing if experiment is loading', () => {
mockExperimentContext = {
...mockExperimentContext,
isPaintedDoorNavbarBtnVariation: false,
experimentLoading: true,
};
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
const wrapper = shallow(<WidgetNavbar {...props} />);
expect(usePaintedDoorExperimentContext).toHaveBeenCalled();
expect(wrapper.type()).toBeNull();
});
});

View File

@@ -12,6 +12,7 @@ const initialState = {
suggestedCourses: [],
filterState: {},
selectSessionModal: {},
certificatePreviewModal: {},
};
export const cardId = (val) => `card-${val}`;
@@ -48,6 +49,10 @@ const app = createSlice({
...state,
selectSessionModal: { cardId: payload },
}),
updateCertificatePreviewModal: (state, { payload }) => ({
...state,
certificatePreviewModal: { cardId: payload },
}),
setPageNumber: (state, { payload }) => ({ ...state, pageNumber: payload }),
},
});

View File

@@ -19,9 +19,15 @@ export const showSelectSessionModal = createSelector(
(data) => data.cardId != null,
);
export const showCertificatePreviewModal = createSelector(
[simpleSelectors.certificatePreviewModal],
(data) => data.cardId != null,
);
export default StrictDict({
numCourses,
hasCourses,
hasAvailableDashboards,
showSelectSessionModal,
showCertificatePreviewModal,
});

View File

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

View File

@@ -13,6 +13,8 @@ export const useEmailConfirmationData = () => useSelector(selectors.emailConfirm
export const useEnterpriseDashboardData = () => useSelector(selectors.enterpriseDashboard);
export const usePlatformSettingsData = () => useSelector(selectors.platformSettings);
export const useSelectSessionModalData = () => useSelector(selectors.selectSessionModal);
export const useCertificatePreviewData = () => useSelector(selectors.certificatePreviewModal);
export const useSocialShareSettings = () => useSelector(selectors.socialShareSettings);
/** global-level meta-selectors **/
@@ -22,6 +24,7 @@ export const useCurrentCourseList = (opts) => useSelector(
state => selectors.currentList(state, opts),
);
export const useShowSelectSessionModal = () => useSelector(selectors.showSelectSessionModal);
export const useShowCertificatePreviewModal = () => useSelector(selectors.showCertificatePreviewModal);
// eslint-disable-next-line
export const useCourseCardData = (selector) => (cardId) => useSelector(
@@ -67,6 +70,11 @@ export const useUpdateSelectSessionModalCallback = (cardId) => {
return () => dispatch(actions.updateSelectSessionModal(cardId));
};
export const useUpdateCertificatePreviewModalCallback = (cardId) => {
const dispatch = useDispatch();
return () => dispatch(actions.updateCertificatePreviewModal(cardId));
};
export const useTrackCourseEvent = (tracker, cardId, ...args) => {
const { courseId } = module.useCardCourseRunData(cardId);
return (e) => tracker(courseId, ...args)(e);

View File

@@ -761,10 +761,13 @@ export const compileCourseRunData = ({ courseName, ...data }, index) => {
credit: {},
...data,
certificate: genCertificateData(data.certificate),
enrollment: genEnrollmentData({ lastEnrolled, ...data.enrollment }),
enrollment: genEnrollmentData({
lastEnrolled,
...getOption(emailOptions, index),
...data.enrollment,
}),
courseRun: genCourseRunData({
...data.courseRun,
...getOption(emailOptions, index),
courseId,
}),
course: {

View File

@@ -19,7 +19,7 @@
"learnerVariantDashboard.menu.profile.label": "الملف الشخصي",
"learnerVariantDashboard.menu.viewPrograms.label": "عرض البرامج",
"learnerVariantDashboard.menu.account.label": "الحساب",
"learnerVariantDashboard.menu.ordersAndSubscriptions.label": "Orders & Subscriptions",
"learnerVariantDashboard.menu.orderHistory.label": "Order History",
"learnerVariantDashboard.menu.signOut.label": "تسجيل الخروج",
"learnerVariantDashboard.course": "المساقات",
"learnerVariantDashboard.program": "البرامج",
@@ -44,6 +44,13 @@
"ProductRecommendations.bootcampDescription": "Intensive, hands-on, project based training",
"ProductRecommendations.courseHeading": "Courses",
"ProductRecommendations.courseDescription": "Find new interests and advance your career",
"RecommendationsPanel.recommendationsFeatureText": "Personalized recommendations feature is not yet available. We are working hard on bringing it to your learner home in the near future.",
"RecommendationsPanel.recommendationsAlertText": "Would you like to be alerted when it becomes available?",
"RecommendationsPanel.recommendationsModalHeading": "Thank you for your interest!",
"RecommendationsPanel.modalSkipButton": "Skip for now",
"RecommendationsPanel.modalCountMeButton": "Count me in!",
"learnerVariantDashboard.recommendedForYou": "Recommended For You",
"RecommendationsPanel.seeAllRecommendationsButton": "See All Recommendations",
"RecommendationsPanel.recommendationsHeading": "توصيات خاصة لك",
"RecommendationsPanel.popularCoursesHeading": "المساقات الشائعة",
"RecommendationsPanel.exploreCoursesButton": "استكشف المساقات"

View File

@@ -19,7 +19,7 @@
"learnerVariantDashboard.menu.profile.label": "Perfil",
"learnerVariantDashboard.menu.viewPrograms.label": "Ver programas",
"learnerVariantDashboard.menu.account.label": "Cuenta",
"learnerVariantDashboard.menu.ordersAndSubscriptions.label": "Pedidos y Subscripciones",
"learnerVariantDashboard.menu.orderHistory.label": "Historial de órdenes",
"learnerVariantDashboard.menu.signOut.label": "Cerrar sesión",
"learnerVariantDashboard.course": "cursos",
"learnerVariantDashboard.program": "Programas",
@@ -44,6 +44,13 @@
"ProductRecommendations.bootcampDescription": "Capacitación intensiva, práctica y basada en proyectos",
"ProductRecommendations.courseHeading": "Cursos",
"ProductRecommendations.courseDescription": "Encontrar nuevos intereses y avanzar en la carrera",
"RecommendationsPanel.recommendationsFeatureText": "La función de recomendaciones personalizadas aún no está disponible. Estamos trabajando arduamente para llevarlo a casa de su alumno en un futuro próximo.",
"RecommendationsPanel.recommendationsAlertText": "¿Le gustaría recibir una alerta cuando esté disponible?",
"RecommendationsPanel.recommendationsModalHeading": "¡Gracias por su interés!",
"RecommendationsPanel.modalSkipButton": "Saltar por ahora ",
"RecommendationsPanel.modalCountMeButton": "¡Cuente conmigo!",
"learnerVariantDashboard.recommendedForYou": "Recomendado para usted",
"RecommendationsPanel.seeAllRecommendationsButton": "Ver todas las recomendaciones",
"RecommendationsPanel.recommendationsHeading": "Recomendaciones para ti",
"RecommendationsPanel.popularCoursesHeading": "Cursos populares",
"RecommendationsPanel.exploreCoursesButton": "Explorar cursos"

View File

@@ -19,7 +19,7 @@
"learnerVariantDashboard.menu.profile.label": "Profil",
"learnerVariantDashboard.menu.viewPrograms.label": "Voir les programmes",
"learnerVariantDashboard.menu.account.label": "Compte",
"learnerVariantDashboard.menu.ordersAndSubscriptions.label": "Orders & Subscriptions",
"learnerVariantDashboard.menu.orderHistory.label": "Order History",
"learnerVariantDashboard.menu.signOut.label": "Se déconnecter",
"learnerVariantDashboard.course": "Cours",
"learnerVariantDashboard.program": "Programmes",
@@ -44,6 +44,13 @@
"ProductRecommendations.bootcampDescription": "Intensive, hands-on, project based training",
"ProductRecommendations.courseHeading": "Courses",
"ProductRecommendations.courseDescription": "Find new interests and advance your career",
"RecommendationsPanel.recommendationsFeatureText": "Personalized recommendations feature is not yet available. We are working hard on bringing it to your learner home in the near future.",
"RecommendationsPanel.recommendationsAlertText": "Would you like to be alerted when it becomes available?",
"RecommendationsPanel.recommendationsModalHeading": "Thank you for your interest!",
"RecommendationsPanel.modalSkipButton": "Skip for now",
"RecommendationsPanel.modalCountMeButton": "Count me in!",
"learnerVariantDashboard.recommendedForYou": "Recommended For You",
"RecommendationsPanel.seeAllRecommendationsButton": "See All Recommendations",
"RecommendationsPanel.recommendationsHeading": "Des recommandations pour vous",
"RecommendationsPanel.popularCoursesHeading": "Cours populaires",
"RecommendationsPanel.exploreCoursesButton": "Explorer les cours"

View File

@@ -1,6 +1,6 @@
{
"dashboard.mycourses": "Mes cours",
"Dashboard.NoCoursesView.lookingForChallengePrompt": "A la recherche d'un nouveau défi ?",
"Dashboard.NoCoursesView.lookingForChallengePrompt": "A la recherche d'un nouveau défi?",
"Dashboard.NoCoursesView.exploreCoursesPrompt": "Explorez nos cours pour les ajouter à votre tableau de bord.",
"Dashboard.NoCoursesView.exploreCoursesButton": "Explorer les cours",
"Dashboard.NoCoursesView.bannerAlt": "Aucune bannière de cours",
@@ -9,7 +9,7 @@
"leanerDashboard.enterpriseDialogDismissButton": "Rejeter",
"leanerDashboard.enterpriseDialogConfirmButton": "Aller au tableau de bord",
"leanerDashboard.confirmEmailBanner": "Confirmer maintenant",
"leanerDashboard.confirmEmailTextReminderBanner": "N'oubliez pas de confirmer votre courriel afin de pouvoir continuer à apprendre sur edX ! {confirmNowButton}.",
"leanerDashboard.confirmEmailTextReminderBanner": "N'oubliez pas de confirmer votre courriel afin de pouvoir continuer à apprendre sur EDUlib! {confirmNowButton}.",
"leanerDashboard.verifiedConfirmEmailButton": "J'ai confirmé mon courriel",
"leanerDashboard.confirmEmailModalHeader": "Confirmer votre courriel",
"leanerDashboard.confirmEmailModalBody": "Nous vous avons envoyé un courriel pour vérifier votre compte. Veuillez vérifier votre boîte de réception et cliquer sur le gros bouton rouge pour confirmer et continuer à apprendre.",
@@ -19,12 +19,12 @@
"learnerVariantDashboard.menu.profile.label": "Profil",
"learnerVariantDashboard.menu.viewPrograms.label": "Voir les programmes",
"learnerVariantDashboard.menu.account.label": "Compte",
"learnerVariantDashboard.menu.ordersAndSubscriptions.label": "Commandes et abonnements",
"learnerVariantDashboard.menu.orderHistory.label": "Historique des commandes",
"learnerVariantDashboard.menu.signOut.label": "Se déconnecter",
"learnerVariantDashboard.course": "Cours",
"learnerVariantDashboard.program": "Programmes",
"learnerVariantDashboard.discoverNew": "Découvrir les nouveautés",
"learnerVariantDashboard.logoAltText": "Tableau de bord edX, Inc.",
"learnerVariantDashboard.logoAltText": "Tableau de bord EDUlib, Inc.",
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
"learnerVariantDashboard.collapseMenuClosedAltText": "Fermer",
"leanerDashboard.menu.career.label": "Carrière",
@@ -35,7 +35,7 @@
"MasqueradeBar.StudentNameInput": "Nom d'utilisateur ou courriel",
"MasqueradeBar.NoStudentFound": "Aucun étudiant avec ce nom d'utilisateur ou cette adresse courriel n'a pu être trouvé",
"MasqueradeBar.UnknownError": "Une erreur inconnue est survenue",
"WidgetSidebar.lookingForChallengePrompt": "A la recherche d'un nouveau défi ?",
"WidgetSidebar.lookingForChallengePrompt": "A la recherche d'un nouveau défi?",
"WidgetSidebar.findCoursesButton": "Trouver un cours {arrow}",
"ProductRecommendations.recommendationsHeading": "Vous pourriez aussi aimer",
"ProductRecommendations.executiveEducationHeading": "Formation des cadres",
@@ -44,6 +44,13 @@
"ProductRecommendations.bootcampDescription": "Formation intensive, pratique et basée sur des projets",
"ProductRecommendations.courseHeading": "Cours",
"ProductRecommendations.courseDescription": "Trouvez de nouveaux intérêts et faites progresser votre carrière",
"RecommendationsPanel.recommendationsFeatureText": "La fonctionnalité de recommandations personnalisées n'est pas encore disponible. Nous travaillons fort pour le proposer à votre apprenant dans un avenir rapproché.",
"RecommendationsPanel.recommendationsAlertText": "Souhaitez-vous être alerté dès qu'il sera disponible?",
"RecommendationsPanel.recommendationsModalHeading": "Merci pour votre intérêt!",
"RecommendationsPanel.modalSkipButton": "Ignorer pour l'instant",
"RecommendationsPanel.modalCountMeButton": "Comptez sur moi!",
"learnerVariantDashboard.recommendedForYou": "Recommandé pour vous",
"RecommendationsPanel.seeAllRecommendationsButton": "Voir toutes les recommandations",
"RecommendationsPanel.recommendationsHeading": "Des recommandations pour vous",
"RecommendationsPanel.popularCoursesHeading": "Cours populaires",
"RecommendationsPanel.exploreCoursesButton": "Explorer les cours"

View File

@@ -19,7 +19,7 @@
"learnerVariantDashboard.menu.profile.label": "Perfil",
"learnerVariantDashboard.menu.viewPrograms.label": "View Programs",
"learnerVariantDashboard.menu.account.label": "Conta",
"learnerVariantDashboard.menu.ordersAndSubscriptions.label": "Orders & Subscriptions",
"learnerVariantDashboard.menu.orderHistory.label": "Order History",
"learnerVariantDashboard.menu.signOut.label": "Sair",
"learnerVariantDashboard.course": "Cursos",
"learnerVariantDashboard.program": "Programas",
@@ -44,6 +44,13 @@
"ProductRecommendations.bootcampDescription": "Intensive, hands-on, project based training",
"ProductRecommendations.courseHeading": "Courses",
"ProductRecommendations.courseDescription": "Find new interests and advance your career",
"RecommendationsPanel.recommendationsFeatureText": "Personalized recommendations feature is not yet available. We are working hard on bringing it to your learner home in the near future.",
"RecommendationsPanel.recommendationsAlertText": "Would you like to be alerted when it becomes available?",
"RecommendationsPanel.recommendationsModalHeading": "Thank you for your interest!",
"RecommendationsPanel.modalSkipButton": "Skip for now",
"RecommendationsPanel.modalCountMeButton": "Count me in!",
"learnerVariantDashboard.recommendedForYou": "Recommended For You",
"RecommendationsPanel.seeAllRecommendationsButton": "See All Recommendations",
"RecommendationsPanel.recommendationsHeading": "Recommendations for you",
"RecommendationsPanel.popularCoursesHeading": "Popular courses",
"RecommendationsPanel.exploreCoursesButton": "Explorar cursos"

View File

@@ -19,7 +19,7 @@
"learnerVariantDashboard.menu.profile.label": "Profile",
"learnerVariantDashboard.menu.viewPrograms.label": "View Programs",
"learnerVariantDashboard.menu.account.label": "Account",
"learnerVariantDashboard.menu.ordersAndSubscriptions.label": "Orders & Subscriptions",
"learnerVariantDashboard.menu.orderHistory.label": "Order History",
"learnerVariantDashboard.menu.signOut.label": "Sign Out",
"learnerVariantDashboard.course": "Courses",
"learnerVariantDashboard.program": "Programs",
@@ -44,6 +44,13 @@
"ProductRecommendations.bootcampDescription": "Intensive, hands-on, project based training",
"ProductRecommendations.courseHeading": "Courses",
"ProductRecommendations.courseDescription": "Find new interests and advance your career",
"RecommendationsPanel.recommendationsFeatureText": "Personalized recommendations feature is not yet available. We are working hard on bringing it to your learner home in the near future.",
"RecommendationsPanel.recommendationsAlertText": "Would you like to be alerted when it becomes available?",
"RecommendationsPanel.recommendationsModalHeading": "Thank you for your interest!",
"RecommendationsPanel.modalSkipButton": "Skip for now",
"RecommendationsPanel.modalCountMeButton": "Count me in!",
"learnerVariantDashboard.recommendedForYou": "Recommended For You",
"RecommendationsPanel.seeAllRecommendationsButton": "See All Recommendations",
"RecommendationsPanel.recommendationsHeading": "Recommendations for you",
"RecommendationsPanel.popularCoursesHeading": "Popular courses",
"RecommendationsPanel.exploreCoursesButton": "Explore courses"

View File

@@ -4,12 +4,14 @@ import 'regenerator-runtime/runtime';
import React from 'react';
import ReactDOM from 'react-dom';
import { Switch, Redirect } from 'react-router-dom';
import {
Route, Navigate, Routes,
} from 'react-router-dom';
import {
AppProvider,
ErrorPage,
PageRoute,
PageWrap,
} from '@edx/frontend-platform/react';
import store from 'data/store';
import {
@@ -31,12 +33,10 @@ subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={store}>
<NoticesWrapper>
<Switch>
<PageRoute path="/">
<App />
</PageRoute>
<Redirect to="/" />
</Switch>
<Routes>
<Route path="/" element={<PageWrap><App /></PageWrap>} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</NoticesWrapper>
</AppProvider>,
document.getElementById('root'),

View File

@@ -3,7 +3,7 @@ import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
Enzyme.configure({ adapter: new Adapter() });

View File

@@ -1,9 +1,9 @@
import { get, stringifyUrl } from 'data/services/lms/utils';
import urls from 'data/services/lms/urls';
export const crossProductAndAmplitudeRecommendationsUrl = (courseId) => `${urls.getApiUrl()}/learner_recommendations/product_recommendations/${courseId}/`;
export const amplitudeRecommendationsUrl = () => `${urls.getApiUrl()}/learner_recommendations/product_recommendations/`;
export const recommendationsContextUrl = () => `${urls.getApiUrl()}/learner_recommendations/recommendations_context/`;
export const crossProductAndAmplitudeRecommendationsUrl = (courseId) => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/cross_product/${courseId}/`;
export const amplitudeRecommendationsUrl = () => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/amplitude/v2/`;
export const recommendationsContextUrl = () => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/recommendations_context/`;
const fetchRecommendationsContext = () => get(stringifyUrl(recommendationsContextUrl()));

View File

@@ -0,0 +1,123 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StrictDict } from 'utils';
import * as module from './PaintedDoorExperimentContext';
import {
useEmailConfirmationData,
useHasAvailableDashboards,
useRequestIsPending,
} from '../../data/redux/hooks';
import { RequestKeys } from '../../data/constants/requests';
import { trackPaintedDoorVariationGroup } from './track';
export const state = StrictDict({
enterpriseUser: (val) => React.useState(val), // eslint-disable-line
experimentData: (val) => React.useState(val), // eslint-disable-line
});
const PAINTED_DOOR_RECOMMENDATIONS_EXP_ID = 25116810832;
const PAINTED_DOOR_RECOMMENDATIONS_PAGE = 'url_targeting_for_van1604_recommendations_painted_door_exp';
const PAINTED_DOOR_RECS_EXP_NAVBAR_BTN_VARIATION = 'btn_navbar';
const PAINTED_DOOR_RECS_EXP_WIDGET_BTN_VARIATION = 'btn_widget';
const PAINTED_DOOR_RECS_EXP_CONTROL_VARIATION = 'control';
export function getPaintedDoorRecommendationsExperimentVariation() {
try {
if (window.optimizely && window.optimizely.get('data').experiments[PAINTED_DOOR_RECOMMENDATIONS_EXP_ID]) {
const selectedVariant = window.optimizely.get('state').getVariationMap()[PAINTED_DOOR_RECOMMENDATIONS_EXP_ID];
return selectedVariant?.name;
}
} catch (e) { /* empty */ }
return '';
}
export function activatePaintedDoorRecommendationsExperiment() {
window.optimizely = window.optimizely || [];
window.optimizely.push({
type: 'page',
pageName: PAINTED_DOOR_RECOMMENDATIONS_PAGE,
});
}
export const useIsEnterpriseUser = () => {
const [enterpriseUser, setEnterpriseUser] = module.state.enterpriseUser({
isEnterpriseUser: false,
isLoading: true,
});
const initIsPending = useRequestIsPending(RequestKeys.initialize);
const hasAvailableDashboards = useHasAvailableDashboards();
const confirmationData = useEmailConfirmationData();
React.useEffect(() => {
if (!initIsPending && Object.keys(confirmationData).length && hasAvailableDashboards) {
setEnterpriseUser(prev => ({
...prev,
isEnterpriseUser: true,
isLoading: false,
}));
} else if (!initIsPending && Object.keys(confirmationData).length && !hasAvailableDashboards) {
setEnterpriseUser(prev => ({
...prev,
isEnterpriseUser: false,
isLoading: false,
}));
}
}, [initIsPending]); // eslint-disable-line react-hooks/exhaustive-deps
return enterpriseUser;
};
export const PaintedDoorExperimentContext = React.createContext();
export const PaintedDoorExperimentProvider = ({ children }) => {
const [experimentData, setExperimentData] = module.state.experimentData({
experimentVariation: '',
isPaintedDoorNavbarBtnVariation: false,
isPaintedDoorWidgetBtnVariation: false,
isPaintedDoorControlVariation: false,
experimentLoading: true,
});
const enterpriseUser = useIsEnterpriseUser();
const contextValue = React.useMemo(
() => ({
...experimentData,
}),
[experimentData],
);
React.useEffect(() => {
let timer = null;
if (!enterpriseUser.isLoading && !enterpriseUser.isEnterpriseUser) {
activatePaintedDoorRecommendationsExperiment();
timer = setTimeout(() => {
const variation = getPaintedDoorRecommendationsExperimentVariation();
setExperimentData(prev => ({
...prev,
experimentVariation: variation,
isPaintedDoorNavbarBtnVariation: variation === PAINTED_DOOR_RECS_EXP_NAVBAR_BTN_VARIATION,
isPaintedDoorWidgetBtnVariation: variation === PAINTED_DOOR_RECS_EXP_WIDGET_BTN_VARIATION,
isPaintedDoorControlVariation: variation === PAINTED_DOOR_RECS_EXP_CONTROL_VARIATION,
experimentLoading: false,
}));
trackPaintedDoorVariationGroup(variation);
}, 500);
}
return () => clearTimeout(timer);
}, [enterpriseUser]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<PaintedDoorExperimentContext.Provider value={contextValue}>
{children}
</PaintedDoorExperimentContext.Provider>
);
};
export const usePaintedDoorExperimentContext = () => React.useContext(PaintedDoorExperimentContext);
PaintedDoorExperimentProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default PaintedDoorExperimentProvider;

View File

@@ -0,0 +1,151 @@
import React from 'react';
import { mount } from 'enzyme';
import { MockUseState } from 'testUtils';
import * as experiment from './PaintedDoorExperimentContext';
import { useEmailConfirmationData, useHasAvailableDashboards, useRequestIsPending } from '../../data/redux/hooks';
import { trackPaintedDoorVariationGroup } from './track';
import { useIsEnterpriseUser } from './PaintedDoorExperimentContext';
const state = new MockUseState(experiment);
trackPaintedDoorVariationGroup();
jest.unmock('react');
jest.spyOn(React, 'useEffect').mockImplementation((cb, prereqs) => ({ useEffect: { cb, prereqs } }));
jest.mock('../../data/redux/hooks', () => ({
useRequestIsPending: jest.fn(),
useHasAvailableDashboards: jest.fn(),
useEmailConfirmationData: jest.fn(),
}));
jest.mock('./track', () => ({
trackPaintedDoorVariationGroup: jest.fn(),
}));
describe('useIsEnterpriseUser hook', () => {
describe('state fields', () => {
state.testGetter(state.keys.enterpriseUser);
});
describe('useIsEnterpriseUser', () => {
beforeEach(() => {
state.mock();
useRequestIsPending.mockReturnValueOnce(false);
useEmailConfirmationData.mockReturnValueOnce({ confirmed: true });
});
it('initializes enterpriseUser', () => {
useIsEnterpriseUser();
state.expectInitializedWith(state.keys.enterpriseUser, {
isEnterpriseUser: false,
isLoading: true,
});
});
it('get isEnterpriseUser false if useHasAvailableDashboards return false', () => {
useHasAvailableDashboards.mockReturnValueOnce(false);
state.mockVal(state.keys.enterpriseUser, {
isEnterpriseUser: false,
isLoading: false,
});
const enterpriseUser = useIsEnterpriseUser();
const [cb] = React.useEffect.mock.calls[0];
cb();
expect(enterpriseUser.isEnterpriseUser).toEqual(false);
expect(enterpriseUser.isLoading).toEqual(false);
});
it('get isEnterpriseUser true if useHasAvailableDashboards return true', () => {
useHasAvailableDashboards.mockReturnValueOnce(true);
state.mockVal(state.keys.enterpriseUser, {
isEnterpriseUser: true,
isLoading: false,
});
const enterpriseUser = useIsEnterpriseUser();
const [cb] = React.useEffect.mock.calls[0];
cb();
expect(enterpriseUser.isEnterpriseUser).toEqual(true);
expect(enterpriseUser.isLoading).toEqual(false);
});
});
});
describe('Painted door experiments context', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('PaintedDoorExperimentProvider', () => {
const { PaintedDoorExperimentProvider } = experiment;
const TestComponent = () => {
const {
experimentVariation,
isPaintedDoorNavbarBtnVariation,
isPaintedDoorWidgetBtnVariation,
isPaintedDoorControlVariation,
experimentLoading,
} = experiment.usePaintedDoorExperimentContext();
expect(experimentVariation).toEqual('');
expect(isPaintedDoorNavbarBtnVariation).toBe(false);
expect(isPaintedDoorWidgetBtnVariation).toBe(false);
expect(isPaintedDoorControlVariation).toBe(false);
expect(experimentLoading).toBe(true);
return (
<div />
);
};
it('test experiment gets activated for non enterprise users', () => {
state.mock();
jest.useFakeTimers();
useRequestIsPending.mockReturnValueOnce(false);
useHasAvailableDashboards.mockReturnValueOnce(false);
useEmailConfirmationData.mockReturnValueOnce({ confirmed: true });
state.mockVal(state.keys.enterpriseUser, {
isEnterpriseUser: false,
isLoading: false,
});
mount(
<PaintedDoorExperimentProvider>
<TestComponent />
</PaintedDoorExperimentProvider>,
);
const [cb] = React.useEffect.mock.calls[1];
cb();
jest.advanceTimersByTime(500);
expect(trackPaintedDoorVariationGroup).toHaveBeenCalledTimes(1);
});
it('test experiment does not get activated for enterprise users', () => {
state.mock();
jest.useFakeTimers();
useRequestIsPending.mockReturnValueOnce(false);
useHasAvailableDashboards.mockReturnValueOnce(true);
useEmailConfirmationData.mockReturnValueOnce({ confirmed: true });
state.mockVal(state.keys.enterpriseUser, {
isEnterpriseUser: true,
isLoading: false,
});
mount(
<PaintedDoorExperimentProvider>
<TestComponent />
</PaintedDoorExperimentProvider>,
);
const [cb] = React.useEffect.mock.calls[1];
cb();
jest.advanceTimersByTime(500);
expect(trackPaintedDoorVariationGroup).toHaveBeenCalledTimes(0);
});
});
});

View File

@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RecommendationsPaintedDoorBtn matches snapshot 1`] = `
<Fragment>
<RecommendationsPanelButton
handleClick={[Function]}
/>
<ModalView
isOpen={false}
onClose={[MockFunction]}
variation=""
/>
</Fragment>
`;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { Button } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import messages from '../messages';
import { COLLAPSED_NAVBAR, EXPANDED_NAVBAR } from '../constants';
export const NavbarButton = ({ placement, handleClick }) => {
const { formatMessage } = useIntl();
return (
<Button
as="a"
className={classNames({
'p-4': placement === EXPANDED_NAVBAR,
})}
variant="inverse-primary"
onClick={handleClick}
>
{formatMessage(messages.recommendedForYou)}
</Button>
);
};
NavbarButton.propTypes = {
placement: PropTypes.oneOf([COLLAPSED_NAVBAR, EXPANDED_NAVBAR]).isRequired,
handleClick: PropTypes.func.isRequired,
};
export default NavbarButton;

View File

@@ -0,0 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ModalView snapshot should renders default ModalView 1`] = `
<div
className="containers modal-container"
>
<ModalDialog
hasCloseButton={false}
isBlocking={true}
isFullscreenScroll={true}
isOpen={true}
onClose={[MockFunction]}
title=""
>
<ModalDialog.Header>
<Component
className="mt-2"
>
Thank you for your interest!
</Component>
</ModalDialog.Header>
<ModalDialog.Body>
<div>
<p
className="mt-2"
>
Personalized recommendations feature is not yet available. We are working hard on bringing it to your learner home in the near future.
</p>
<p>
Would you like to be alerted when it becomes available?
</p>
</div>
</ModalDialog.Body>
<Component>
<ActionRow>
<Component
onClick={[Function]}
variant="tertiary"
>
Skip for now
</Component>
<Component
onClick={[Function]}
variant="primary"
>
Count me in!
</Component>
</ActionRow>
</Component>
</ModalDialog>
</div>
`;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { StrictDict } from 'utils';
import * as module from './hooks';
export const state = StrictDict({
isModalOpen: (val) => React.useState(val), // eslint-disable-line
});
export const usePaintedDoorModal = () => {
const [isModalOpen, setIsModalOpen] = module.state.isModalOpen(false);
const toggleModal = () => setIsModalOpen(!isModalOpen);
return {
isModalOpen,
toggleModal,
};
};

View File

@@ -0,0 +1,30 @@
import { MockUseState } from 'testUtils';
import * as hooks from './hooks';
const state = new MockUseState(hooks);
const {
usePaintedDoorModal,
} = hooks;
describe('LearnerDashboardHeader hooks', () => {
describe('state fields', () => {
state.testGetter(state.keys.isModalOpen);
});
describe('useRecommendationsModal', () => {
beforeEach(() => {
state.mock();
});
it('initializes isModalOpen with false', () => {
usePaintedDoorModal();
state.expectInitializedWith(state.keys.isModalOpen, false);
});
test('change isModalOpen value on toggle', () => {
const out = usePaintedDoorModal();
state.expectInitializedWith(state.keys.isModalOpen, false);
out.toggleModal();
expect(state.values.isModalOpen).toEqual(true);
});
});
});

View File

@@ -0,0 +1,70 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ModalDialog, ActionRow } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../../messages';
import './index.scss';
import {
trackPaintedDoorRecommendationHomeInterestBtnClicked,
trackPaintedDoorRecommendationHomeSkipBtnClicked,
} from '../../track';
export const ModalView = ({
isOpen,
onClose,
variation,
}) => {
const { formatMessage } = useIntl();
const handleSkipBtnClick = () => trackPaintedDoorRecommendationHomeSkipBtnClicked(variation);
const handleInterestBtnClick = () => trackPaintedDoorRecommendationHomeInterestBtnClicked(variation);
return (
<div className="containers modal-container">
<ModalDialog
title=""
isOpen={isOpen}
onClose={onClose}
hasCloseButton={false}
isFullscreenScroll
isBlocking
>
<ModalDialog.Header>
<ModalDialog.Title className="mt-2">
{formatMessage(messages.recommendationsModalHeading)}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<div>
<p className="mt-2">{formatMessage(messages.recommendationsFeatureText)}</p>
<p>{formatMessage(messages.recommendationsAlertText)}</p>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary" onClick={handleSkipBtnClick}>
{formatMessage(messages.modalSkipButton)}
</ModalDialog.CloseButton>
<ModalDialog.CloseButton variant="primary" onClick={handleInterestBtnClick}>
{formatMessage(messages.modalCountMeButton)}
</ModalDialog.CloseButton>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
</div>
);
};
ModalView.defaultProps = {
isOpen: false,
};
ModalView.propTypes = {
onClose: PropTypes.func.isRequired,
isOpen: PropTypes.bool,
variation: PropTypes.string.isRequired,
};
export default ModalView;

View File

@@ -0,0 +1,15 @@
@import "@edx/paragon/scss/core/core";
.modal-container {
display: none;
}
@media (max-width: 464px) {
.pgn__action-row{
flex-direction: column;
gap: 5px;
button {
width: 100%;
}
}
}

View File

@@ -0,0 +1,22 @@
import { shallow } from 'enzyme';
import ModalView from '.';
jest.mock('../../track', () => ({
trackPaintedDoorRecommendationHomeSkipBtnClicked: jest.fn(),
trackPaintedDoorRecommendationHomeInterestBtnClicked: jest.fn(),
}));
describe('ModalView', () => {
const props = {
isOpen: true,
onClose: jest.fn(),
variation: '',
};
describe('snapshot', () => {
test('should renders default ModalView', () => {
const wrapper = shallow(<ModalView {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { Button } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import messages from '../messages';
export const RecommendationsPanelButton = ({ handleClick }) => {
const { formatMessage } = useIntl();
return (
<Button
as="a"
variant="brand"
onClick={handleClick}
>
{formatMessage(messages.seeAllRecommendationsButton)}
</Button>
);
};
RecommendationsPanelButton.propTypes = {
handleClick: PropTypes.func.isRequired,
};
export default RecommendationsPanelButton;

View File

@@ -0,0 +1,3 @@
export const COLLAPSED_NAVBAR = 'collapsedNavbar';
export const EXPANDED_NAVBAR = 'expendedNavbar';
export const RECOMMENDATIONS_PANEL = 'recommendationsPanel';

View File

@@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import NavbarButton from './components/NavbarButton';
import PaintedDoorModal from './components/PaintedDoorModal';
import { usePaintedDoorModal } from './components/PaintedDoorModal/hooks';
import { COLLAPSED_NAVBAR, EXPANDED_NAVBAR, RECOMMENDATIONS_PANEL } from './constants';
import RecommendationsPanelButton from './components/RecommendationsPanelButton';
import { trackPaintedDoorRecommendationHomeBtnClicked } from './track';
export const RecommendationsPaintedDoorBtn = ({ placement, experimentVariation }) => {
const { isModalOpen, toggleModal } = usePaintedDoorModal();
const handleClick = () => {
toggleModal();
trackPaintedDoorRecommendationHomeBtnClicked(experimentVariation);
};
const getPaintedDoorButton = () => {
if ([COLLAPSED_NAVBAR, EXPANDED_NAVBAR].includes(placement)) {
return (
<NavbarButton handleClick={handleClick} placement={placement} />
);
} if (placement === RECOMMENDATIONS_PANEL) {
return (
<RecommendationsPanelButton handleClick={handleClick} />
);
}
return null;
};
return (
<>
{getPaintedDoorButton()}
<PaintedDoorModal isOpen={isModalOpen} onClose={toggleModal} variation={experimentVariation} />
</>
);
};
RecommendationsPaintedDoorBtn.propTypes = {
placement: PropTypes.oneOf([COLLAPSED_NAVBAR, EXPANDED_NAVBAR, RECOMMENDATIONS_PANEL]).isRequired,
experimentVariation: PropTypes.string.isRequired,
};
export default RecommendationsPaintedDoorBtn;

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { Button, ModalDialog } from '@edx/paragon';
import RecommendationsPaintedDoorBtn from './index';
import { EXPANDED_NAVBAR, RECOMMENDATIONS_PANEL } from './constants';
import NavbarButton from './components/NavbarButton';
import RecommendationsPanelButton from './components/RecommendationsPanelButton';
import { trackPaintedDoorRecommendationHomeBtnClicked } from './track';
jest.mock('./components/PaintedDoorModal/hooks', () => ({
usePaintedDoorModal: jest.fn(() => ({
isModalOpen: false,
toggleModal: jest.fn(),
})),
}));
jest.mock('./track', () => ({
trackPaintedDoorRecommendationHomeBtnClicked: jest.fn(),
}));
describe('RecommendationsPaintedDoorBtn', () => {
let props = {
placement: RECOMMENDATIONS_PANEL,
experimentVariation: '',
};
it('matches snapshot', () => {
expect(shallow(<RecommendationsPaintedDoorBtn {...props} />)).toMatchSnapshot();
});
it('renders painted door modal', () => {
const wrapper = shallow(<RecommendationsPaintedDoorBtn {...props} />);
expect(wrapper.find(ModalDialog)).toBeTruthy();
});
it('renders painted door navbar button', () => {
props = {
...props,
placement: EXPANDED_NAVBAR,
};
const wrapper = shallow(<RecommendationsPaintedDoorBtn {...props} />);
expect(wrapper.find(NavbarButton).exists()).toBe(true);
expect(wrapper.find(RecommendationsPanelButton).exists()).toBe(false);
});
it('renders painted door recommendations panel button', () => {
props = {
...props,
placement: RECOMMENDATIONS_PANEL,
};
const wrapper = shallow(<RecommendationsPaintedDoorBtn {...props} />);
expect(wrapper.find(NavbarButton).exists()).toBe(false);
expect(wrapper.find(RecommendationsPanelButton).exists()).toBe(true);
});
it('test no button (null) rendered for invalid placement', () => {
props = {
...props,
placement: '',
};
const wrapper = shallow(<RecommendationsPaintedDoorBtn {...props} />);
expect(wrapper.find(NavbarButton).exists()).toBe(false);
expect(wrapper.find(RecommendationsPanelButton).exists()).toBe(false);
});
it('test track event is fired on navbar button click', () => {
props = {
...props,
placement: EXPANDED_NAVBAR,
};
const wrapper = mount(<RecommendationsPaintedDoorBtn {...props} />);
const navbarButton = wrapper.find(NavbarButton);
navbarButton.find(Button).simulate('click');
expect(trackPaintedDoorRecommendationHomeBtnClicked).toHaveBeenCalled();
});
it('test track event is fired on recommendations panel button click', () => {
props = {
...props,
placement: RECOMMENDATIONS_PANEL,
};
const wrapper = mount(<RecommendationsPaintedDoorBtn {...props} />);
const navbarButton = wrapper.find(RecommendationsPanelButton);
navbarButton.find(Button).simulate('click');
expect(trackPaintedDoorRecommendationHomeBtnClicked).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,41 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
recommendationsFeatureText: {
id: 'RecommendationsPanel.recommendationsFeatureText',
defaultMessage: 'Personalized recommendations feature is not yet available. We are working hard on bringing it to your learner home in the near future.',
description: 'recommendations feature text',
},
recommendationsAlertText: {
id: 'RecommendationsPanel.recommendationsAlertText',
defaultMessage: 'Would you like to be alerted when it becomes available?',
description: 'recommendations alerted text',
},
recommendationsModalHeading: {
id: 'RecommendationsPanel.recommendationsModalHeading',
defaultMessage: 'Thank you for your interest!',
description: 'Heading of modal',
},
modalSkipButton: {
id: 'RecommendationsPanel.modalSkipButton',
defaultMessage: 'Skip for now',
description: 'button for Skip for now',
},
modalCountMeButton: {
id: 'RecommendationsPanel.modalCountMeButton',
defaultMessage: 'Count me in!',
description: 'button for Count me in!',
},
recommendedForYou: {
id: 'learnerVariantDashboard.recommendedForYou',
defaultMessage: 'Recommended For You',
description: 'Header link for recommended page.',
},
seeAllRecommendationsButton: {
id: 'RecommendationsPanel.seeAllRecommendationsButton',
defaultMessage: 'See All Recommendations',
description: 'Button to see all recommendations',
},
});
export default messages;

View File

@@ -0,0 +1,35 @@
import { StrictDict } from 'utils';
import { createEventTracker } from 'data/services/segment/utils';
export const eventNames = StrictDict({
variationGroup: 'edx.bi.user.recommendation_home.variation.group',
recommendationHomeBtnClicked: 'edx.bi.user.recommendation_home.btn.clicked',
recommendationHomeModalInterestBtnClicked: 'edx.bi.user.recommendation_home.modal.interest_btn.clicked',
recommendationHomeModalSkipBtnClicked: 'edx.bi.user.recommendation_home.modal.skip_btn.clicked',
});
export const trackPaintedDoorVariationGroup = (variation) => {
createEventTracker(eventNames.variationGroup, {
variation,
page: 'dashboard',
})();
};
export const trackPaintedDoorRecommendationHomeBtnClicked = (variation) => {
createEventTracker(eventNames.recommendationHomeBtnClicked, {
variation,
page: 'dashboard',
})();
};
export const trackPaintedDoorRecommendationHomeInterestBtnClicked = (variation) => {
createEventTracker(eventNames.recommendationHomeModalInterestBtnClicked, {
variation,
})();
};
export const trackPaintedDoorRecommendationHomeSkipBtnClicked = (variation) => {
createEventTracker(eventNames.recommendationHomeModalSkipBtnClicked, {
variation,
})();
};

View File

@@ -0,0 +1,58 @@
import { createEventTracker } from 'data/services/segment/utils';
import {
eventNames,
trackPaintedDoorRecommendationHomeBtnClicked,
trackPaintedDoorVariationGroup,
trackPaintedDoorRecommendationHomeSkipBtnClicked,
trackPaintedDoorRecommendationHomeInterestBtnClicked,
} from './track';
jest.mock('data/services/segment/utils', () => ({
createEventTracker: jest.fn(() => () => {}),
}));
const TEST_VARIATION = 'testVariation';
describe('Recommendations Painted Door experiment trackers', () => {
it('test creates an event tracker for painted door variation group', () => {
trackPaintedDoorVariationGroup(TEST_VARIATION);
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.variationGroup,
{
variation: TEST_VARIATION,
page: 'dashboard',
},
);
});
it('test creates an event tracker for painted door button click', () => {
trackPaintedDoorRecommendationHomeBtnClicked(TEST_VARIATION);
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.recommendationHomeBtnClicked,
{
variation: TEST_VARIATION,
page: 'dashboard',
},
);
});
it('test creates an event tracker for painted door skip button click', () => {
trackPaintedDoorRecommendationHomeSkipBtnClicked(TEST_VARIATION);
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.recommendationHomeModalSkipBtnClicked,
{
variation: TEST_VARIATION,
},
);
});
it('test creates an event tracker for painted door interest button click', () => {
trackPaintedDoorRecommendationHomeInterestBtnClicked(TEST_VARIATION);
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.recommendationHomeModalInterestBtnClicked,
{
variation: TEST_VARIATION,
},
);
});
});

View File

@@ -12,6 +12,9 @@ import CourseCard from './components/CourseCard';
import messages from './messages';
import './index.scss';
import { usePaintedDoorExperimentContext } from '../RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
import { RECOMMENDATIONS_PANEL } from '../RecommendationsPaintedDoorBtn/constants';
import RecommendationsPaintedDoorBtn from '../RecommendationsPaintedDoorBtn';
export const LoadedView = ({
courses,
@@ -19,6 +22,11 @@ export const LoadedView = ({
}) => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const {
experimentVariation,
isPaintedDoorWidgetBtnVariation,
experimentLoading,
} = usePaintedDoorExperimentContext();
return (
<div className="p-4 w-100 panel-background">
@@ -35,15 +43,19 @@ export const LoadedView = ({
))}
</div>
<div className="text-center explore-courses-btn">
<Button
variant="tertiary"
iconBefore={Search}
as="a"
href={baseAppUrl(courseSearchUrl)}
onClick={track.findCoursesWidgetClicked(baseAppUrl(courseSearchUrl))}
>
{formatMessage(messages.exploreCoursesButton)}
</Button>
{!experimentLoading && isPaintedDoorWidgetBtnVariation ? (
<RecommendationsPaintedDoorBtn placement={RECOMMENDATIONS_PANEL} experimentVariation={experimentVariation} />
) : (
<Button
variant="tertiary"
iconBefore={Search}
as="a"
href={baseAppUrl(courseSearchUrl)}
onClick={track.findCoursesWidgetClicked(baseAppUrl(courseSearchUrl))}
>
{formatMessage(messages.exploreCoursesButton)}
</Button>
)}
</div>
</div>
);

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Button } from '@edx/paragon';
import LoadedView from './LoadedView';
import mockData from './mockData';
import messages from './messages';
import { usePaintedDoorExperimentContext } from '../RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
import RecommendationsPaintedDoorBtn from '../RecommendationsPaintedDoorBtn';
jest.mock('hooks', () => ({
reduxHooks: {
@@ -19,22 +22,62 @@ jest.mock('./track', () => ({
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
}));
jest.mock('./components/CourseCard', () => 'CourseCard');
jest.mock('widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext', () => ({
usePaintedDoorExperimentContext: jest.fn(),
}));
describe('RecommendationsPanel LoadedView', () => {
const props = {
courses: mockData.courses,
isControl: null,
};
describe('snapshot', () => {
let mockExperimentContext = {
experimentVariation: '',
isPaintedDoorWidgetBtnVariation: true,
experimentLoading: false,
};
describe('RecommendationPanelLoadedView', () => {
test('without personalize recommendation', () => {
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
const el = shallow(<LoadedView {...props} />);
expect(el).toMatchSnapshot();
expect(el.find('h3').text()).toEqual(messages.popularCoursesHeading.defaultMessage);
});
test('with personalize recommendation', () => {
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
const el = shallow(<LoadedView {...props} isControl={false} />);
expect(el).toMatchSnapshot();
expect(el.find('h3').text()).toEqual(messages.recommendationsHeading.defaultMessage);
});
test('test painted door button is rendered if user is in variation', () => {
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
const wrapper = shallow(<LoadedView {...props} />);
expect(wrapper.find(RecommendationsPaintedDoorBtn).exists()).toEqual(true);
});
test('test explore courses button is returned if user is not in variation', () => {
mockExperimentContext = {
...mockExperimentContext,
isPaintedDoorWidgetBtnVariation: false,
};
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
const wrapper = shallow(<LoadedView {...props} />);
expect(wrapper.find(RecommendationsPaintedDoorBtn).exists()).toEqual(false);
expect(wrapper.find(Button).text()).toEqual(messages.exploreCoursesButton.defaultMessage);
});
test('test explore courses button is returned if experiment is loading', () => {
mockExperimentContext = {
...mockExperimentContext,
isPaintedDoorWidgetBtnVariation: false,
experimentLoading: true,
};
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
const wrapper = shallow(<LoadedView {...props} />);
expect(wrapper.find(RecommendationsPaintedDoorBtn).exists()).toEqual(false);
expect(wrapper.find(Button).text()).toEqual(messages.exploreCoursesButton.defaultMessage);
});
});
});

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RecommendationsPanel LoadedView snapshot with personalize recommendation 1`] = `
exports[`RecommendationsPanel LoadedView RecommendationPanelLoadedView with personalize recommendation 1`] = `
<div
className="p-4 w-100 panel-background"
>
@@ -62,20 +62,15 @@ exports[`RecommendationsPanel LoadedView snapshot with personalize recommendatio
<div
className="text-center explore-courses-btn"
>
<Button
as="a"
href="http://localhost:18000/course-search-url"
iconBefore={[MockFunction icons.Search]}
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
variant="tertiary"
>
Explore courses
</Button>
<RecommendationsPaintedDoorBtn
experimentVariation=""
placement="recommendationsPanel"
/>
</div>
</div>
`;
exports[`RecommendationsPanel LoadedView snapshot without personalize recommendation 1`] = `
exports[`RecommendationsPanel LoadedView RecommendationPanelLoadedView without personalize recommendation 1`] = `
<div
className="p-4 w-100 panel-background"
>
@@ -137,15 +132,10 @@ exports[`RecommendationsPanel LoadedView snapshot without personalize recommenda
<div
className="text-center explore-courses-btn"
>
<Button
as="a"
href="http://localhost:18000/course-search-url"
iconBefore={[MockFunction icons.Search]}
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
variant="tertiary"
>
Explore courses
</Button>
<RecommendationsPaintedDoorBtn
experimentVariation=""
placement="recommendationsPanel"
/>
</div>
</div>
`;

View File

@@ -2,7 +2,7 @@ import { StrictDict } from 'utils';
import { get, stringifyUrl } from 'data/services/lms/utils';
import urls from 'data/services/lms/urls';
export const getFetchUrl = () => (`${urls.getApiUrl()}/learner_recommendations/courses/`);
export const getFetchUrl = () => (`${urls.getApiUrl()}/edx_recommendations/learner_dashboard/amplitude/`);
export const apiKeys = StrictDict({ user: 'user' });
const fetchRecommendedCourses = () => get(stringifyUrl(getFetchUrl()));

View File

@@ -23,7 +23,7 @@
}
}
.pgn__card-section {
padding: 0;
padding: 0 !important;
}
margin-top: 0.313rem;
}

View File

@@ -4,6 +4,9 @@ import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
import LoadingView from './LoadingView';
import LoadedView from './LoadedView';
import hooks from './hooks';
import RecommendationsPaintedDoorBtn from '../RecommendationsPaintedDoorBtn';
import { RECOMMENDATIONS_PANEL } from '../RecommendationsPaintedDoorBtn/constants';
import { usePaintedDoorExperimentContext } from '../RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
export const RecommendationsPanel = () => {
const {
@@ -13,6 +16,29 @@ export const RecommendationsPanel = () => {
isLoaded,
isLoading,
} = hooks.useRecommendationPanelData();
const {
experimentVariation,
isPaintedDoorWidgetBtnVariation,
experimentLoading,
} = usePaintedDoorExperimentContext();
const getDefaultOrFailedStateWidget = () => {
if (!experimentLoading && isPaintedDoorWidgetBtnVariation) {
return (
<>
<LookingForChallengeWidget />
<div className="pt-3" />
<RecommendationsPaintedDoorBtn
experimentVariation={experimentVariation}
placement={RECOMMENDATIONS_PANEL}
/>
</>
);
}
return (
<LookingForChallengeWidget />
);
};
if (isLoading) {
return (<LoadingView />);
@@ -23,10 +49,10 @@ export const RecommendationsPanel = () => {
);
}
if (isFailed) {
return (<LookingForChallengeWidget />);
return getDefaultOrFailedStateWidget();
}
// default fallback
return (<LookingForChallengeWidget />);
return getDefaultOrFailedStateWidget();
};
export default RecommendationsPanel;

View File

@@ -7,6 +7,8 @@ import mockData from './mockData';
import LoadedView from './LoadedView';
import LoadingView from './LoadingView';
import RecommendationsPanel from '.';
import { usePaintedDoorExperimentContext } from '../RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
import RecommendationsPaintedDoorBtn from '../RecommendationsPaintedDoorBtn';
jest.mock('./hooks', () => ({
useRecommendationPanelData: jest.fn(),
@@ -14,6 +16,9 @@ jest.mock('./hooks', () => ({
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('./LoadingView', () => 'LoadingView');
jest.mock('./LoadedView', () => 'LoadedView');
jest.mock('widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext', () => ({
usePaintedDoorExperimentContext: jest.fn(),
}));
const { courses } = mockData;
@@ -28,38 +33,108 @@ describe('RecommendationsPanel snapshot', () => {
isLoading: false,
...defaultLoadedViewProps,
};
it('displays LoadingView if request is loading', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
isLoading: true,
describe('RecommendationsPanel recommendations tests', () => {
beforeEach(() => {
usePaintedDoorExperimentContext.mockReturnValueOnce({
experimentVariation: '',
isPaintedDoorWidgetBtnVariation: false,
experimentLoading: false,
});
});
it('displays LoadingView if request is loading', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
isLoading: true,
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(shallow(<LoadingView />));
});
it('displays LoadedView with courses if request is loaded', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
courses,
isLoaded: true,
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(
shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />),
);
});
it('displays LookingForChallengeWidget if request is failed', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
isFailed: true,
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(
shallow(<LookingForChallengeWidget />),
);
});
it('defaults to LookingForChallengeWidget if no flags are true', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(
shallow(<LookingForChallengeWidget />),
);
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(shallow(<LoadingView />));
});
it('displays LoadedView with courses if request is loaded', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
courses,
isLoaded: true,
describe('RecommendationsPanel painted door exp tests', () => {
it('displays painted door btn if user is in variation and request is failed', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
isFailed: true,
});
usePaintedDoorExperimentContext.mockReturnValueOnce({
experimentVariation: '',
isPaintedDoorWidgetBtnVariation: true,
experimentLoading: false,
});
const wrapper = shallow(<RecommendationsPanel />);
expect(wrapper.find(RecommendationsPaintedDoorBtn).exists()).toBe(true);
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(
shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />),
);
});
it('displays LookingForChallengeWidget if request is failed', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
isFailed: true,
it('displays painted door btn if user is in variation and no flags are set (defaults)', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
isFailed: true,
});
usePaintedDoorExperimentContext.mockReturnValueOnce({
experimentVariation: '',
isPaintedDoorWidgetBtnVariation: true,
experimentLoading: false,
});
const wrapper = shallow(<RecommendationsPanel />);
expect(wrapper.find(RecommendationsPaintedDoorBtn).exists()).toBe(true);
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(
shallow(<LookingForChallengeWidget />),
);
});
it('defaults to LookingForChallengeWidget if no flags are true', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
it('renders only LookingForChallengeWidget if user is not in variation', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
isFailed: true,
});
usePaintedDoorExperimentContext.mockReturnValueOnce({
experimentVariation: '',
isPaintedDoorWidgetBtnVariation: false,
experimentLoading: false,
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(
shallow(<LookingForChallengeWidget />),
);
});
it('renders only LookingForChallengeWidget if experiment is loading', () => {
hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues,
isFailed: true,
});
usePaintedDoorExperimentContext.mockReturnValueOnce({
experimentVariation: '',
isPaintedDoorWidgetBtnVariation: false,
experimentLoading: true,
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(
shallow(<LookingForChallengeWidget />),
);
});
expect(shallow(<RecommendationsPanel />)).toMatchObject(
shallow(<LookingForChallengeWidget />),
);
});
});