Compare commits
24 Commits
recommenda
...
schen/upse
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdb0f48f08 | ||
|
|
3cdcc1fe61 | ||
|
|
82ff0d7ddb | ||
|
|
1478956e34 | ||
|
|
f049712430 | ||
|
|
c977de2df9 | ||
|
|
4b20c5bbdd | ||
|
|
0c1fa2f030 | ||
|
|
91117cce6a | ||
|
|
e6dba8bdc2 | ||
|
|
1d67ac5f24 | ||
|
|
60d2f22c50 | ||
|
|
5dc89d7404 | ||
|
|
0f24d3a52d | ||
|
|
fc885d02dc | ||
|
|
2e09d3632e | ||
|
|
d8cb46da60 | ||
|
|
199d6e7c60 | ||
|
|
64563d58f9 | ||
|
|
1e9a0a87b6 | ||
|
|
d42d0cdc59 | ||
|
|
8fef92d94d | ||
|
|
b41eee47c9 | ||
|
|
909f3f1f47 |
1
.env
1
.env
@@ -41,3 +41,4 @@ ACCOUNT_PROFILE_URL=''
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
EXPERIMENT_08_23_VAN_PAINTED_DOOR='true'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
7153
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
38
src/App.jsx
38
src/App.jsx
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
29
src/containers/CertificatePreviewModal/hooks.js
Normal file
29
src/containers/CertificatePreviewModal/hooks.js
Normal 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;
|
||||
48
src/containers/CertificatePreviewModal/index.jsx
Normal file
48
src/containers/CertificatePreviewModal/index.jsx
Normal 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;
|
||||
12
src/containers/CertificatePreviewModal/messages.js
Normal file
12
src/containers/CertificatePreviewModal/messages.js
Normal 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;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,9 @@ exports[`ExpandedHeader render 1`] = `
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<WidgetNavbar
|
||||
placement="expendedNavbar"
|
||||
/>
|
||||
<span
|
||||
className="flex-grow-1"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
23
src/containers/WidgetContainers/AppWrapper/index.jsx
Normal file
23
src/containers/WidgetContainers/AppWrapper/index.jsx
Normal 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;
|
||||
56
src/containers/WidgetContainers/AppWrapper/index.test.jsx
Normal file
56
src/containers/WidgetContainers/AppWrapper/index.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ exports[`WidgetSidebar snapshots default 1`] = `
|
||||
className="widget-sidebar"
|
||||
>
|
||||
<div
|
||||
className="d-flex"
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<RecommendationsPanel />
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WidgetNavbar snapshots default 1`] = `
|
||||
<RecommendationsPaintedDoorBtn
|
||||
experimentVariation=""
|
||||
placement="expendedNavbar"
|
||||
/>
|
||||
`;
|
||||
29
src/containers/WidgetContainers/WidgetNavbar/index.jsx
Normal file
29
src/containers/WidgetContainers/WidgetNavbar/index.jsx
Normal 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;
|
||||
65
src/containers/WidgetContainers/WidgetNavbar/index.test.jsx
Normal file
65
src/containers/WidgetContainers/WidgetNavbar/index.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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 }),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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": "استكشف المساقات"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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() });
|
||||
|
||||
|
||||
@@ -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()));
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
3
src/widgets/RecommendationsPaintedDoorBtn/constants.js
Normal file
3
src/widgets/RecommendationsPaintedDoorBtn/constants.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const COLLAPSED_NAVBAR = 'collapsedNavbar';
|
||||
export const EXPANDED_NAVBAR = 'expendedNavbar';
|
||||
export const RECOMMENDATIONS_PANEL = 'recommendationsPanel';
|
||||
44
src/widgets/RecommendationsPaintedDoorBtn/index.jsx
Normal file
44
src/widgets/RecommendationsPaintedDoorBtn/index.jsx
Normal 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;
|
||||
95
src/widgets/RecommendationsPaintedDoorBtn/index.test.jsx
Normal file
95
src/widgets/RecommendationsPaintedDoorBtn/index.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
41
src/widgets/RecommendationsPaintedDoorBtn/messages.js
Normal file
41
src/widgets/RecommendationsPaintedDoorBtn/messages.js
Normal 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;
|
||||
35
src/widgets/RecommendationsPaintedDoorBtn/track.js
Normal file
35
src/widgets/RecommendationsPaintedDoorBtn/track.js
Normal 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,
|
||||
})();
|
||||
};
|
||||
58
src/widgets/RecommendationsPaintedDoorBtn/track.test.js
Normal file
58
src/widgets/RecommendationsPaintedDoorBtn/track.test.js
Normal 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,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
}
|
||||
.pgn__card-section {
|
||||
padding: 0;
|
||||
padding: 0 !important;
|
||||
}
|
||||
margin-top: 0.313rem;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user