Compare commits
45 Commits
recommenda
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73ed886715 | ||
|
|
c41ce5d87b | ||
|
|
a98439635b | ||
|
|
141d4900ae | ||
|
|
f9eef109c1 | ||
|
|
ad25abc222 | ||
|
|
a4f14da17a | ||
|
|
81ce59eab7 | ||
|
|
d1bf6f9c91 | ||
|
|
5ca1e9dc1f | ||
|
|
b83f128f81 | ||
|
|
c2a20af9b8 | ||
|
|
8f2ed779ca | ||
|
|
0cedeb0809 | ||
|
|
1a51ac07a2 | ||
|
|
4b2d65c44c | ||
|
|
c44db75273 | ||
|
|
a98fd50788 | ||
|
|
fd57523b2e | ||
|
|
2cf6e5a23e | ||
|
|
3cdcc1fe61 | ||
|
|
82ff0d7ddb | ||
|
|
7375c8f27b | ||
|
|
1478956e34 | ||
|
|
0cf98c9b78 | ||
|
|
f049712430 | ||
|
|
c977de2df9 | ||
|
|
4b20c5bbdd | ||
|
|
0c1fa2f030 | ||
|
|
91117cce6a | ||
|
|
e6dba8bdc2 | ||
|
|
1d67ac5f24 | ||
|
|
60d2f22c50 | ||
|
|
5dc89d7404 | ||
|
|
0f24d3a52d | ||
|
|
fc885d02dc | ||
|
|
2e09d3632e | ||
|
|
d8cb46da60 | ||
|
|
199d6e7c60 | ||
|
|
64563d58f9 | ||
|
|
1e9a0a87b6 | ||
|
|
d42d0cdc59 | ||
|
|
8fef92d94d | ||
|
|
b41eee47c9 | ||
|
|
909f3f1f47 |
2
.env
2
.env
@@ -41,3 +41,5 @@ ACCOUNT_PROFILE_URL=''
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
|
||||
@@ -48,3 +48,4 @@ ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
|
||||
@@ -47,3 +47,5 @@ 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
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
||||
|
||||
2
Makefile
2
Makefile
@@ -10,7 +10,7 @@ i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
|
||||
39
README.md
39
README.md
@@ -21,6 +21,45 @@ Some guidelines for writing widgets:
|
||||
* You can load data from the redux store, but should not add or modify fields in that structure.
|
||||
* Network events should be managed in component hooks, though can use our `data/constants/requests:requestStates` for ease of tracking the request states.
|
||||
|
||||
## License
|
||||
|
||||
The code in this repository is licensed under the AGPLv3 unless otherwise
|
||||
noted.
|
||||
|
||||
Please see `LICENSE <LICENSE>`_ for details.
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you're having trouble, we have discussion forums at
|
||||
https://discuss.openedx.org where you can connect with others in the community.
|
||||
|
||||
Our real-time conversations are on Slack. You can request a `Slack
|
||||
invitation`_, then join our `community Slack workspace`_. Because this is a
|
||||
frontend repository, the best place to discuss it would be in the `#wg-frontend
|
||||
channel`_.
|
||||
|
||||
For anything non-trivial, the best path is to open an issue in this repository
|
||||
with as many details about the issue you are facing as you can provide.
|
||||
|
||||
https://github.com/openedx/frontend-app-learner-dashboard/issues
|
||||
|
||||
For more information about these options, see the `Getting Help`_ page.
|
||||
|
||||
.. _Slack invitation: https://openedx.org/slack
|
||||
.. _community Slack workspace: https://openedx.slack.com/
|
||||
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
||||
.. _Getting Help: https://openedx.org/community/connect
|
||||
|
||||
## Resources
|
||||
|
||||
* [Learner Home project info at the Open edX Wiki](https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3575906333/Learner+Home)
|
||||
|
||||
## The Open edX Code of Conduct
|
||||
|
||||
All community members are expected to follow the `Open edX Code of Conduct`_.
|
||||
|
||||
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
|
||||
8978
package-lock.json
generated
8978
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"semantic-release": "semantic-release",
|
||||
@@ -26,12 +26,12 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@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",
|
||||
@@ -80,19 +80,19 @@
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "12.8.27",
|
||||
"@edx/frontend-build": "13.0.1",
|
||||
"@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"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us" dir="ltr">
|
||||
<head>
|
||||
<title>Learner Dashboard | <%= process.env.SITE_NAME %></title>
|
||||
<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" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
66
src/App.jsx
66
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,12 @@ 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 { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
import './App.scss';
|
||||
|
||||
export const App = () => {
|
||||
@@ -41,8 +42,21 @@ export const App = () => {
|
||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
||||
const loadData = reduxHooks.useLoadData();
|
||||
|
||||
const optimizelyScript = () => {
|
||||
if (getConfig().OPTIMIZELY_URL) {
|
||||
return <script src={getConfig().OPTIMIZELY_URL} />;
|
||||
} if (getConfig().OPTIMIZELY_PROJECT_ID) {
|
||||
return (
|
||||
<script
|
||||
src={`${getConfig().MARKETING_SITE_BASE_URL}/optimizelyjs/${getConfig().OPTIMIZELY_PROJECT_ID}.js`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') {
|
||||
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
|
||||
window.loadEmptyData = () => {
|
||||
loadData({ ...fakeData.globalData, courses: [] });
|
||||
};
|
||||
@@ -60,12 +74,12 @@ export const App = () => {
|
||||
window.actions = actions;
|
||||
window.track = track;
|
||||
}
|
||||
if (process.env.HOTJAR_APP_ID) {
|
||||
if (getConfig().HOTJAR_APP_ID) {
|
||||
try {
|
||||
initializeHotjar({
|
||||
hotjarId: process.env.HOTJAR_APP_ID,
|
||||
hotjarVersion: process.env.HOTJAR_VERSION,
|
||||
hotjarDebug: !!process.env.HOTJAR_DEBUG,
|
||||
hotjarId: getConfig().HOTJAR_APP_ID,
|
||||
hotjarVersion: getConfig().HOTJAR_VERSION,
|
||||
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
@@ -73,28 +87,32 @@ export const App = () => {
|
||||
}
|
||||
}, [authenticatedUser, loadData]);
|
||||
return (
|
||||
<Router>
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages.pageTitle)}</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
{optimizelyScript()}
|
||||
</Helmet>
|
||||
<div>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
) : (
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
)}
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
) : (
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
)}
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<Footer logo={getConfig().LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Router>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
116
src/App.test.jsx
116
src/App.test.jsx
@@ -1,18 +1,16 @@
|
||||
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 { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
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 +23,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',
|
||||
@@ -39,76 +38,131 @@ jest.mock('hooks', () => ({
|
||||
}));
|
||||
jest.mock('data/store', () => 'data/store');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
const loadData = jest.fn();
|
||||
reduxHooks.useLoadData.mockReturnValue(loadData);
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
|
||||
const supportEmail = 'test-support-url';
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
||||
|
||||
describe('App router component', () => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
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', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
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('no network failure with optimizely url', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo, OPTIMIZELY_URL: 'fake.url' });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
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('no network failure with optimizely project id', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo, OPTIMIZELY_PROJECT_ID: 'fakeId' });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
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', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
|
||||
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
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', () => {
|
||||
beforeAll(() => {
|
||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
|
||||
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo });
|
||||
el = shallow(<App />);
|
||||
});
|
||||
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}
|
||||
@@ -9,28 +9,34 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</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}
|
||||
@@ -38,24 +44,98 @@ exports[`App router component component no network failure snapshot 1`] = `
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</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 no network failure with optimizely project id snapshot 1`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<script
|
||||
src="undefined/optimizelyjs/fakeId.js"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`App router component component no network failure with optimizely url snapshot 1`] = `
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<script
|
||||
src="fake.url"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`App router component component refresh failure snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<Fragment>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
@@ -63,22 +143,28 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</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>
|
||||
`;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
|
||||
export const EmailLink = ({ address }) => {
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MailtoLink to={address}>{address}</MailtoLink>
|
||||
);
|
||||
};
|
||||
EmailLink.defaultProps = { address: null };
|
||||
EmailLink.propTypes = { address: PropTypes.string };
|
||||
|
||||
export default EmailLink;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import EmailLink from './EmailLink';
|
||||
|
||||
describe('EmailLink', () => {
|
||||
it('renders null when no address is provided', () => {
|
||||
const wrapper = shallow(<EmailLink />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
it('renders a MailtoLink when an address is provided', () => {
|
||||
const wrapper = shallow(<EmailLink address="test@email.com" />);
|
||||
expect(wrapper.find('MailtoLink').length).toEqual(1);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmailLink renders a MailtoLink when an address is provided 1`] = `
|
||||
<MailtoLink
|
||||
to="test@email.com"
|
||||
>
|
||||
test@email.com
|
||||
</MailtoLink>
|
||||
`;
|
||||
|
||||
exports[`EmailLink renders null when no address is provided 1`] = `""`;
|
||||
@@ -16,6 +16,8 @@ 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,
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
@@ -26,17 +26,14 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
const emailLink = address => address && <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
const emailLink = address => <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
|
||||
if (certificate.isRestricted) {
|
||||
return (
|
||||
<Banner variant="danger">
|
||||
{formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) })}
|
||||
{ supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
|
||||
{isVerified && ' '}
|
||||
{isVerified && formatMessage(
|
||||
messages.certRefundContactBilling,
|
||||
{ billingEmail: emailLink(billingEmail) },
|
||||
)}
|
||||
{isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,10 +21,6 @@ jest.mock('components/Banner', () => 'Banner');
|
||||
|
||||
describe('CertificateBanner', () => {
|
||||
const props = { cardId: 'cardId' };
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValue({
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
});
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({
|
||||
minPassingGrade: 0.8,
|
||||
progressUrl: 'progressUrl',
|
||||
@@ -42,16 +38,19 @@ describe('CertificateBanner', () => {
|
||||
};
|
||||
const defaultCourseRun = { isArchived: false };
|
||||
const defaultGrade = { isPassing: false };
|
||||
const defaultPlatformSettings = {};
|
||||
const createWrapper = ({
|
||||
certificate = {},
|
||||
enrollment = {},
|
||||
grade = {},
|
||||
courseRun = {},
|
||||
platformSettings = {},
|
||||
}) => {
|
||||
reduxHooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
|
||||
reduxHooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings });
|
||||
return shallow(<CertificateBanner {...props} />);
|
||||
};
|
||||
/** TODO: Update tests to validate snapshots **/
|
||||
@@ -64,6 +63,28 @@ describe('CertificateBanner', () => {
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted with support email', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
platformSettings: {
|
||||
supportEmail: 'suport@email',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted with billing email', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
platformSettings: {
|
||||
billingEmail: 'billing@email',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted and verified', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
@@ -75,6 +96,49 @@ describe('CertificateBanner', () => {
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted and verified with support email', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
enrollment: {
|
||||
isVerified: true,
|
||||
},
|
||||
platformSettings: {
|
||||
supportEmail: 'suport@email',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted and verified with billing email', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
enrollment: {
|
||||
isVerified: true,
|
||||
},
|
||||
platformSettings: {
|
||||
billingEmail: 'billing@email',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted and verified with support and billing email', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
enrollment: {
|
||||
isVerified: true,
|
||||
},
|
||||
platformSettings: {
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is passing and is downloadable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: { isPassing: true },
|
||||
@@ -133,6 +197,10 @@ describe('CertificateBanner', () => {
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
platformSettings: {
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
},
|
||||
});
|
||||
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
|
||||
expect(bannerMessage).toEqual(messages.certRestricted.defaultMessage);
|
||||
@@ -146,6 +214,10 @@ describe('CertificateBanner', () => {
|
||||
enrollment: {
|
||||
isVerified: true,
|
||||
},
|
||||
platformSettings: {
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
},
|
||||
});
|
||||
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
|
||||
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
|
||||
|
||||
@@ -17,9 +17,11 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmailLink": <EmailLink
|
||||
address="test-support-email"
|
||||
/>,
|
||||
"supportEmailLink": <MailtoLink
|
||||
to="test-support-email"
|
||||
>
|
||||
test-support-email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -30,6 +32,21 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CreditBanner component render with error state with no email snapshot 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
<p
|
||||
className="credit-error-msg"
|
||||
>
|
||||
An error occurred with this transaction.
|
||||
</p>
|
||||
<ContentComponent
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CreditBanner component render with no error state snapshot 1`] = `
|
||||
<Banner>
|
||||
<ContentComponent
|
||||
|
||||
@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
import EmailLink from 'components/EmailLink';
|
||||
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
import hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -15,13 +15,14 @@ export const CreditBanner = ({ cardId }) => {
|
||||
if (hookData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ContentComponent, error, supportEmail } = hookData;
|
||||
const supportEmailLink = (<EmailLink address={supportEmail} />);
|
||||
const supportEmailLink = (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>);
|
||||
return (
|
||||
<Banner {...(error && { variant: 'danger' })}>
|
||||
{error && (
|
||||
<p className="credit-error-msg">
|
||||
{formatMessage(messages.error, { supportEmailLink })}
|
||||
{supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)}
|
||||
</p>
|
||||
)}
|
||||
<ContentComponent cardId={cardId} />
|
||||
|
||||
@@ -2,15 +2,13 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import EmailLink from 'components/EmailLink';
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
|
||||
import hooks from './hooks';
|
||||
import messages from './messages';
|
||||
import CreditBanner from '.';
|
||||
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
jest.mock('components/EmailLink', () => 'EmailLink');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useCreditBannerData: jest.fn(),
|
||||
@@ -54,7 +52,7 @@ describe('CreditBanner component', () => {
|
||||
it('includes credit-error-msg with support email link', () => {
|
||||
expect(el.find('.credit-error-msg').containsMatchingElement(
|
||||
formatMessage(messages.error, {
|
||||
supportEmailLink: (<EmailLink address={supportEmail} />),
|
||||
supportEmailLink: (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>),
|
||||
}),
|
||||
)).toEqual(true);
|
||||
});
|
||||
@@ -62,6 +60,25 @@ describe('CreditBanner component', () => {
|
||||
expect(el.find('ContentComponent').props().cardId).toEqual(cardId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with error state with no email', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useCreditBannerData.mockReturnValue({
|
||||
error: true,
|
||||
ContentComponent,
|
||||
});
|
||||
el = shallow(<CreditBanner cardId={cardId} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('includes credit-error-msg without support email link', () => {
|
||||
expect(el.find('.credit-error-msg').containsMatchingElement(
|
||||
formatMessage(messages.errorNoEmail),
|
||||
)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no error state', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useCreditBannerData.mockReturnValue({
|
||||
|
||||
@@ -6,6 +6,11 @@ export const messages = StrictDict({
|
||||
description: '',
|
||||
defaultMessage: 'An error occurred with this transaction. For help, contact {supportEmailLink}.',
|
||||
},
|
||||
errorNoEmail: {
|
||||
id: 'learner-dash.courseCard.banners.credit.errorNoEmail',
|
||||
description: '',
|
||||
defaultMessage: 'An error occurred with this transaction.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -21,20 +21,40 @@ exports[`CertificateBanner snapshot is restricted 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted and verified 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
|
||||
|
||||
If you would like a refund on your Certificate of Achievement, please contact us.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted and verified with billing email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
|
||||
|
||||
<format-message-function
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
|
||||
"description": "Restricted certificate warning message",
|
||||
"id": "learner-dash.courseCard.banners.certificateRestricted",
|
||||
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
|
||||
"description": "Message to learners to contact billing for certificate refunds",
|
||||
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmail": <MailtoLink
|
||||
to="suport@email"
|
||||
"billingEmail": <MailtoLink
|
||||
to="billing@email"
|
||||
>
|
||||
suport@email
|
||||
billing@email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
@@ -42,7 +62,7 @@ exports[`CertificateBanner snapshot is restricted 1`] = `
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted and verified 1`] = `
|
||||
exports[`CertificateBanner snapshot is restricted and verified with support and billing email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
@@ -86,6 +106,66 @@ exports[`CertificateBanner snapshot is restricted and verified 1`] = `
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted and verified with support email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
|
||||
"description": "Restricted certificate warning message",
|
||||
"id": "learner-dash.courseCard.banners.certificateRestricted",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmail": <MailtoLink
|
||||
to="suport@email"
|
||||
>
|
||||
suport@email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
If you would like a refund on your Certificate of Achievement, please contact us.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted with billing email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted with support email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
|
||||
"description": "Restricted certificate warning message",
|
||||
"id": "learner-dash.courseCard.banners.certificateRestricted",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmail": <MailtoLink
|
||||
to="suport@email"
|
||||
>
|
||||
suport@email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot not passing and audit 1`] = `
|
||||
<Banner>
|
||||
Grade required to pass the course: 0.8%
|
||||
|
||||
@@ -31,11 +31,21 @@ export const messages = StrictDict({
|
||||
description: 'Restricted certificate warning message',
|
||||
defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.',
|
||||
},
|
||||
certRestrictedNoEmail: {
|
||||
id: 'learner-dash.courseCard.banners.certificateRestrictedNoEmail',
|
||||
description: 'Restricted certificate warning message',
|
||||
defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.',
|
||||
},
|
||||
certRefundContactBilling: {
|
||||
id: 'learner-dash.courseCard.banners.certificateRefundContactBilling',
|
||||
description: 'Message to learners to contact billing for certificate refunds',
|
||||
defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}',
|
||||
},
|
||||
certRefundContactBillingNoEmail: {
|
||||
id: 'learner-dash.courseCard.banners.certificateRefundContactBillingNoEmail',
|
||||
description: 'Message to learners to contact billing for certificate refunds',
|
||||
defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact us.',
|
||||
},
|
||||
passingGrade: {
|
||||
id: 'learner-dash.courseCard.banners.passingGrade',
|
||||
description: 'Message to learners with minimum passing grade for the course',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -26,7 +26,7 @@ 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);
|
||||
@@ -58,7 +58,7 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
{formatMessage(messages.unenroll)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<SocialShareMenu cardId={cardId} />
|
||||
<SocialShareMenu cardId={cardId} emailSettings={emailSettings} />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
@@ -68,8 +68,8 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
/>
|
||||
{isEmailEnabled && (
|
||||
<EmailSettingsModal
|
||||
show={emailSettingsModal.isVisible}
|
||||
closeModal={emailSettingsModal.hide}
|
||||
show={emailSettings.isVisible}
|
||||
closeModal={emailSettings.hide}
|
||||
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', () => {
|
||||
|
||||
@@ -68,7 +68,7 @@ export const CourseFilterControls = ({
|
||||
onClose={close}
|
||||
>
|
||||
<div className="p-1 mr-3">
|
||||
<b>Refine</b>
|
||||
<b>{formatMessage(messages.refine)}</b>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="filter-form-row">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export const messages = StrictDict({
|
||||
const messages = defineMessages({
|
||||
inProgress: {
|
||||
id: 'learner-dash.courseListFilters.inProgress',
|
||||
description: 'in-progress filter checkbox label for course list filters',
|
||||
@@ -52,4 +52,5 @@ export const messages = StrictDict({
|
||||
defaultMessage: 'Refine',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
export const BrandLogo = () => {
|
||||
@@ -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={getConfig().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 }) => {
|
||||
@@ -29,7 +29,7 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
||||
<Button as="a" href="/" variant="inverse-primary">
|
||||
{formatMessage(messages.course)}
|
||||
</Button>
|
||||
<Button as="a" href={urls.programsUrl} variant="inverse-primary">
|
||||
<Button as="a" href={urls.programsUrl()} variant="inverse-primary">
|
||||
{formatMessage(messages.program)}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -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
|
||||
|
||||
@@ -29,16 +29,21 @@ export const AuthenticatedUserDropdown = () => {
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
<Dropdown.Header>SWITCH DASHBOARD</Dropdown.Header>
|
||||
<Dropdown.Item as="a" href="/edx-dashboard" className="active">
|
||||
Personal
|
||||
</Dropdown.Item>
|
||||
{!!dashboard && (
|
||||
<Dropdown.Item as="a" href={dashboard.url} key={dashboard.label}>
|
||||
{dashboard.label} {formatMessage(messages.dashboard)}
|
||||
</Dropdown.Item>
|
||||
{ getConfig().ENABLE_EDX_PERSONAL_DASHBOARD && (
|
||||
<>
|
||||
<Dropdown.Header>{formatMessage(messages.dashboardSwitch)}</Dropdown.Header>
|
||||
<Dropdown.Item as="a" href="/edx-dashboard" className="active">
|
||||
{formatMessage(messages.dashboardPersonal)}
|
||||
</Dropdown.Item>
|
||||
{!!dashboard && (
|
||||
<Dropdown.Item as="a" href={dashboard.url} key={dashboard.label}>
|
||||
{dashboard.label} {formatMessage(messages.dashboard)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Divider />
|
||||
</>
|
||||
)}
|
||||
<Dropdown.Divider />
|
||||
|
||||
{!dashboard && getConfig().CAREER_LINK_URL && (
|
||||
<Dropdown.Item href={`${getConfig().CAREER_LINK_URL}`}>
|
||||
{formatMessage(messages.career)}
|
||||
@@ -55,7 +60,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 />
|
||||
|
||||
@@ -46,6 +46,7 @@ const config = {
|
||||
SUPPORT_URL: 'http://localhost:18000/support',
|
||||
CAREER_LINK_URL: 'http://localhost:18000/career',
|
||||
LMS_BASE_URL: 'http:/localhost:18000',
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD: true,
|
||||
};
|
||||
getConfig.mockReturnValue(config);
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -36,7 +37,7 @@ export const ExpandedHeader = () => {
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={urls.programsUrl}
|
||||
href={urls.programsUrl()}
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
>
|
||||
@@ -51,6 +52,7 @@ export const ExpandedHeader = () => {
|
||||
>
|
||||
{formatMessage(messages.discoverNew)}
|
||||
</Button>
|
||||
<WidgetNavbar placement={EXPANDED_NAVBAR} />
|
||||
<span className="flex-grow-1" />
|
||||
<Button
|
||||
as="a"
|
||||
|
||||
@@ -5,7 +5,7 @@ import ExpandedHeader from '.';
|
||||
import { useIsCollapsed } from '../hooks';
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
programsUrl: 'programsUrl',
|
||||
programsUrl: () => 'programsUrl',
|
||||
baseAppUrl: url => (`http://localhost:18000${url}`),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -6,6 +6,16 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Dashboard',
|
||||
description: 'The text for the user menu Dashboard navigation link.',
|
||||
},
|
||||
dashboardPersonal: {
|
||||
id: 'learnerVariantDashboard.menu.dashboardPersonal.label',
|
||||
defaultMessage: 'Personal',
|
||||
description: 'Link to personal dashboard in user menu',
|
||||
},
|
||||
dashboardSwitch: {
|
||||
id: 'learnerVariantDashboard.menu.dashboardSwitch.label',
|
||||
defaultMessage: 'SWITCH DASHBOARD',
|
||||
description: 'Switch Dashboard header in the user menu',
|
||||
},
|
||||
help: {
|
||||
id: 'learnerVariantDashboard.help.label',
|
||||
defaultMessage: 'Help',
|
||||
@@ -26,10 +36,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>
|
||||
`;
|
||||
25
src/containers/WidgetContainers/AppWrapper/index.jsx
Normal file
25
src/containers/WidgetContainers/AppWrapper/index.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import PaintedDoorExperimentProvider from 'widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
|
||||
|
||||
export const AppWrapper = ({
|
||||
children,
|
||||
}) => {
|
||||
if (process.env.EXPERIMENT_08_23_VAN_PAINTED_DOOR) {
|
||||
return (
|
||||
<PaintedDoorExperimentProvider>
|
||||
{children}
|
||||
</PaintedDoorExperimentProvider>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
};
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -34,16 +34,16 @@ export const deleteEntitlementEnrollment = ({ uuid, isRefundable }) => client()
|
||||
);
|
||||
|
||||
export const updateEmailSettings = ({ courseId, enable }) => post(
|
||||
urls.updateEmailSettings,
|
||||
urls.updateEmailSettings(),
|
||||
{ [apiKeys.courseId]: courseId, ...(enable && enableEmailsAction) },
|
||||
);
|
||||
|
||||
export const unenrollFromCourse = ({ courseId }) => post(
|
||||
urls.courseUnenroll,
|
||||
urls.courseUnenroll(),
|
||||
{ [apiKeys.courseId]: courseId, ...unenrollmentAction },
|
||||
);
|
||||
|
||||
export const logEvent = ({ eventName, data, courseId }) => post(urls.event, {
|
||||
export const logEvent = ({ eventName, data, courseId }) => post(urls.event(), {
|
||||
courserun_key: courseId,
|
||||
event_type: eventName,
|
||||
page: window.location.href,
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('lms api methods', () => {
|
||||
expect(
|
||||
api.updateEmailSettings({ courseId, enable: false }),
|
||||
).toEqual(
|
||||
utils.post(urls.updateEmailSettings, { [apiKeys.courseId]: courseId }),
|
||||
utils.post(urls.updateEmailSettings(), { [apiKeys.courseId]: courseId }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -87,7 +87,7 @@ describe('lms api methods', () => {
|
||||
api.updateEmailSettings({ courseId, enable: true }),
|
||||
).toEqual(
|
||||
utils.post(
|
||||
urls.updateEmailSettings,
|
||||
urls.updateEmailSettings(),
|
||||
{ [apiKeys.courseId]: courseId, ...enableEmailsAction },
|
||||
),
|
||||
);
|
||||
@@ -100,7 +100,7 @@ describe('lms api methods', () => {
|
||||
api.unenrollFromCourse({ courseId }),
|
||||
).toEqual(
|
||||
utils.post(
|
||||
urls.courseUnenroll,
|
||||
urls.courseUnenroll(),
|
||||
{ [apiKeys.courseId]: courseId, ...unenrollmentAction },
|
||||
),
|
||||
);
|
||||
@@ -116,7 +116,7 @@ describe('lms api methods', () => {
|
||||
expect(
|
||||
api.logEvent({ courseId, eventName, data }),
|
||||
).toEqual(
|
||||
utils.post(urls.event, {
|
||||
utils.post(urls.event(), {
|
||||
courserun_key: courseId,
|
||||
event_type: eventName,
|
||||
page: href,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -10,9 +10,9 @@ export const getApiUrl = () => (`${getConfig().LMS_BASE_URL}/api`);
|
||||
|
||||
const getInitApiUrl = () => (`${getApiUrl()}/learner_home/init`);
|
||||
|
||||
const event = `${getBaseUrl()}/event`;
|
||||
const courseUnenroll = `${getBaseUrl()}/change_enrollment`;
|
||||
const updateEmailSettings = `${getApiUrl()}/change_email_settings`;
|
||||
const event = () => `${getBaseUrl()}/event`;
|
||||
const courseUnenroll = () => `${getBaseUrl()}/change_enrollment`;
|
||||
const updateEmailSettings = () => `${getApiUrl()}/change_email_settings`;
|
||||
const entitlementEnrollment = (uuid) => `${getApiUrl()}/entitlements/v1/entitlements/${uuid}/enrollments`;
|
||||
|
||||
// if url is null or absolute, return it as is
|
||||
@@ -22,7 +22,7 @@ export const baseAppUrl = (url) => updateUrl(getBaseUrl(), url);
|
||||
export const learningMfeUrl = (url) => updateUrl(getConfig().LEARNING_BASE_URL, url);
|
||||
|
||||
// static view url
|
||||
const programsUrl = baseAppUrl('/dashboard/programs');
|
||||
const programsUrl = () => baseAppUrl('/dashboard/programs');
|
||||
|
||||
export const creditPurchaseUrl = (courseId) => `${getEcommerceUrl()}/credit/checkout/${courseId}/`;
|
||||
export const creditRequestUrl = (providerId) => `${getApiUrl()}/credit/v1/providers/${providerId}/request/`;
|
||||
|
||||
@@ -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()));
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { Icon, Hyperlink } from '@edx/paragon';
|
||||
import { ChevronRight } from '@edx/paragon/icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { trackProductHeaderClicked } from '../optimizelyExperiment';
|
||||
import { recommendationsHeaderClicked } from '../track';
|
||||
import { executiveEducation, bootCamp } from '../constants';
|
||||
@@ -44,7 +45,7 @@ const ProductCardHeader = ({ courseType }) => {
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
const productTypeDetail = getProductTypeDetail(courseType);
|
||||
const headerUrl = `${process.env.MARKETING_SITE_BASE_URL}${productTypeDetail.url}`;
|
||||
const headerUrl = `${getConfig().MARKETING_SITE_BASE_URL}${productTypeDetail.url}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createInstance, setLogLevel } from '@optimizely/react-sdk';
|
||||
|
||||
const OPTIMIZELY_SDK_KEY = process.env.OPTIMIZELY_FULL_STACK_SDK_KEY;
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
const OPTIMIZELY_SDK_KEY = getConfig().OPTIMIZELY_FULL_STACK_SDK_KEY;
|
||||
|
||||
const configureClient = () => {
|
||||
setLogLevel('error');
|
||||
|
||||
@@ -6,6 +6,10 @@ jest.mock('@optimizely/react-sdk', () => ({
|
||||
setLogLevel: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({ OPTIMIZELY_FULL_STACK_SDK_KEY: 'SDK Key' })),
|
||||
}));
|
||||
|
||||
describe('optimizelyClient', () => {
|
||||
it('should configure an Optimizely client instance with the correct SDK key', () => {
|
||||
expect(optimizelyClient).toBeDefined();
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
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({
|
||||
experimentVariation: null,
|
||||
isPaintedDoorNavbarBtnVariation: null,
|
||||
isPaintedDoorWidgetBtnVariation: null,
|
||||
isPaintedDoorControlVariation: null,
|
||||
experimentLoading: null,
|
||||
});
|
||||
|
||||
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