Compare commits
9 Commits
master
...
recommenda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a20ec2c3e6 | ||
|
|
362d2406d3 | ||
|
|
736abb4247 | ||
|
|
05bdf4ee02 | ||
|
|
30fadadb26 | ||
|
|
825cca2d8d | ||
|
|
3bad4897ba | ||
|
|
743c233f6d | ||
|
|
6cecc38e85 |
@@ -5,6 +5,15 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import LearnerDashboardHeader from './containers/LearnerDashboardHeader';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
import { PaintedDoorExperimentProvider } from './RecsPaintedDoorExpContext';
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const { authenticatedUser } = React.useContext(AppContext);
|
const { authenticatedUser } = React.useContext(AppContext);
|
||||||
@@ -78,6 +79,7 @@ export const App = () => {
|
|||||||
<title>{formatMessage(messages.pageTitle)}</title>
|
<title>{formatMessage(messages.pageTitle)}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div>
|
<div>
|
||||||
|
<PaintedDoorExperimentProvider>
|
||||||
<LearnerDashboardHeader />
|
<LearnerDashboardHeader />
|
||||||
<main>
|
<main>
|
||||||
{hasNetworkFailure
|
{hasNetworkFailure
|
||||||
@@ -93,6 +95,7 @@ export const App = () => {
|
|||||||
</main>
|
</main>
|
||||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||||
<ZendeskFab />
|
<ZendeskFab />
|
||||||
|
</PaintedDoorExperimentProvider>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ jest.mock('components/ZendeskFab', () => 'ZendeskFab');
|
|||||||
jest.mock('ExperimentContext', () => ({
|
jest.mock('ExperimentContext', () => ({
|
||||||
ExperimentProvider: 'ExperimentProvider',
|
ExperimentProvider: 'ExperimentProvider',
|
||||||
}));
|
}));
|
||||||
|
jest.mock('RecsPaintedDoorExpContext', () => ({
|
||||||
|
PaintedDoorExperimentProvider: 'PaintedDoorExperimentProvider',
|
||||||
|
}));
|
||||||
jest.mock('data/redux', () => ({
|
jest.mock('data/redux', () => ({
|
||||||
selectors: 'redux.selectors',
|
selectors: 'redux.selectors',
|
||||||
actions: 'redux.actions',
|
actions: 'redux.actions',
|
||||||
|
|||||||
120
src/RecsPaintedDoorExpContext.jsx
Normal file
120
src/RecsPaintedDoorExpContext.jsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { StrictDict } from 'utils';
|
||||||
|
import * as module from './RecsPaintedDoorExpContext';
|
||||||
|
import {
|
||||||
|
useEmailConfirmationData,
|
||||||
|
useHasAvailableDashboards,
|
||||||
|
useRequestIsPending,
|
||||||
|
} from './data/redux/hooks';
|
||||||
|
import { RequestKeys } from './data/constants/requests';
|
||||||
|
import { trackPaintedDoorVariationGroup } from './widgets/RecommendationsPanel/recsPaintedDoorExpTrack';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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 usePaintedDoorExperimentContext;
|
||||||
@@ -11,6 +11,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
|||||||
</title>
|
</title>
|
||||||
</HelmetWrapper>
|
</HelmetWrapper>
|
||||||
<div>
|
<div>
|
||||||
|
<PaintedDoorExperimentProvider>
|
||||||
<LearnerDashboardHeader />
|
<LearnerDashboardHeader />
|
||||||
<main>
|
<main>
|
||||||
<Alert
|
<Alert
|
||||||
@@ -25,6 +26,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
|||||||
logo="fakeLogo.png"
|
logo="fakeLogo.png"
|
||||||
/>
|
/>
|
||||||
<ZendeskFab />
|
<ZendeskFab />
|
||||||
|
</PaintedDoorExperimentProvider>
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
`;
|
`;
|
||||||
@@ -40,6 +42,7 @@ exports[`App router component component no network failure snapshot 1`] = `
|
|||||||
</title>
|
</title>
|
||||||
</HelmetWrapper>
|
</HelmetWrapper>
|
||||||
<div>
|
<div>
|
||||||
|
<PaintedDoorExperimentProvider>
|
||||||
<LearnerDashboardHeader />
|
<LearnerDashboardHeader />
|
||||||
<main>
|
<main>
|
||||||
<ExperimentProvider>
|
<ExperimentProvider>
|
||||||
@@ -50,6 +53,7 @@ exports[`App router component component no network failure snapshot 1`] = `
|
|||||||
logo="fakeLogo.png"
|
logo="fakeLogo.png"
|
||||||
/>
|
/>
|
||||||
<ZendeskFab />
|
<ZendeskFab />
|
||||||
|
</PaintedDoorExperimentProvider>
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
`;
|
`;
|
||||||
@@ -65,6 +69,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
|||||||
</title>
|
</title>
|
||||||
</HelmetWrapper>
|
</HelmetWrapper>
|
||||||
<div>
|
<div>
|
||||||
|
<PaintedDoorExperimentProvider>
|
||||||
<LearnerDashboardHeader />
|
<LearnerDashboardHeader />
|
||||||
<main>
|
<main>
|
||||||
<Alert
|
<Alert
|
||||||
@@ -79,6 +84,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
|||||||
logo="fakeLogo.png"
|
logo="fakeLogo.png"
|
||||||
/>
|
/>
|
||||||
<ZendeskFab />
|
<ZendeskFab />
|
||||||
|
</PaintedDoorExperimentProvider>
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
`;
|
`;
|
||||||
|
|||||||
54
src/components/ModalView/__snapshots__/index.test.jsx.snap
Normal file
54
src/components/ModalView/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ModalView snapshot should renders default ModalView 1`] = `
|
||||||
|
<div
|
||||||
|
className="containers"
|
||||||
|
>
|
||||||
|
<ModalDialog
|
||||||
|
hasCloseButton={false}
|
||||||
|
isBlocking={true}
|
||||||
|
isFullscreenScroll={true}
|
||||||
|
isOpen={true}
|
||||||
|
onClose={[MockFunction]}
|
||||||
|
title=""
|
||||||
|
>
|
||||||
|
<ModalDialog.Header>
|
||||||
|
<Component>
|
||||||
|
<h3
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
Thank you for your interest!
|
||||||
|
</h3>
|
||||||
|
</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>
|
||||||
|
`;
|
||||||
18
src/components/ModalView/hooks.js
Normal file
18
src/components/ModalView/hooks.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { StrictDict } from 'utils';
|
||||||
|
|
||||||
|
import * as module from './hooks';
|
||||||
|
|
||||||
|
export const state = StrictDict({
|
||||||
|
isRecommendationsModalOpen: (val) => React.useState(val), // eslint-disable-line
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useRecommendationsModal = () => {
|
||||||
|
const [isRecommendationsModalOpen, setIsRecommendationsModalOpen] = module.state.isRecommendationsModalOpen(false);
|
||||||
|
const toggleRecommendationsModal = () => setIsRecommendationsModalOpen(!isRecommendationsModalOpen);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRecommendationsModalOpen,
|
||||||
|
toggleRecommendationsModal,
|
||||||
|
};
|
||||||
|
};
|
||||||
21
src/components/ModalView/hooks.test.js
Normal file
21
src/components/ModalView/hooks.test.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { MockUseState } from 'testUtils';
|
||||||
|
|
||||||
|
import * as hooks from './hooks';
|
||||||
|
|
||||||
|
const state = new MockUseState(hooks);
|
||||||
|
|
||||||
|
const {
|
||||||
|
useRecommendationsModal,
|
||||||
|
} = hooks;
|
||||||
|
|
||||||
|
describe('LearnerDashboardHeader hooks', () => {
|
||||||
|
describe('useRecommendationsModal', () => {
|
||||||
|
test('default state', () => {
|
||||||
|
state.mock();
|
||||||
|
const out = useRecommendationsModal();
|
||||||
|
state.expectInitializedWith(state.keys.isRecommendationsModalOpen, false);
|
||||||
|
out.toggleRecommendationsModal();
|
||||||
|
expect(state.values.isRecommendationsModalOpen).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
70
src/components/ModalView/index.jsx
Normal file
70
src/components/ModalView/index.jsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { ModalDialog, ActionRow } from '@edx/paragon';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
import './index.scss';
|
||||||
|
import {
|
||||||
|
trackPaintedDoorRecommendationHomeInterestBtnClicked,
|
||||||
|
trackPaintedDoorRecommendationHomeSkipBtnClicked,
|
||||||
|
} from '../../widgets/RecommendationsPanel/recsPaintedDoorExpTrack';
|
||||||
|
|
||||||
|
export const ModalView = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
variation,
|
||||||
|
}) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const handleSkipBtnClick = () => trackPaintedDoorRecommendationHomeSkipBtnClicked(variation);
|
||||||
|
const handleInterestBtnClick = () => trackPaintedDoorRecommendationHomeInterestBtnClicked(variation);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="containers">
|
||||||
|
<ModalDialog
|
||||||
|
title=""
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
hasCloseButton={false}
|
||||||
|
isFullscreenScroll
|
||||||
|
isBlocking
|
||||||
|
>
|
||||||
|
<ModalDialog.Header>
|
||||||
|
<ModalDialog.Title>
|
||||||
|
<h3 className="mt-2">{formatMessage(messages.recommendationsModalHeading)}</h3>
|
||||||
|
</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;
|
||||||
13
src/components/ModalView/index.scss
Normal file
13
src/components/ModalView/index.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@import "@edx/paragon/scss/core/core";
|
||||||
|
|
||||||
|
@media (max-width: 464px) {
|
||||||
|
.pgn__action-row{
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
16
src/components/ModalView/index.test.jsx
Normal file
16
src/components/ModalView/index.test.jsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import ModalView from '.';
|
||||||
|
|
||||||
|
describe('ModalView', () => {
|
||||||
|
const props = {
|
||||||
|
isOpen: true,
|
||||||
|
onClose: jest.fn(),
|
||||||
|
};
|
||||||
|
describe('snapshot', () => {
|
||||||
|
test('should renders default ModalView', () => {
|
||||||
|
const wrapper = shallow(<ModalView {...props} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
31
src/components/ModalView/messages.js
Normal file
31
src/components/ModalView/messages.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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!',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -12,7 +12,13 @@ import { reduxHooks } from 'hooks';
|
|||||||
|
|
||||||
import { findCoursesNavDropdownClicked } from '../hooks';
|
import { findCoursesNavDropdownClicked } from '../hooks';
|
||||||
|
|
||||||
|
import ModalView from '../../../components/ModalView';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
|
||||||
|
import usePaintedDoorExperimentContext from '../../../RecsPaintedDoorExpContext';
|
||||||
|
import {
|
||||||
|
trackPaintedDoorRecommendationHomeBtnClicked,
|
||||||
|
} from '../../../widgets/RecommendationsPanel/recsPaintedDoorExpTrack';
|
||||||
|
|
||||||
export const CollapseMenuBody = ({ isOpen }) => {
|
export const CollapseMenuBody = ({ isOpen }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@@ -20,8 +26,19 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
|||||||
|
|
||||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||||
|
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
|
||||||
|
|
||||||
const exploreCoursesClick = findCoursesNavDropdownClicked(urls.baseAppUrl(courseSearchUrl));
|
const exploreCoursesClick = findCoursesNavDropdownClicked(urls.baseAppUrl(courseSearchUrl));
|
||||||
|
const {
|
||||||
|
experimentVariation,
|
||||||
|
isPaintedDoorNavbarBtnVariation,
|
||||||
|
experimentLoading,
|
||||||
|
} = usePaintedDoorExperimentContext();
|
||||||
|
|
||||||
|
const handleSeeAllRecommendationsClick = () => {
|
||||||
|
toggleRecommendationsModal();
|
||||||
|
trackPaintedDoorRecommendationHomeBtnClicked(experimentVariation);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isOpen && (
|
isOpen && (
|
||||||
@@ -40,6 +57,14 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
|||||||
>
|
>
|
||||||
{formatMessage(messages.discoverNew)}
|
{formatMessage(messages.discoverNew)}
|
||||||
</Button>
|
</Button>
|
||||||
|
{(!experimentLoading && isPaintedDoorNavbarBtnVariation) && (
|
||||||
|
<Button
|
||||||
|
variant="inverse-primary"
|
||||||
|
onClick={handleSeeAllRecommendationsClick}
|
||||||
|
>
|
||||||
|
{formatMessage(messages.recommendedForYou)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
|
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
|
||||||
{formatMessage(messages.help)}
|
{formatMessage(messages.help)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -92,6 +117,11 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<ModalView
|
||||||
|
isOpen={isRecommendationsModalOpen}
|
||||||
|
onClose={toggleRecommendationsModal}
|
||||||
|
variation={experimentVariation}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import { AppContext } from '@edx/frontend-platform/react';
|
|||||||
|
|
||||||
import CollapseMenuBody from './CollapseMenuBody';
|
import CollapseMenuBody from './CollapseMenuBody';
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform', () => ({
|
||||||
|
getConfig: jest.fn(() => ({
|
||||||
|
ORDER_HISTORY_URL: 'http://account-profile-url.test',
|
||||||
|
CAREER_LINK_URL: 'http://account-profile-url.test',
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/react', () => ({
|
jest.mock('@edx/frontend-platform/react', () => ({
|
||||||
AppContext: {
|
AppContext: {
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
@@ -15,6 +22,7 @@ jest.mock('hooks', () => ({
|
|||||||
reduxHooks: {
|
reduxHooks: {
|
||||||
useEnterpriseDashboardData: () => ({
|
useEnterpriseDashboardData: () => ({
|
||||||
url: 'url',
|
url: 'url',
|
||||||
|
dashboard: false,
|
||||||
}),
|
}),
|
||||||
usePlatformSettingsData: () => ({
|
usePlatformSettingsData: () => ({
|
||||||
courseSearchUrl: '/courseSearchUrl',
|
courseSearchUrl: '/courseSearchUrl',
|
||||||
@@ -22,6 +30,13 @@ jest.mock('hooks', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../components/ModalView/hooks', () => ({
|
||||||
|
useRecommendationsModal: jest.fn(() => ({
|
||||||
|
isRecommendationsModalOpen: false,
|
||||||
|
toggleRecommendationsModal: jest.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('../hooks', () => ({
|
jest.mock('../hooks', () => ({
|
||||||
findCoursesNavDropdownClicked: (url) => jest.fn().mockName(`findCoursesNavDropdownClicked("${url}")`),
|
findCoursesNavDropdownClicked: (url) => jest.fn().mockName(`findCoursesNavDropdownClicked("${url}")`),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -13,22 +13,21 @@ exports[`CollapseMenuBody render 1`] = `
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/dashboard/programs"
|
href="undefined/dashboard/programs"
|
||||||
variant="inverse-primary"
|
variant="inverse-primary"
|
||||||
>
|
>
|
||||||
Programs
|
Programs
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/courseSearchUrl"
|
href="undefined/courseSearchUrl"
|
||||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
onClick={[MockFunction findCoursesNavDropdownClicked("undefined/courseSearchUrl")]}
|
||||||
variant="inverse-primary"
|
variant="inverse-primary"
|
||||||
>
|
>
|
||||||
Discover New
|
Discover New
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/support"
|
|
||||||
variant="inverse-primary"
|
variant="inverse-primary"
|
||||||
>
|
>
|
||||||
Help
|
Help
|
||||||
@@ -42,25 +41,35 @@ exports[`CollapseMenuBody render 1`] = `
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/u/username"
|
href="undefined/u/username"
|
||||||
variant="inverse-primary"
|
variant="inverse-primary"
|
||||||
>
|
>
|
||||||
Profile
|
Profile
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/account/settings"
|
href="undefined/account/settings"
|
||||||
variant="inverse-primary"
|
variant="inverse-primary"
|
||||||
>
|
>
|
||||||
Account
|
Account
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/logout"
|
href="http://account-profile-url.test"
|
||||||
|
variant="inverse-primary"
|
||||||
|
>
|
||||||
|
Orders & Subscriptions
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
as="a"
|
||||||
variant="inverse-primary"
|
variant="inverse-primary"
|
||||||
>
|
>
|
||||||
Sign Out
|
Sign Out
|
||||||
</Button>
|
</Button>
|
||||||
|
<ModalView
|
||||||
|
isOpen={false}
|
||||||
|
onClose={[MockFunction]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -79,25 +88,28 @@ exports[`CollapseMenuBody render unauthenticated 1`] = `
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/dashboard/programs"
|
href="undefined/dashboard/programs"
|
||||||
variant="inverse-primary"
|
variant="inverse-primary"
|
||||||
>
|
>
|
||||||
Programs
|
Programs
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/courseSearchUrl"
|
href="undefined/courseSearchUrl"
|
||||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
onClick={[MockFunction findCoursesNavDropdownClicked("undefined/courseSearchUrl")]}
|
||||||
variant="inverse-primary"
|
variant="inverse-primary"
|
||||||
>
|
>
|
||||||
Discover New
|
Discover New
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/support"
|
|
||||||
variant="inverse-primary"
|
variant="inverse-primary"
|
||||||
>
|
>
|
||||||
Help
|
Help
|
||||||
</Button>
|
</Button>
|
||||||
|
<ModalView
|
||||||
|
isOpen={false}
|
||||||
|
onClose={[MockFunction]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export const CollapsedHeader = () => {
|
|||||||
/>
|
/>
|
||||||
<BrandLogo />
|
<BrandLogo />
|
||||||
</header>
|
</header>
|
||||||
<CollapseMenuBody isOpen={isOpen} />
|
<CollapseMenuBody
|
||||||
|
isOpen={isOpen}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ exports[`ExpandedHeader render 1`] = `
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<AuthenticatedUserDropdown />
|
<AuthenticatedUserDropdown />
|
||||||
|
<ModalView
|
||||||
|
isOpen={false}
|
||||||
|
onClose={[MockFunction]}
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,32 @@ import { reduxHooks } from 'hooks';
|
|||||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||||
|
|
||||||
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
|
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
|
||||||
|
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
import ModalView from '../../../components/ModalView';
|
||||||
import BrandLogo from '../BrandLogo';
|
import BrandLogo from '../BrandLogo';
|
||||||
|
import usePaintedDoorExperimentContext from '../../../RecsPaintedDoorExpContext';
|
||||||
|
import {
|
||||||
|
trackPaintedDoorRecommendationHomeBtnClicked,
|
||||||
|
} from '../../../widgets/RecommendationsPanel/recsPaintedDoorExpTrack';
|
||||||
|
|
||||||
export const ExpandedHeader = () => {
|
export const ExpandedHeader = () => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||||
const isCollapsed = useIsCollapsed();
|
const isCollapsed = useIsCollapsed();
|
||||||
|
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
|
||||||
|
|
||||||
const exploreCoursesClick = findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
|
const exploreCoursesClick = findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
|
||||||
|
const {
|
||||||
|
experimentVariation,
|
||||||
|
isPaintedDoorNavbarBtnVariation,
|
||||||
|
experimentLoading,
|
||||||
|
} = usePaintedDoorExperimentContext();
|
||||||
|
|
||||||
|
const handleSeeAllRecommendationsClick = () => {
|
||||||
|
toggleRecommendationsModal();
|
||||||
|
trackPaintedDoorRecommendationHomeBtnClicked(experimentVariation);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!isCollapsed && (
|
!isCollapsed && (
|
||||||
@@ -51,6 +68,15 @@ export const ExpandedHeader = () => {
|
|||||||
>
|
>
|
||||||
{formatMessage(messages.discoverNew)}
|
{formatMessage(messages.discoverNew)}
|
||||||
</Button>
|
</Button>
|
||||||
|
{(!experimentLoading && isPaintedDoorNavbarBtnVariation) && (
|
||||||
|
<Button
|
||||||
|
variant="inverse-primary"
|
||||||
|
className="p-4"
|
||||||
|
onClick={handleSeeAllRecommendationsClick}
|
||||||
|
>
|
||||||
|
{formatMessage(messages.recommendedForYou)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<span className="flex-grow-1" />
|
<span className="flex-grow-1" />
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
@@ -63,6 +89,11 @@ export const ExpandedHeader = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AuthenticatedUserDropdown />
|
<AuthenticatedUserDropdown />
|
||||||
|
<ModalView
|
||||||
|
isOpen={isRecommendationsModalOpen}
|
||||||
|
onClose={toggleRecommendationsModal}
|
||||||
|
variation={experimentVariation}
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
|||||||
import ExpandedHeader from '.';
|
import ExpandedHeader from '.';
|
||||||
|
|
||||||
import { useIsCollapsed } from '../hooks';
|
import { useIsCollapsed } from '../hooks';
|
||||||
|
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
|
||||||
|
|
||||||
jest.mock('data/services/lms/urls', () => ({
|
jest.mock('data/services/lms/urls', () => ({
|
||||||
programsUrl: 'programsUrl',
|
programsUrl: 'programsUrl',
|
||||||
@@ -19,21 +20,34 @@ jest.mock('hooks', () => ({
|
|||||||
|
|
||||||
jest.mock('../hooks', () => ({
|
jest.mock('../hooks', () => ({
|
||||||
useIsCollapsed: jest.fn(),
|
useIsCollapsed: jest.fn(),
|
||||||
|
useRecommendationsModal: jest.fn(),
|
||||||
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
|
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../components/ModalView/hooks', () => ({
|
||||||
|
useRecommendationsModal: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
|
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
|
||||||
jest.mock('../BrandLogo', () => 'BrandLogo');
|
jest.mock('../BrandLogo', () => 'BrandLogo');
|
||||||
|
|
||||||
describe('ExpandedHeader', () => {
|
describe('ExpandedHeader', () => {
|
||||||
test('render', () => {
|
test('render', () => {
|
||||||
useIsCollapsed.mockReturnValueOnce(false);
|
useIsCollapsed.mockReturnValueOnce(false);
|
||||||
|
useRecommendationsModal.mockReturnValueOnce({
|
||||||
|
isRecommendationsModalOpen: false,
|
||||||
|
toggleRecommendationsModal: jest.fn(),
|
||||||
|
});
|
||||||
const wrapper = shallow(<ExpandedHeader />);
|
const wrapper = shallow(<ExpandedHeader />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('render empty if collapsed', () => {
|
test('render empty if collapsed', () => {
|
||||||
useIsCollapsed.mockReturnValueOnce(true);
|
useIsCollapsed.mockReturnValueOnce(true);
|
||||||
|
useRecommendationsModal.mockReturnValueOnce({
|
||||||
|
isRecommendationsModalOpen: false,
|
||||||
|
toggleRecommendationsModal: jest.fn(),
|
||||||
|
});
|
||||||
const wrapper = shallow(<ExpandedHeader />);
|
const wrapper = shallow(<ExpandedHeader />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
expect(wrapper.isEmptyRender()).toBe(true);
|
expect(wrapper.isEmptyRender()).toBe(true);
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Discover New',
|
defaultMessage: 'Discover New',
|
||||||
description: 'Header link for switching to discover page.',
|
description: 'Header link for switching to discover page.',
|
||||||
},
|
},
|
||||||
|
recommendedForYou: {
|
||||||
|
id: 'learnerVariantDashboard.recommendedForYou',
|
||||||
|
defaultMessage: 'Recommended for you',
|
||||||
|
description: 'Header link for recommended page.',
|
||||||
|
},
|
||||||
logoAltText: {
|
logoAltText: {
|
||||||
id: 'learnerVariantDashboard.logoAltText',
|
id: 'learnerVariantDashboard.logoAltText',
|
||||||
defaultMessage: 'edX, Inc. Dashboard',
|
defaultMessage: 'edX, Inc. Dashboard',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { reduxHooks } from 'hooks';
|
|||||||
import moreCoursesSVG from 'assets/more-courses-sidewidget.svg';
|
import moreCoursesSVG from 'assets/more-courses-sidewidget.svg';
|
||||||
import { baseAppUrl } from 'data/services/lms/urls';
|
import { baseAppUrl } from 'data/services/lms/urls';
|
||||||
|
|
||||||
import track from '../RecommendationsPanel/track';
|
import track, { trackRecommendationUnavailable } from '../RecommendationsPanel/track';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
@@ -17,6 +17,11 @@ export const arrowIcon = (<Icon className="mx-1" src={ArrowForward} />);
|
|||||||
export const LookingForChallengeWidget = () => {
|
export const LookingForChallengeWidget = () => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
trackRecommendationUnavailable();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card orientation="horizontal" id="looking-for-challenge-widget">
|
<Card orientation="horizontal" id="looking-for-challenge-widget">
|
||||||
<Card.ImageCap
|
<Card.ImageCap
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
import LookingForChallengeWidget from '.';
|
import LookingForChallengeWidget from '.';
|
||||||
|
import { trackRecommendationUnavailable } from '../RecommendationsPanel/track';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
reduxHooks: {
|
||||||
@@ -12,6 +14,7 @@ jest.mock('hooks', () => ({
|
|||||||
|
|
||||||
jest.mock('../RecommendationsPanel/track', () => ({
|
jest.mock('../RecommendationsPanel/track', () => ({
|
||||||
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
|
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
|
||||||
|
trackRecommendationUnavailable: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('LookingForChallengeWidget', () => {
|
describe('LookingForChallengeWidget', () => {
|
||||||
@@ -21,4 +24,10 @@ describe('LookingForChallengeWidget', () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
test('test recommendations unavailable event is fired', () => {
|
||||||
|
shallow(<LookingForChallengeWidget />);
|
||||||
|
const [cb] = React.useEffect.mock.calls[0];
|
||||||
|
cb();
|
||||||
|
expect(trackRecommendationUnavailable).toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ import { reduxHooks } from 'hooks';
|
|||||||
import track from './track';
|
import track from './track';
|
||||||
import CourseCard from './components/CourseCard';
|
import CourseCard from './components/CourseCard';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import ModalView from '../../components/ModalView';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
import usePaintedDoorExperimentContext from '../../RecsPaintedDoorExpContext';
|
||||||
|
import { useRecommendationsModal } from '../../components/ModalView/hooks';
|
||||||
|
import { trackPaintedDoorRecommendationHomeBtnClicked } from './recsPaintedDoorExpTrack';
|
||||||
|
|
||||||
export const LoadedView = ({
|
export const LoadedView = ({
|
||||||
courses,
|
courses,
|
||||||
@@ -19,8 +23,21 @@ export const LoadedView = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||||
|
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
|
||||||
|
|
||||||
|
const {
|
||||||
|
experimentVariation,
|
||||||
|
isPaintedDoorWidgetBtnVariation,
|
||||||
|
experimentLoading,
|
||||||
|
} = usePaintedDoorExperimentContext();
|
||||||
|
|
||||||
|
const handleSeeAllRecommendationsClick = () => {
|
||||||
|
toggleRecommendationsModal();
|
||||||
|
trackPaintedDoorRecommendationHomeBtnClicked(experimentVariation);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="p-4 w-100 panel-background">
|
<div className="p-4 w-100 panel-background">
|
||||||
<h3 className="pb-2">{isControl === false
|
<h3 className="pb-2">{isControl === false
|
||||||
? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)}
|
? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)}
|
||||||
@@ -35,6 +52,14 @@ export const LoadedView = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center explore-courses-btn">
|
<div className="text-center explore-courses-btn">
|
||||||
|
{!experimentLoading && isPaintedDoorWidgetBtnVariation ? (
|
||||||
|
<Button
|
||||||
|
variant="brand"
|
||||||
|
onClick={handleSeeAllRecommendationsClick}
|
||||||
|
>
|
||||||
|
{formatMessage(messages.seeAllRecommendationsButton)}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
iconBefore={Search}
|
iconBefore={Search}
|
||||||
@@ -44,8 +69,15 @@ export const LoadedView = ({
|
|||||||
>
|
>
|
||||||
{formatMessage(messages.exploreCoursesButton)}
|
{formatMessage(messages.exploreCoursesButton)}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ModalView
|
||||||
|
isOpen={isRecommendationsModalOpen}
|
||||||
|
onClose={toggleRecommendationsModal}
|
||||||
|
variation={experimentVariation}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,19 @@ jest.mock('./track', () => ({
|
|||||||
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
|
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
|
||||||
}));
|
}));
|
||||||
jest.mock('./components/CourseCard', () => 'CourseCard');
|
jest.mock('./components/CourseCard', () => 'CourseCard');
|
||||||
|
jest.mock('../../components/ModalView/hooks', () => ({
|
||||||
|
useRecommendationsModal: jest.fn(() => ({
|
||||||
|
isRecommendationsModalOpen: false,
|
||||||
|
toggleRecommendationsModal: jest.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('RecommendationsPanel LoadedView', () => {
|
describe('RecommendationsPanel LoadedView', () => {
|
||||||
const props = {
|
const props = {
|
||||||
courses: mockData.courses,
|
courses: mockData.courses,
|
||||||
isControl: null,
|
isControl: null,
|
||||||
|
setIsRecommendationsModalOpen: () => {},
|
||||||
|
isRecommendationsModalOpen: false,
|
||||||
};
|
};
|
||||||
describe('snapshot', () => {
|
describe('snapshot', () => {
|
||||||
test('without personalize recommendation', () => {
|
test('without personalize recommendation', () => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`RecommendationsPanel LoadedView snapshot with personalize recommendation 1`] = `
|
exports[`RecommendationsPanel LoadedView snapshot with personalize recommendation 1`] = `
|
||||||
<div
|
<Fragment>
|
||||||
|
<div
|
||||||
className="p-4 w-100 panel-background"
|
className="p-4 w-100 panel-background"
|
||||||
>
|
>
|
||||||
<h3
|
<h3
|
||||||
className="pb-2"
|
className="pb-2"
|
||||||
>
|
>
|
||||||
@@ -72,13 +73,19 @@ exports[`RecommendationsPanel LoadedView snapshot with personalize recommendatio
|
|||||||
Explore courses
|
Explore courses
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ModalView
|
||||||
|
isOpen={false}
|
||||||
|
onClose={[MockFunction]}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`RecommendationsPanel LoadedView snapshot without personalize recommendation 1`] = `
|
exports[`RecommendationsPanel LoadedView snapshot without personalize recommendation 1`] = `
|
||||||
<div
|
<Fragment>
|
||||||
|
<div
|
||||||
className="p-4 w-100 panel-background"
|
className="p-4 w-100 panel-background"
|
||||||
>
|
>
|
||||||
<h3
|
<h3
|
||||||
className="pb-2"
|
className="pb-2"
|
||||||
>
|
>
|
||||||
@@ -147,5 +154,10 @@ exports[`RecommendationsPanel LoadedView snapshot without personalize recommenda
|
|||||||
Explore courses
|
Explore courses
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ModalView
|
||||||
|
isOpen={false}
|
||||||
|
onClose={[MockFunction]}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ export const RecommendationsPanel = () => {
|
|||||||
}
|
}
|
||||||
if (isLoaded && courses.length > 0) {
|
if (isLoaded && courses.length > 0) {
|
||||||
return (
|
return (
|
||||||
<LoadedView courses={courses} isControl={isControl} />
|
<LoadedView
|
||||||
|
courses={courses}
|
||||||
|
isControl={isControl}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isFailed) {
|
if (isFailed) {
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ import mockData from './mockData';
|
|||||||
import LoadedView from './LoadedView';
|
import LoadedView from './LoadedView';
|
||||||
import LoadingView from './LoadingView';
|
import LoadingView from './LoadingView';
|
||||||
import RecommendationsPanel from '.';
|
import RecommendationsPanel from '.';
|
||||||
|
import { useRecommendationsModal } from '../../components/ModalView/hooks';
|
||||||
|
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('./hooks', () => ({
|
||||||
useRecommendationPanelData: jest.fn(),
|
useRecommendationPanelData: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('../../components/ModalView/hooks', () => ({
|
||||||
|
useRecommendationsModal: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||||
jest.mock('./LoadingView', () => 'LoadingView');
|
jest.mock('./LoadingView', () => 'LoadingView');
|
||||||
jest.mock('./LoadedView', () => 'LoadedView');
|
jest.mock('./LoadedView', () => 'LoadedView');
|
||||||
@@ -21,6 +25,8 @@ describe('RecommendationsPanel snapshot', () => {
|
|||||||
const defaultLoadedViewProps = {
|
const defaultLoadedViewProps = {
|
||||||
courses: [],
|
courses: [],
|
||||||
isControl: false,
|
isControl: false,
|
||||||
|
setIsRecommendationsModalOpen: jest.fn(),
|
||||||
|
isRecommendationsModalOpen: false,
|
||||||
};
|
};
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
isFailed: false,
|
isFailed: false,
|
||||||
@@ -29,6 +35,10 @@ describe('RecommendationsPanel snapshot', () => {
|
|||||||
...defaultLoadedViewProps,
|
...defaultLoadedViewProps,
|
||||||
};
|
};
|
||||||
it('displays LoadingView if request is loading', () => {
|
it('displays LoadingView if request is loading', () => {
|
||||||
|
useRecommendationsModal.mockReturnValueOnce({
|
||||||
|
isRecommendationsModalOpen: false,
|
||||||
|
toggleRecommendationsModal: jest.fn(),
|
||||||
|
});
|
||||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
@@ -36,16 +46,23 @@ describe('RecommendationsPanel snapshot', () => {
|
|||||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(shallow(<LoadingView />));
|
expect(shallow(<RecommendationsPanel />)).toMatchObject(shallow(<LoadingView />));
|
||||||
});
|
});
|
||||||
it('displays LoadedView with courses if request is loaded', () => {
|
it('displays LoadedView with courses if request is loaded', () => {
|
||||||
|
useRecommendationsModal.mockReturnValueOnce({
|
||||||
|
isRecommendationsModalOpen: false,
|
||||||
|
toggleRecommendationsModal: jest.fn(),
|
||||||
|
});
|
||||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
courses,
|
courses,
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
});
|
});
|
||||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(
|
expect(JSON.stringify(shallow(<RecommendationsPanel />)))
|
||||||
shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />),
|
.toEqual(JSON.stringify(shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />)));
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('displays LookingForChallengeWidget if request is failed', () => {
|
it('displays LookingForChallengeWidget if request is failed', () => {
|
||||||
|
useRecommendationsModal.mockReturnValueOnce({
|
||||||
|
isRecommendationsModalOpen: false,
|
||||||
|
toggleRecommendationsModal: jest.fn(),
|
||||||
|
});
|
||||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
isFailed: true,
|
isFailed: true,
|
||||||
@@ -55,6 +72,10 @@ describe('RecommendationsPanel snapshot', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('defaults to LookingForChallengeWidget if no flags are true', () => {
|
it('defaults to LookingForChallengeWidget if no flags are true', () => {
|
||||||
|
useRecommendationsModal.mockReturnValueOnce({
|
||||||
|
isRecommendationsModalOpen: false,
|
||||||
|
toggleRecommendationsModal: jest.fn(),
|
||||||
|
});
|
||||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Explore courses',
|
defaultMessage: 'Explore courses',
|
||||||
description: 'Button to explore more courses on recommendations panel',
|
description: 'Button to explore more courses on recommendations panel',
|
||||||
},
|
},
|
||||||
|
seeAllRecommendationsButton: {
|
||||||
|
id: 'RecommendationsPanel.seeAllRecommendationsButton',
|
||||||
|
defaultMessage: 'See all recommendations',
|
||||||
|
description: 'Button to see all recommendations',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
35
src/widgets/RecommendationsPanel/recsPaintedDoorExpTrack.js
Normal file
35
src/widgets/RecommendationsPanel/recsPaintedDoorExpTrack.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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import track from 'tracking';
|
|||||||
|
|
||||||
export const eventNames = StrictDict({
|
export const eventNames = StrictDict({
|
||||||
recommendedCourseClicked: 'edx.bi.user.recommended.course.click',
|
recommendedCourseClicked: 'edx.bi.user.recommended.course.click',
|
||||||
|
recommendationsUnavailable: 'edx.bi.user.recommendations.unavailable',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const linkNames = StrictDict({
|
export const linkNames = StrictDict({
|
||||||
@@ -23,7 +24,15 @@ export const recommendedCourseClicked = (courseKey, isControl, href) => createLi
|
|||||||
href,
|
href,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const trackRecommendationUnavailable = () => {
|
||||||
|
createEventTracker(eventNames.recommendationsUnavailable, {
|
||||||
|
type: 'personalized',
|
||||||
|
page: 'dashboard',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
findCoursesWidgetClicked,
|
findCoursesWidgetClicked,
|
||||||
recommendedCourseClicked,
|
recommendedCourseClicked,
|
||||||
|
trackRecommendationUnavailable,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user