Compare commits
13 Commits
open-relea
...
jodybailey
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1e4e2d414 | ||
|
|
f4c27f02ba | ||
|
|
55a647bb5b | ||
|
|
c3b98d954e | ||
|
|
91a694736a | ||
|
|
7fe3bf7ab8 | ||
|
|
68db9a9829 | ||
|
|
bdf3870808 | ||
|
|
69e7c71885 | ||
|
|
cd7650ab42 | ||
|
|
7bd3452dc3 | ||
|
|
be1e1bf7d9 | ||
|
|
807d9f70b8 |
3
.env
3
.env
@@ -40,6 +40,3 @@ ACCOUNT_SETTINGS_URL=''
|
||||
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
|
||||
|
||||
@@ -47,5 +47,3 @@ ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
|
||||
@@ -46,6 +46,3 @@ ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
|
||||
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
|
||||
|
||||
@@ -5,7 +5,6 @@ const config = createConfig('eslint', {
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'import/no-import-module-exports': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
},
|
||||
});
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -49,9 +49,9 @@ jobs:
|
||||
server_port: 465
|
||||
username: ${{ secrets.EDX_SMTP_USERNAME }}
|
||||
password: ${{ secrets.EDX_SMTP_PASSWORD }}
|
||||
subject: CI workflow failed in ${{github.repository}}
|
||||
subject: Upgrade python requirements workflow failed in ${{github.repository}}
|
||||
to: masters-grades@edx.org
|
||||
from: github-actions <github-actions@edx.org>
|
||||
body: CI workflow in ${{github.repository}} failed!
|
||||
body: Upgrade python requirements workflow in ${{github.repository}} failed!
|
||||
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
|
||||
}}"
|
||||
|
||||
5
Makefile
5
Makefile
@@ -10,7 +10,7 @@ i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
@@ -64,11 +64,10 @@ pull_translations:
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
|
||||
|
||||
$(intl_imports) paragon frontend-component-footer frontend-app-learner-dashboard
|
||||
$(intl_imports) frontend-component-footer frontend-app-learner-dashboard
|
||||
endif
|
||||
|
||||
# This target is used by CI.
|
||||
|
||||
39
README.md
39
README.md
@@ -21,45 +21,6 @@ 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.
|
||||
|
||||
10233
package-lock.json
generated
10233
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"semantic-release": "semantic-release",
|
||||
@@ -26,18 +26,16 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
|
||||
"@edx/browserslist-config": "^1.1.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",
|
||||
"@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",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.15",
|
||||
"@optimizely/react-sdk": "^2.9.2",
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@@ -54,18 +52,17 @@
|
||||
"history": "5.0.1",
|
||||
"html-react-parser": "^1.3.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest-when": "^3.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "7.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^5.20.9",
|
||||
"react-pdf": "^5.5.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-router-dom": "5.3.3",
|
||||
"react-share": "^4.4.0",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.1.1",
|
||||
@@ -80,19 +77,19 @@
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "13.0.1",
|
||||
"@edx/frontend-build": "12.8.27",
|
||||
"@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": "^17.0.2",
|
||||
"react-test-renderer": "^16.14.0",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"semantic-release": "^20.1.3"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<!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>
|
||||
|
||||
63
src/App.jsx
63
src/App.jsx
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -18,17 +19,14 @@ import {
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import ZendeskFab from 'components/ZendeskFab';
|
||||
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 = () => {
|
||||
@@ -42,21 +40,8 @@ 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 || getConfig().NODE_ENV === 'development') {
|
||||
if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') {
|
||||
window.loadEmptyData = () => {
|
||||
loadData({ ...fakeData.globalData, courses: [] });
|
||||
};
|
||||
@@ -74,12 +59,12 @@ export const App = () => {
|
||||
window.actions = actions;
|
||||
window.track = track;
|
||||
}
|
||||
if (getConfig().HOTJAR_APP_ID) {
|
||||
if (process.env.HOTJAR_APP_ID) {
|
||||
try {
|
||||
initializeHotjar({
|
||||
hotjarId: getConfig().HOTJAR_APP_ID,
|
||||
hotjarVersion: getConfig().HOTJAR_VERSION,
|
||||
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
|
||||
hotjarId: process.env.HOTJAR_APP_ID,
|
||||
hotjarVersion: process.env.HOTJAR_VERSION,
|
||||
hotjarDebug: !!process.env.HOTJAR_DEBUG,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
@@ -87,32 +72,24 @@ 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>
|
||||
<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} />
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
) : (<Dashboard />)}
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
120
src/App.test.jsx
120
src/App.test.jsx
@@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
||||
import { ExperimentProvider } from 'ExperimentContext';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -20,10 +21,6 @@ jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||
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',
|
||||
@@ -38,131 +35,76 @@ 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.snapshot).toMatchSnapshot(); });
|
||||
test('snapshot', () => { expect(el).toMatchSnapshot(); });
|
||||
it('displays title in helmet component', () => {
|
||||
const control = el.instance
|
||||
.findByType(Helmet)[0]
|
||||
.findByType('title')[0];
|
||||
expect(control.children[0].el).toEqual(formatMessage(messages.pageTitle));
|
||||
expect(el.find(Helmet).find('title').text()).toEqual(useIntl().formatMessage(messages.pageTitle));
|
||||
});
|
||||
it('displays learner dashboard header', () => {
|
||||
expect(el.instance.findByType(LearnerDashboardHeader).length).toEqual(1);
|
||||
expect(el.find(LearnerDashboardHeader).length).toEqual(1);
|
||||
});
|
||||
it('wraps the page in a browser router', () => {
|
||||
expect(el.find(Router)).toMatchObject(el);
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
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');
|
||||
expect(el.find(Footer).props().logo).toEqual(logo);
|
||||
});
|
||||
};
|
||||
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', () => {
|
||||
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);
|
||||
expect(el.find('main')).toMatchObject(shallow(
|
||||
<main><Dashboard /></main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
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', () => {
|
||||
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 }));
|
||||
expect(el.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
</main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
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', () => {
|
||||
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 }));
|
||||
expect(el.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
</main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import { StrictDict } from 'utils';
|
||||
import api from 'widgets/ProductRecommendations/api';
|
||||
import * as module from './ExperimentContext';
|
||||
|
||||
export const state = StrictDict({
|
||||
experiment: (val) => React.useState(val), // eslint-disable-line
|
||||
countryCode: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useCountryCode = (setCountryCode) => {
|
||||
React.useEffect(() => {
|
||||
api
|
||||
.fetchRecommendationsContext()
|
||||
.then((response) => {
|
||||
setCountryCode(response.data.countryCode);
|
||||
})
|
||||
.catch(() => {
|
||||
setCountryCode('');
|
||||
});
|
||||
/* eslint-disable */
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const ExperimentContext = React.createContext();
|
||||
|
||||
export const ExperimentProvider = ({ children }) => {
|
||||
const [countryCode, setCountryCode] = module.state.countryCode(null);
|
||||
const [experiment, setExperiment] = module.state.experiment({
|
||||
isExperimentActive: false,
|
||||
inRecommendationsVariant: true,
|
||||
});
|
||||
|
||||
module.useCountryCode(setCountryCode);
|
||||
const { width } = useWindowSize();
|
||||
const isMobile = width < breakpoints.small.minWidth;
|
||||
|
||||
const contextValue = React.useMemo(
|
||||
() => ({
|
||||
experiment,
|
||||
countryCode,
|
||||
setExperiment,
|
||||
setCountryCode,
|
||||
isMobile,
|
||||
}),
|
||||
[experiment, countryCode, setExperiment, setCountryCode, isMobile]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExperimentContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ExperimentContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useExperimentContext = () => React.useContext(ExperimentContext);
|
||||
|
||||
ExperimentProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default { useCountryCode, useExperimentContext };
|
||||
@@ -1,123 +0,0 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { useWindowSize } from '@edx/paragon';
|
||||
|
||||
import api from 'widgets/ProductRecommendations/api';
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import * as experiment from 'ExperimentContext';
|
||||
|
||||
const state = new MockUseState(experiment);
|
||||
|
||||
jest.unmock('react');
|
||||
jest.spyOn(React, 'useEffect').mockImplementation((cb, prereqs) => ({ useEffect: { cb, prereqs } }));
|
||||
|
||||
jest.mock('widgets/ProductRecommendations/api', () => ({
|
||||
fetchRecommendationsContext: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('experiments context', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('useCountryCode', () => {
|
||||
describe('behaviour', () => {
|
||||
describe('useEffect call', () => {
|
||||
let calls;
|
||||
let cb;
|
||||
const setCountryCode = jest.fn();
|
||||
const successfulFetch = { data: { countryCode: 'ZA' } };
|
||||
|
||||
beforeEach(() => {
|
||||
experiment.useCountryCode(setCountryCode);
|
||||
|
||||
({ calls } = React.useEffect.mock);
|
||||
[[cb]] = calls;
|
||||
});
|
||||
|
||||
it('calls useEffect once', () => {
|
||||
expect(calls.length).toEqual(1);
|
||||
});
|
||||
describe('successfull fetch', () => {
|
||||
it('sets the country code', async () => {
|
||||
let resolveFn;
|
||||
api.fetchRecommendationsContext.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveFn = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
cb();
|
||||
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
|
||||
expect(setCountryCode).not.toHaveBeenCalled();
|
||||
resolveFn(successfulFetch);
|
||||
await waitFor(() => {
|
||||
expect(setCountryCode).toHaveBeenCalledWith(successfulFetch.data.countryCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('unsuccessfull fetch', () => {
|
||||
it('sets the country code to an empty string', async () => {
|
||||
let rejectFn;
|
||||
api.fetchRecommendationsContext.mockReturnValueOnce(
|
||||
new Promise((resolve, reject) => {
|
||||
rejectFn = reject;
|
||||
}),
|
||||
);
|
||||
cb();
|
||||
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
|
||||
expect(setCountryCode).not.toHaveBeenCalled();
|
||||
rejectFn();
|
||||
await waitFor(() => {
|
||||
expect(setCountryCode).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ExperimentProvider', () => {
|
||||
const { ExperimentProvider } = experiment;
|
||||
|
||||
const TestComponent = () => {
|
||||
const {
|
||||
experiment: exp,
|
||||
setExperiment,
|
||||
countryCode,
|
||||
setCountryCode,
|
||||
isMobile,
|
||||
} = experiment.useExperimentContext();
|
||||
|
||||
expect(exp.isExperimentActive).toBeFalsy();
|
||||
expect(exp.inRecommendationsVariant).toBeTruthy();
|
||||
expect(countryCode).toBeNull();
|
||||
expect(isMobile).toBe(false);
|
||||
expect(setExperiment).toBeDefined();
|
||||
expect(setCountryCode).toBeDefined();
|
||||
|
||||
return (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
|
||||
it('allows access to child components with the context stateful values', () => {
|
||||
const countryCodeSpy = jest.spyOn(experiment, 'useCountryCode').mockImplementationOnce(() => {});
|
||||
useWindowSize.mockImplementationOnce(() => ({ width: 577, height: 943 }));
|
||||
|
||||
state.mock();
|
||||
|
||||
mount(
|
||||
<ExperimentProvider>
|
||||
<TestComponent />
|
||||
</ExperimentProvider>,
|
||||
);
|
||||
|
||||
expect(countryCodeSpy).toHaveBeenCalledWith(state.setState.countryCode);
|
||||
state.expectInitializedWith(state.keys.countryCode, null);
|
||||
state.expectInitializedWith(state.keys.experiment, { isExperimentActive: false, inRecommendationsVariant: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component component initialize failure snapshot 1`] = `
|
||||
<Fragment>
|
||||
<BrowserRouter>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
@@ -9,34 +9,28 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<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>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`App router component component no network failure snapshot 1`] = `
|
||||
<Fragment>
|
||||
<BrowserRouter>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
@@ -44,98 +38,22 @@ exports[`App router component component no network failure snapshot 1`] = `
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<ExperimentProvider>
|
||||
<Dashboard />
|
||||
</ExperimentProvider>
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Dashboard />
|
||||
</main>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</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>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`App router component component refresh failure snapshot 1`] = `
|
||||
<Fragment>
|
||||
<BrowserRouter>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
@@ -143,28 +61,22 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<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>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</Fragment>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
@@ -13,28 +13,18 @@ exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
"redux": "store",
|
||||
}
|
||||
}
|
||||
wrapWithRouter={true}
|
||||
>
|
||||
<NoticesWrapper>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<App />
|
||||
</PageWrap>
|
||||
}
|
||||
<Switch>
|
||||
<PageRoute
|
||||
path="/"
|
||||
>
|
||||
<App />
|
||||
</PageRoute>
|
||||
<Redirect
|
||||
to="/"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<Navigate
|
||||
replace={true}
|
||||
to="/"
|
||||
/>
|
||||
}
|
||||
path="*"
|
||||
/>
|
||||
</Routes>
|
||||
</Switch>
|
||||
</NoticesWrapper>
|
||||
</AppProvider>
|
||||
`;
|
||||
|
||||
17
src/components/EmailLink.jsx
Normal file
17
src/components/EmailLink.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
16
src/components/EmailLink.test.jsx
Normal file
16
src/components/EmailLink.test.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
11
src/components/__snapshots__/EmailLink.test.jsx.snap
Normal file
11
src/components/__snapshots__/EmailLink.test.jsx.snap
Normal file
@@ -0,0 +1,11 @@
|
||||
// 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,8 +16,6 @@ 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 = {};
|
||||
|
||||
@@ -12,13 +12,11 @@ import messages from './messages';
|
||||
export const BeginCourseButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||
const { disableBeginCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl + execEdTrackingParam,
|
||||
homeUrl,
|
||||
);
|
||||
return (
|
||||
<ActionButton
|
||||
|
||||
@@ -14,22 +14,17 @@ jest.mock('tracking', () => ({
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'home-url' })),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
let wrapper;
|
||||
const homeUrl = 'home-url';
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
);
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData();
|
||||
|
||||
describe('BeginCourseButton', () => {
|
||||
const props = {
|
||||
@@ -38,50 +33,27 @@ describe('BeginCourseButton', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('renders default button when learner has access to the course', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data with cardId', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads exec education path param', () => {
|
||||
test('disabled states', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads disabled states for begin action from action hooks', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
describe('disabled', () => {
|
||||
beforeEach(() => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should be disabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('enabled', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should be enabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
it('should track enter course clicked event on click, with exec ed param', () => {
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
homeUrl + execEdPath(props.cardId),
|
||||
));
|
||||
});
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,13 +12,11 @@ import messages from './messages';
|
||||
export const ResumeButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||
const { disableResumeCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
resumeUrl + execEdTrackingParam,
|
||||
resumeUrl,
|
||||
);
|
||||
return (
|
||||
<ActionButton
|
||||
|
||||
@@ -6,80 +6,52 @@ import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ResumeButton from './ResumeButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(() => ({ resumeUrl: 'resumeUrl' })),
|
||||
useTrackCourseEvent: (eventName, cardId, url) => jest
|
||||
.fn()
|
||||
.mockName(`useTrackCourseEvent('${eventName}', '${cardId}', '${url}')`),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: 'enterCourseClicked',
|
||||
},
|
||||
}));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
const resumeUrl = 'resume-url';
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
|
||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
);
|
||||
|
||||
let wrapper;
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData();
|
||||
|
||||
describe('ResumeButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data with cardId', () => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads exec education path param', () => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads disabled states for resume action from action hooks', () => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
describe('snapshot', () => {
|
||||
test('renders default button when learner has access to the course', () => {
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick).getMockName()).toContain(
|
||||
'useTrackCourseEvent',
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
resumeUrl,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
describe('disabled', () => {
|
||||
beforeEach(() => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should be disabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data based on cardId', () => {
|
||||
shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
|
||||
props.cardId,
|
||||
);
|
||||
});
|
||||
describe('enabled', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ResumeButton {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('should be enabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
it('should track enter course clicked event on click, with exec ed param', () => {
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
resumeUrl + execEdPath(props.cardId),
|
||||
));
|
||||
});
|
||||
test('disabled states', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Begin Course
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
|
||||
exports[`BeginCourseButton snapshot renders default button when learner has access to the course 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
@@ -29,7 +10,7 @@ exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||
"upgradeUrl": "home-url",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResumeButton snapshot disabled snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Resume
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`ResumeButton snapshot enabled snapshot 1`] = `
|
||||
exports[`ResumeButton snapshot renders default button when learner has access to the course 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
||||
},
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction useTrackCourseEvent('enterCourseClicked', 'cardId', 'resumeUrl')]}
|
||||
>
|
||||
Resume
|
||||
</ActionButton>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardActions snapshot show begin course button when verified and not entitlement and has started 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<BeginCourseButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show resume button when verified and not entitlement and has started 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<ResumeButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show select session button when not verified and entitlement 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<SelectSessionButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show upgrade button when not verified and not entitlement 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<UpgradeButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
<BeginCourseButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show view course button when not verified and entitlement and fulfilled 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<ViewCourseButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
@@ -13,27 +13,21 @@ import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
export const CourseCardActions = ({ cardId }) => {
|
||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const {
|
||||
isVerified,
|
||||
hasStarted,
|
||||
isExecEd2UCourse,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isVerified, hasStarted } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
||||
let PrimaryButton;
|
||||
if (isEntitlement) {
|
||||
PrimaryButton = isFulfilled ? ViewCourseButton : SelectSessionButton;
|
||||
} else if (isArchived) {
|
||||
PrimaryButton = ViewCourseButton;
|
||||
} else {
|
||||
PrimaryButton = hasStarted ? ResumeButton : BeginCourseButton;
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionRow data-test-id="CourseCardActions">
|
||||
{!(isEntitlement || isVerified || isExecEd2UCourse) && <UpgradeButton cardId={cardId} />}
|
||||
{isEntitlement && (isFulfilled
|
||||
? <ViewCourseButton cardId={cardId} />
|
||||
: <SelectSessionButton cardId={cardId} />
|
||||
)}
|
||||
{(isArchived && !isEntitlement) && (
|
||||
<ViewCourseButton cardId={cardId} />
|
||||
)}
|
||||
{!(isArchived || isEntitlement) && (hasStarted
|
||||
? <ResumeButton cardId={cardId} />
|
||||
: <BeginCourseButton cardId={cardId} />
|
||||
)}
|
||||
{!(isEntitlement || isVerified) && <UpgradeButton cardId={cardId} />}
|
||||
<PrimaryButton cardId={cardId} />
|
||||
</ActionRow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
import CourseCardActions from '.';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
@@ -15,7 +9,6 @@ jest.mock('hooks', () => ({
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -25,92 +18,87 @@ jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
|
||||
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
|
||||
jest.mock('./ResumeButton', () => 'ResumeButton');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const props = { cardId };
|
||||
|
||||
let el;
|
||||
describe('CourseCardActions', () => {
|
||||
const mockHooks = ({
|
||||
isEntitlement = false,
|
||||
isExecEd2UCourse = false,
|
||||
isFulfilled = false,
|
||||
isArchived = false,
|
||||
isVerified = false,
|
||||
hasStarted = false,
|
||||
isMasquerading = false,
|
||||
} = {}) => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
const createWrapper = ({
|
||||
isEntitlement, isFulfilled, isArchived, isVerified, hasStarted,
|
||||
}) => {
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted });
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isVerified, hasStarted });
|
||||
return shallow(<CourseCardActions {...props} />);
|
||||
};
|
||||
const render = () => {
|
||||
el = shallow(<CourseCardActions {...props} />);
|
||||
};
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
mockHooks();
|
||||
render();
|
||||
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
describe('snapshot', () => {
|
||||
test('show upgrade button when not verified and not entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show select session button when not verified and entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show begin course button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show resume button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: true,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show view course button when not verified and entitlement and fulfilled', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: true, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('Exec Ed course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isExecEd2UCourse: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
|
||||
describe('behavior', () => {
|
||||
it('show upgrade button when not verified and not entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper.find('UpgradeButton')).toHaveLength(1);
|
||||
});
|
||||
describe('entitlement course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isEntitlement: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
});
|
||||
it('renders ViewCourseButton if fulfilled', () => {
|
||||
mockHooks({ isEntitlement: true, isFulfilled: true });
|
||||
render();
|
||||
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
it('renders SelectSessionButton if not fulfilled', () => {
|
||||
mockHooks({ isEntitlement: true });
|
||||
render();
|
||||
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId);
|
||||
it('show select session button when not verified and entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper.find('SelectSessionButton')).toHaveLength(1);
|
||||
});
|
||||
describe('verified course', () => {
|
||||
it('does not render upgrade button', () => {
|
||||
mockHooks({ isVerified: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||
it('show begin course button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: false,
|
||||
});
|
||||
expect(wrapper.find('BeginCourseButton')).toHaveLength(1);
|
||||
});
|
||||
describe('not entielement, verified, or exec ed', () => {
|
||||
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
|
||||
mockHooks({ isArchived: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
|
||||
it('show resume button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: true,
|
||||
});
|
||||
describe('unstarted courses', () => {
|
||||
it('renders UpgradeButton and BeginCourseButton', () => {
|
||||
mockHooks();
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
expect(wrapper.find('ResumeButton')).toHaveLength(1);
|
||||
});
|
||||
it('show view course button when not verified and entitlement and fulfilled', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: true, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
describe('active courses (started, and not archived)', () => {
|
||||
it('renders UpgradeButton and ResumeButton', () => {
|
||||
mockHooks({ hasStarted: true });
|
||||
render();
|
||||
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||
expect(el.instance.findByType(ResumeButton)[0].props.cardId).toEqual(cardId);
|
||||
});
|
||||
expect(wrapper.find('ViewCourseButton')).toHaveLength(1);
|
||||
});
|
||||
it('show view course button when not verified and entitlement and fulfilled and archived', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: true, isArchived: true, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper.find('ViewCourseButton')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,14 +26,17 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
const emailLink = address => <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
const emailLink = address => address && <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
|
||||
if (certificate.isRestricted) {
|
||||
return (
|
||||
<Banner variant="danger">
|
||||
{ supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
|
||||
{formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) })}
|
||||
{isVerified && ' '}
|
||||
{isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))}
|
||||
{isVerified && formatMessage(
|
||||
messages.certRefundContactBilling,
|
||||
{ billingEmail: emailLink(billingEmail) },
|
||||
)}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ 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',
|
||||
@@ -38,19 +42,16 @@ 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 **/
|
||||
@@ -63,28 +64,6 @@ 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: {
|
||||
@@ -96,49 +75,6 @@ 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 },
|
||||
@@ -197,10 +133,6 @@ 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);
|
||||
@@ -214,10 +146,6 @@ 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,11 +17,9 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmailLink": <MailtoLink
|
||||
to="test-support-email"
|
||||
>
|
||||
test-support-email
|
||||
</MailtoLink>,
|
||||
"supportEmailLink": <EmailLink
|
||||
address="test-support-email"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
@@ -32,21 +30,6 @@ 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,14 +15,13 @@ export const CreditBanner = ({ cardId }) => {
|
||||
if (hookData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ContentComponent, error, supportEmail } = hookData;
|
||||
const supportEmailLink = (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>);
|
||||
const supportEmailLink = (<EmailLink address={supportEmail} />);
|
||||
return (
|
||||
<Banner {...(error && { variant: 'danger' })}>
|
||||
{error && (
|
||||
<p className="credit-error-msg">
|
||||
{supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)}
|
||||
{formatMessage(messages.error, { supportEmailLink })}
|
||||
</p>
|
||||
)}
|
||||
<ContentComponent cardId={cardId} />
|
||||
|
||||
@@ -2,13 +2,15 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
|
||||
import EmailLink from 'components/EmailLink';
|
||||
|
||||
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(),
|
||||
@@ -52,7 +54,7 @@ describe('CreditBanner component', () => {
|
||||
it('includes credit-error-msg with support email link', () => {
|
||||
expect(el.find('.credit-error-msg').containsMatchingElement(
|
||||
formatMessage(messages.error, {
|
||||
supportEmailLink: (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>),
|
||||
supportEmailLink: (<EmailLink address={supportEmail} />),
|
||||
}),
|
||||
)).toEqual(true);
|
||||
});
|
||||
@@ -60,25 +62,6 @@ 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,11 +6,6 @@ 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,48 +21,28 @@ 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.
|
||||
<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 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": "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 {
|
||||
"billingEmail": <MailtoLink
|
||||
to="billing@email"
|
||||
>
|
||||
billing@email
|
||||
</MailtoLink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot is restricted and verified with support and billing email 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
@@ -106,66 +86,6 @@ exports[`CertificateBanner snapshot is restricted and verified with support and
|
||||
</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,21 +31,11 @@ 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',
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as ReactShare from 'react-share';
|
||||
|
||||
import { StrictDict } from '@edx/react-unit-test-utils';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const testIds = StrictDict({
|
||||
emailSettingsModalToggle: 'emailSettingsModalToggle',
|
||||
});
|
||||
|
||||
export const SocialShareMenu = ({ cardId, emailSettings }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const { isEmailEnabled, isExecEd2UCourse } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
|
||||
const handleTwitterShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'twitter');
|
||||
const handleFacebookShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'facebook');
|
||||
|
||||
if (isExecEd2UCourse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isEmailEnabled && (
|
||||
<Dropdown.Item
|
||||
disabled={isMasquerading}
|
||||
onClick={emailSettings.show}
|
||||
data-testid={testIds.emailSettingsModalToggle}
|
||||
>
|
||||
{formatMessage(messages.emailSettings)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{facebook.isEnabled && (
|
||||
<ReactShare.FacebookShareButton
|
||||
url={facebook.shareUrl}
|
||||
onClick={handleFacebookShare}
|
||||
title={formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: facebook.socialBrand,
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
>
|
||||
{formatMessage(messages.shareToFacebook)}
|
||||
</ReactShare.FacebookShareButton>
|
||||
)}
|
||||
{twitter.isEnabled && (
|
||||
<ReactShare.TwitterShareButton
|
||||
url={twitter.shareUrl}
|
||||
onClick={handleTwitterShare}
|
||||
title={formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: twitter.socialBrand,
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
>
|
||||
{formatMessage(messages.shareToTwitter)}
|
||||
</ReactShare.TwitterShareButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
SocialShareMenu.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
emailSettings: PropTypes.shape({
|
||||
show: PropTypes.func,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default SocialShareMenu;
|
||||
@@ -1,235 +0,0 @@
|
||||
import { when } from 'jest-when';
|
||||
import * as ReactShare from 'react-share';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage, shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { useEmailSettings } from './hooks';
|
||||
import SocialShareMenu, { testIds } from './SocialShareMenu';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('react-share', () => ({
|
||||
FacebookShareButton: () => 'FacebookShareButton',
|
||||
TwitterShareButton: () => 'TwitterShareButton',
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
socialShare: 'test-social-share-key',
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
useIntl: jest.fn().mockReturnValue({
|
||||
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardSocialSettingsData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn((...args) => ({ trackCourseEvent: args })),
|
||||
},
|
||||
}));
|
||||
jest.mock('./hooks', () => ({
|
||||
useEmailSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
const props = {
|
||||
cardId: 'test-card-id',
|
||||
emailSettings: { show: jest.fn() },
|
||||
};
|
||||
|
||||
const mockHook = (fn, returnValue, options = {}) => {
|
||||
if (options.isCardHook) {
|
||||
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue);
|
||||
} else {
|
||||
when(fn).calledWith().mockReturnValueOnce(returnValue);
|
||||
}
|
||||
};
|
||||
|
||||
const courseName = 'test-course-name';
|
||||
|
||||
const socialShare = {
|
||||
facebook: {
|
||||
isEnabled: true,
|
||||
shareUrl: 'facebook-share-url',
|
||||
socialBrand: 'facebook-social-brand',
|
||||
},
|
||||
twitter: {
|
||||
isEnabled: true,
|
||||
shareUrl: 'twitter-share-url',
|
||||
socialBrand: 'twitter-social-brand',
|
||||
},
|
||||
};
|
||||
|
||||
const mockHooks = (returnVals = {}) => {
|
||||
mockHook(
|
||||
reduxHooks.useCardEnrollmentData,
|
||||
{
|
||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||
isExecEd2UCourse: !!returnVals.isExecEd2UCourse,
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
mockHook(reduxHooks.useCardCourseData, { courseName }, { isCardHook: true });
|
||||
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
|
||||
mockHook(
|
||||
reduxHooks.useCardSocialSettingsData,
|
||||
{
|
||||
facebook: { ...socialShare.facebook, isEnabled: !!returnVals.facebook?.isEnabled },
|
||||
twitter: { ...socialShare.twitter, isEnabled: !!returnVals.twitter?.isEnabled },
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
};
|
||||
|
||||
let el;
|
||||
const render = () => {
|
||||
el = shallow(<SocialShareMenu {...props} />);
|
||||
};
|
||||
|
||||
describe('SocialShareMenu', () => {
|
||||
describe('behavior', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks();
|
||||
render();
|
||||
});
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
});
|
||||
it('initializes local hooks', () => {
|
||||
when(useEmailSettings).expectCalledWith();
|
||||
});
|
||||
it('initializes redux hook data ', () => {
|
||||
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useCardCourseData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useCardSocialSettingsData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
||||
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter');
|
||||
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook');
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
it('renders null if exec ed course', () => {
|
||||
mockHooks({ isExecEd2UCourse: true });
|
||||
render();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
const testEmailSettingsDropdown = (isMasquerading = false) => {
|
||||
describe('email settings dropdown', () => {
|
||||
const loadToggle = () => el.instance.findByTestId(testIds.emailSettingsModalToggle)[0];
|
||||
it('renders', () => {
|
||||
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(1);
|
||||
});
|
||||
if (isMasquerading) {
|
||||
it('is disabled', () => {
|
||||
expect(loadToggle().props.disabled).toEqual(true);
|
||||
});
|
||||
} else {
|
||||
it('is enabled', () => {
|
||||
expect(loadToggle().props.disabled).toEqual(false);
|
||||
});
|
||||
}
|
||||
test('show email settings modal on click', () => {
|
||||
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
|
||||
});
|
||||
});
|
||||
};
|
||||
const testFacebookShareButton = () => {
|
||||
test('renders facebook share button with courseName and brand', () => {
|
||||
const button = el.instance.findByType(ReactShare.FacebookShareButton)[0];
|
||||
expect(button.props.url).toEqual(socialShare.facebook.shareUrl);
|
||||
expect(button.props.onClick).toEqual(
|
||||
reduxHooks.useTrackCourseEvent(track.socialShare, props.cardId, 'facebook'),
|
||||
);
|
||||
expect(button.props.title).toEqual(formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: socialShare.facebook.socialBrand,
|
||||
}));
|
||||
});
|
||||
};
|
||||
const testTwitterShareButton = () => {
|
||||
test('renders twitter share button with courseName and brand', () => {
|
||||
const button = el.instance.findByType(ReactShare.TwitterShareButton)[0];
|
||||
expect(button.props.url).toEqual(socialShare.twitter.shareUrl);
|
||||
expect(button.props.onClick).toEqual(
|
||||
reduxHooks.useTrackCourseEvent(track.socialShare, props.cardId, 'twitter'),
|
||||
);
|
||||
expect(button.props.title).toEqual(formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: socialShare.twitter.socialBrand,
|
||||
}));
|
||||
});
|
||||
};
|
||||
describe('all enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({
|
||||
facebook: { isEnabled: true },
|
||||
twitter: { isEnabled: true },
|
||||
isEmailEnabled: true,
|
||||
});
|
||||
render();
|
||||
});
|
||||
describe('email settings dropdown', () => {
|
||||
const loadToggle = () => el.instance.findByTestId(testIds.emailSettingsModalToggle)[0];
|
||||
it('renders', () => {
|
||||
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(1);
|
||||
});
|
||||
it('is enabled', () => {
|
||||
expect(loadToggle().props.disabled).toEqual(false);
|
||||
});
|
||||
test('show email settings modal on click', () => {
|
||||
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
|
||||
});
|
||||
});
|
||||
testEmailSettingsDropdown();
|
||||
testFacebookShareButton();
|
||||
testTwitterShareButton();
|
||||
});
|
||||
describe('only email enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ isEmailEnabled: true });
|
||||
render();
|
||||
});
|
||||
testEmailSettingsDropdown();
|
||||
it('does not render facebook or twitter controls', () => {
|
||||
expect(el.instance.findByType(ReactShare.FacebookShareButton).length).toEqual(0);
|
||||
expect(el.instance.findByType(ReactShare.TwitterShareButton).length).toEqual(0);
|
||||
});
|
||||
describe('masquerading', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ isEmailEnabled: true, isMasquerading: true });
|
||||
render();
|
||||
});
|
||||
testEmailSettingsDropdown(true);
|
||||
});
|
||||
});
|
||||
describe('only facebook enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ facebook: { isEnabled: true } });
|
||||
render();
|
||||
});
|
||||
testFacebookShareButton();
|
||||
it('does not render email or twitter controls', () => {
|
||||
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(0);
|
||||
expect(el.instance.findByType(ReactShare.TwitterShareButton).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe('only twitter enabled', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ twitter: { isEnabled: true } });
|
||||
render();
|
||||
});
|
||||
testTwitterShareButton();
|
||||
it('does not render email or facebook controls', () => {
|
||||
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(0);
|
||||
expect(el.instance.findByType(ReactShare.FacebookShareButton).length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardMenu render show dropdown hide unenroll item and disable email snapshot 1`] = `
|
||||
exports[`CourseCardMenu disable and stop rendering buttons snapshot when no dropdown items exist 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown
|
||||
onToggle={[MockFunction hooks.handleToggleDropdown]}
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
alt="Course actions dropdown"
|
||||
as="IconButton"
|
||||
iconAs="Icon"
|
||||
id="course-actions-dropdown-test-card-id"
|
||||
src={[MockFunction icons.MoreVert]}
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<SocialShareMenu
|
||||
cardId="test-card-id"
|
||||
emailSettings={
|
||||
Object {
|
||||
"hide": [MockFunction emailSettingHide],
|
||||
"isVisible": false,
|
||||
"show": [MockFunction emailSettingShow],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
cardId="test-card-id"
|
||||
closeModal={[MockFunction unenrollHide]}
|
||||
show={false}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseCardMenu render show dropdown show unenroll and enable email snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown
|
||||
onToggle={[MockFunction hooks.handleToggleDropdown]}
|
||||
>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
alt="Course actions dropdown"
|
||||
as="IconButton"
|
||||
@@ -55,16 +19,149 @@ exports[`CourseCardMenu render show dropdown show unenroll and enable email snap
|
||||
>
|
||||
Unenroll
|
||||
</Dropdown.Item>
|
||||
<SocialShareMenu
|
||||
cardId="test-card-id"
|
||||
emailSettings={
|
||||
Object {
|
||||
"hide": [MockFunction emailSettingHide],
|
||||
"isVisible": false,
|
||||
"show": [MockFunction emailSettingShow],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Dropdown.Item
|
||||
data-testid="emailSettingsModalToggle"
|
||||
disabled={false}
|
||||
onClick={[MockFunction emailSettingShow]}
|
||||
>
|
||||
Email settings
|
||||
</Dropdown.Item>
|
||||
<FacebookShareButton
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
onClick={[MockFunction facebookShareClick]}
|
||||
resetButtonStyle={false}
|
||||
title="I'm taking test-course-name online with facebook-social-brand. Check it out!"
|
||||
url="facebook-share-url"
|
||||
>
|
||||
Share to Facebook
|
||||
</FacebookShareButton>
|
||||
<TwitterShareButton
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
onClick={[MockFunction twitterShareClick]}
|
||||
resetButtonStyle={false}
|
||||
title="I'm taking test-course-name online with twitter-social-brand. Check it out!"
|
||||
url="twitter-share-url"
|
||||
>
|
||||
Share to Twitter
|
||||
</TwitterShareButton>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
cardId="test-card-id"
|
||||
closeModal={[MockFunction unenrollHide]}
|
||||
show={false}
|
||||
/>
|
||||
<EmailSettingsModal
|
||||
cardId="test-card-id"
|
||||
closeModal={[MockFunction emailSettingHide]}
|
||||
show={false}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
alt="Course actions dropdown"
|
||||
as="IconButton"
|
||||
iconAs="Icon"
|
||||
id="course-actions-dropdown-test-card-id"
|
||||
src={[MockFunction icons.MoreVert]}
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
data-testid="unenrollModalToggle"
|
||||
disabled={false}
|
||||
onClick={[MockFunction unenrollShow]}
|
||||
>
|
||||
Unenroll
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid="emailSettingsModalToggle"
|
||||
disabled={false}
|
||||
onClick={[MockFunction emailSettingShow]}
|
||||
>
|
||||
Email settings
|
||||
</Dropdown.Item>
|
||||
<FacebookShareButton
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
onClick={[MockFunction facebookShareClick]}
|
||||
resetButtonStyle={false}
|
||||
title="I'm taking test-course-name online with facebook-social-brand. Check it out!"
|
||||
url="facebook-share-url"
|
||||
>
|
||||
Share to Facebook
|
||||
</FacebookShareButton>
|
||||
<TwitterShareButton
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
onClick={[MockFunction twitterShareClick]}
|
||||
resetButtonStyle={false}
|
||||
title="I'm taking test-course-name online with twitter-social-brand. Check it out!"
|
||||
url="twitter-share-url"
|
||||
>
|
||||
Share to Twitter
|
||||
</TwitterShareButton>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
cardId="test-card-id"
|
||||
closeModal={[MockFunction unenrollHide]}
|
||||
show={false}
|
||||
/>
|
||||
<EmailSettingsModal
|
||||
cardId="test-card-id"
|
||||
closeModal={[MockFunction emailSettingHide]}
|
||||
show={false}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CourseCardMenu masquerading snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
alt="Course actions dropdown"
|
||||
as="IconButton"
|
||||
iconAs="Icon"
|
||||
id="course-actions-dropdown-test-card-id"
|
||||
src={[MockFunction icons.MoreVert]}
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
data-testid="unenrollModalToggle"
|
||||
disabled={true}
|
||||
onClick={[MockFunction unenrollShow]}
|
||||
>
|
||||
Unenroll
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid="emailSettingsModalToggle"
|
||||
disabled={true}
|
||||
onClick={[MockFunction emailSettingShow]}
|
||||
>
|
||||
Email settings
|
||||
</Dropdown.Item>
|
||||
<FacebookShareButton
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
onClick={[MockFunction facebookShareClick]}
|
||||
resetButtonStyle={false}
|
||||
title="I'm taking test-course-name online with facebook-social-brand. Check it out!"
|
||||
url="facebook-share-url"
|
||||
>
|
||||
Share to Facebook
|
||||
</FacebookShareButton>
|
||||
<TwitterShareButton
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
onClick={[MockFunction twitterShareClick]}
|
||||
resetButtonStyle={false}
|
||||
title="I'm taking test-course-name online with twitter-social-brand. Check it out!"
|
||||
url="twitter-share-url"
|
||||
>
|
||||
Share to Twitter
|
||||
</TwitterShareButton>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils';
|
||||
import React from 'react';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
export const stateKeys = StrictDict({
|
||||
isUnenrollConfirmVisible: 'isUnenrollConfirmVisible',
|
||||
isEmailSettingsVisible: 'isEmailSettingsVisible',
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
isUnenrollConfirmVisible: (val) => React.useState(val), // eslint-disable-line
|
||||
isEmailSettingsVisible: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useUnenrollData = () => {
|
||||
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isUnenrollConfirmVisible, false);
|
||||
const [isVisible, setIsVisible] = module.state.isUnenrollConfirmVisible(false);
|
||||
return {
|
||||
show: () => setIsVisible(true),
|
||||
hide: () => setIsVisible(false),
|
||||
@@ -18,7 +21,7 @@ export const useUnenrollData = () => {
|
||||
};
|
||||
|
||||
export const useEmailSettings = () => {
|
||||
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isEmailSettingsVisible, false);
|
||||
const [isVisible, setIsVisible] = module.state.isEmailSettingsVisible(false);
|
||||
return {
|
||||
show: () => setIsVisible(true),
|
||||
hide: () => setIsVisible(false),
|
||||
@@ -27,30 +30,9 @@ export const useEmailSettings = () => {
|
||||
};
|
||||
|
||||
export const useHandleToggleDropdown = (cardId) => {
|
||||
const trackCourseEvent = reduxHooks.useTrackCourseEvent(
|
||||
track.course.courseOptionsDropdownClicked,
|
||||
cardId,
|
||||
);
|
||||
const eventName = track.course.courseOptionsDropdownClicked;
|
||||
const trackCourseEvent = reduxHooks.useTrackCourseEvent(eventName, cardId);
|
||||
return (isOpen) => {
|
||||
if (isOpen) { trackCourseEvent(); }
|
||||
};
|
||||
};
|
||||
|
||||
export const useOptionVisibility = (cardId) => {
|
||||
const { isEnrolled, isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
|
||||
const { isEarned } = reduxHooks.useCardCertificateData(cardId);
|
||||
|
||||
const shouldShowUnenrollItem = isEnrolled && !isEarned;
|
||||
const shouldShowDropdown = (
|
||||
shouldShowUnenrollItem
|
||||
|| isEmailEnabled
|
||||
|| facebook.isEnabled
|
||||
|| twitter.isEnabled
|
||||
);
|
||||
|
||||
return {
|
||||
shouldShowUnenrollItem,
|
||||
shouldShowDropdown,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
|
||||
@@ -7,77 +6,71 @@ import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCertificateData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardSocialSettingsData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const trackCourseEvent = jest.fn();
|
||||
reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
let out;
|
||||
|
||||
const state = mockUseKeyedState(hooks.stateKeys);
|
||||
|
||||
describe('CourseCardMenu hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
state.mock();
|
||||
describe('state values', () => {
|
||||
state.testGetter(state.keys.isUnenrollConfirmVisible);
|
||||
state.testGetter(state.keys.isEmailSettingsVisible);
|
||||
});
|
||||
|
||||
describe('useUnenrollData', () => {
|
||||
beforeEach(() => {
|
||||
state.mockVals({ isUnenrollConfirmVisible: true });
|
||||
state.mock();
|
||||
out = hooks.useUnenrollData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes isUnenrollConfirmVisible state to false', () => {
|
||||
state.expectInitializedWith(state.keys.isUnenrollConfirmVisible, false);
|
||||
});
|
||||
afterEach(state.restore);
|
||||
|
||||
test('default state', () => {
|
||||
expect(out.isVisible).toEqual(state.stateVals.isUnenrollConfirmVisible);
|
||||
});
|
||||
describe('output', () => {
|
||||
test('state is loaded from current state value', () => {
|
||||
expect(out.isVisible).toEqual(true);
|
||||
});
|
||||
test('show sets state value to true', () => {
|
||||
out.show();
|
||||
expect(state.setState.isUnenrollConfirmVisible).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('hide sets state value to false', () => {
|
||||
out.hide();
|
||||
expect(state.setState.isUnenrollConfirmVisible).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test('show', () => {
|
||||
out.show();
|
||||
state.expectSetStateCalledWith(state.keys.isUnenrollConfirmVisible, true);
|
||||
});
|
||||
|
||||
test('hide', () => {
|
||||
out.hide();
|
||||
state.expectSetStateCalledWith(state.keys.isUnenrollConfirmVisible, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useEmailSettings', () => {
|
||||
beforeEach(() => {
|
||||
state.mockVals({ isEmailSettingsVisible: true });
|
||||
state.mock();
|
||||
out = hooks.useEmailSettings();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes isEmailSettingsVisible state to false', () => {
|
||||
state.expectInitializedWith(state.keys.isEmailSettingsVisible, false);
|
||||
});
|
||||
afterEach(state.restore);
|
||||
|
||||
test('default state', () => {
|
||||
expect(out.isVisible).toEqual(state.stateVals.isEmailSettingsVisible);
|
||||
});
|
||||
describe('output', () => {
|
||||
test('state is loaded from current state value', () => {
|
||||
expect(out.isVisible).toEqual(state.values.isEmailSettingsVisible);
|
||||
});
|
||||
test('show sets state value to true', () => {
|
||||
out.show();
|
||||
expect(state.setState.isEmailSettingsVisible).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('hide sets state value to false', () => {
|
||||
out.hide();
|
||||
expect(state.setState.isEmailSettingsVisible).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test('show', () => {
|
||||
out.show();
|
||||
state.expectSetStateCalledWith(state.keys.isEmailSettingsVisible, true);
|
||||
});
|
||||
|
||||
test('hide', () => {
|
||||
out.hide();
|
||||
state.expectSetStateCalledWith(state.keys.isEmailSettingsVisible, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useHandleToggleDropdown', () => {
|
||||
beforeEach(() => { out = hooks.useHandleToggleDropdown(cardId); });
|
||||
beforeEach(() => {
|
||||
out = hooks.useHandleToggleDropdown(cardId);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes course event tracker with event name and card ID', () => {
|
||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
||||
@@ -95,59 +88,4 @@ describe('CourseCardMenu hooks', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useOptionVisibility', () => {
|
||||
const mockReduxHooks = (returnVals = {}) => {
|
||||
reduxHooks.useCardSocialSettingsData.mockReturnValueOnce({
|
||||
facebook: { isEnabled: !!returnVals.facebook?.isEnabled },
|
||||
twitter: { isEnabled: !!returnVals.twitter?.isEnabled },
|
||||
});
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
isEnrolled: !!returnVals.isEnrolled,
|
||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||
});
|
||||
reduxHooks.useCardCertificateData.mockReturnValueOnce({
|
||||
isEarned: !!returnVals.isEarned,
|
||||
});
|
||||
};
|
||||
describe('shouldShowUnenrollItem', () => {
|
||||
it('returns true if enrolled and not earned', () => {
|
||||
mockReduxHooks({ isEnrolled: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(true);
|
||||
});
|
||||
it('returns false if not enrolled', () => {
|
||||
mockReduxHooks();
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(false);
|
||||
});
|
||||
it('returns false if enrolled but also earned', () => {
|
||||
mockReduxHooks({ isEarned: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldShowDropdown', () => {
|
||||
it('returns false if not enrolled and both email and socials are disabled', () => {
|
||||
mockReduxHooks();
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(false);
|
||||
});
|
||||
it('returns false if enrolled but already earned, and both email and socials are disabled', () => {
|
||||
mockReduxHooks({ isEnrolled: true, isEarned: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(false);
|
||||
});
|
||||
it('returns true if either social is enabled', () => {
|
||||
mockReduxHooks({ facebook: { isEnabled: true } });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
mockReduxHooks({ twitter: { isEnabled: true } });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
});
|
||||
it('returns true if email is enabled', () => {
|
||||
mockReduxHooks({ isEmailEnabled: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
});
|
||||
it('returns true if enrolled and not earned', () => {
|
||||
mockReduxHooks({ isEnrolled: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,39 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as ReactShare from 'react-share';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown, Icon, IconButton } from '@edx/paragon';
|
||||
import { MoreVert } from '@edx/paragon/icons';
|
||||
import { StrictDict } from '@edx/react-unit-test-utils';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import EmailSettingsModal from 'containers/EmailSettingsModal';
|
||||
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import SocialShareMenu from './SocialShareMenu';
|
||||
import {
|
||||
useEmailSettings,
|
||||
useUnenrollData,
|
||||
useHandleToggleDropdown,
|
||||
useOptionVisibility,
|
||||
} from './hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const testIds = StrictDict({
|
||||
unenrollModalToggle: 'unenrollModalToggle',
|
||||
});
|
||||
|
||||
export const CourseCardMenu = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const emailSettings = useEmailSettings();
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const { isEnrolled, isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isEarned } = reduxHooks.useCardCertificateData(cardId);
|
||||
const handleTwitterShare = reduxHooks.useTrackCourseEvent(
|
||||
track.socialShare,
|
||||
cardId,
|
||||
'twitter',
|
||||
);
|
||||
const handleFacebookShare = reduxHooks.useTrackCourseEvent(
|
||||
track.socialShare,
|
||||
cardId,
|
||||
'facebook',
|
||||
);
|
||||
|
||||
const emailSettingsModal = useEmailSettings();
|
||||
const unenrollModal = useUnenrollData();
|
||||
const handleToggleDropdown = useHandleToggleDropdown(cardId);
|
||||
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
|
||||
if (!shouldShowDropdown) {
|
||||
const showUnenrollItem = isEnrolled && !isEarned;
|
||||
const showDropdown = showUnenrollItem || isEmailEnabled || facebook.isEnabled || twitter.isEnabled;
|
||||
|
||||
if (!showDropdown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -49,16 +60,52 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
alt={formatMessage(messages.dropdownAlt)}
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
{shouldShowUnenrollItem && (
|
||||
{showUnenrollItem && (
|
||||
<Dropdown.Item
|
||||
disabled={isMasquerading}
|
||||
onClick={unenrollModal.show}
|
||||
data-testid={testIds.unenrollModalToggle}
|
||||
data-testid="unenrollModalToggle"
|
||||
>
|
||||
{formatMessage(messages.unenroll)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<SocialShareMenu cardId={cardId} emailSettings={emailSettings} />
|
||||
{isEmailEnabled && (
|
||||
<Dropdown.Item
|
||||
disabled={isMasquerading}
|
||||
onClick={emailSettingsModal.show}
|
||||
data-testid="emailSettingsModalToggle"
|
||||
>
|
||||
{formatMessage(messages.emailSettings)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{facebook.isEnabled && (
|
||||
<ReactShare.FacebookShareButton
|
||||
url={facebook.shareUrl}
|
||||
onClick={handleFacebookShare}
|
||||
title={formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: facebook.socialBrand,
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
>
|
||||
{formatMessage(messages.shareToFacebook)}
|
||||
</ReactShare.FacebookShareButton>
|
||||
)}
|
||||
{twitter.isEnabled && (
|
||||
<ReactShare.TwitterShareButton
|
||||
url={twitter.shareUrl}
|
||||
onClick={handleTwitterShare}
|
||||
title={formatMessage(messages.shareQuote, {
|
||||
courseName,
|
||||
socialBrand: twitter.socialBrand,
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
>
|
||||
{formatMessage(messages.shareToTwitter)}
|
||||
</ReactShare.TwitterShareButton>
|
||||
)}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<UnenrollConfirmModal
|
||||
@@ -68,8 +115,8 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
/>
|
||||
{isEmailEnabled && (
|
||||
<EmailSettingsModal
|
||||
show={emailSettings.isVisible}
|
||||
closeModal={emailSettings.hide}
|
||||
show={emailSettingsModal.isVisible}
|
||||
closeModal={emailSettingsModal.hide}
|
||||
cardId={cardId}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,213 +1,216 @@
|
||||
import { when } from 'jest-when';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
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 '.';
|
||||
import { useEmailSettings, useUnenrollData } from './hooks';
|
||||
import CourseCardMenu from '.';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
useIntl: jest.fn().mockReturnValue({
|
||||
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
|
||||
}),
|
||||
jest.mock('react-share', () => ({
|
||||
FacebookShareButton: () => 'FacebookShareButton',
|
||||
TwitterShareButton: () => 'TwitterShareButton',
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: { useMasqueradeData: jest.fn(), useCardEnrollmentData: jest.fn() },
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardSocialSettingsData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardCertificateData: jest.fn(),
|
||||
useTrackCourseEvent: (_, __, site) => jest.fn().mockName(`${site}ShareClick`),
|
||||
},
|
||||
}));
|
||||
jest.mock('./SocialShareMenu', () => 'SocialShareMenu');
|
||||
jest.mock('./hooks', () => ({
|
||||
useEmailSettings: jest.fn(),
|
||||
useUnenrollData: jest.fn(),
|
||||
useHandleToggleDropdown: jest.fn(),
|
||||
useOptionVisibility: jest.fn(),
|
||||
}));
|
||||
|
||||
const props = {
|
||||
cardId: 'test-card-id',
|
||||
};
|
||||
|
||||
const emailSettings = {
|
||||
const defaultEmailSettingsModal = {
|
||||
isVisible: false,
|
||||
show: jest.fn().mockName('emailSettingShow'),
|
||||
hide: jest.fn().mockName('emailSettingHide'),
|
||||
};
|
||||
|
||||
const unenrollData = {
|
||||
const defaultUnenrollModal = {
|
||||
isVisible: false,
|
||||
show: jest.fn().mockName('unenrollShow'),
|
||||
hide: jest.fn().mockName('unenrollHide'),
|
||||
};
|
||||
|
||||
const defaultSocialShare = {
|
||||
facebook: {
|
||||
isEnabled: true,
|
||||
shareUrl: 'facebook-share-url',
|
||||
socialBrand: 'facebook-social-brand',
|
||||
},
|
||||
twitter: {
|
||||
isEnabled: true,
|
||||
shareUrl: 'twitter-share-url',
|
||||
socialBrand: 'twitter-social-brand',
|
||||
},
|
||||
};
|
||||
const courseName = 'test-course-name';
|
||||
let wrapper;
|
||||
let el;
|
||||
|
||||
const mockHook = (fn, returnValue, options = {}) => {
|
||||
if (options.isCardHook) {
|
||||
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue);
|
||||
} else {
|
||||
when(fn).calledWith().mockReturnValueOnce(returnValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleDropdown = jest.fn().mockName('hooks.handleToggleDropdown');
|
||||
|
||||
const mockHooks = (returnVals = {}) => {
|
||||
mockHook(
|
||||
hooks.useEmailSettings,
|
||||
returnVals.emailSettings ? returnVals.emailSettings : emailSettings,
|
||||
);
|
||||
mockHook(
|
||||
hooks.useUnenrollData,
|
||||
returnVals.unenrollData ? returnVals.unenrollData : unenrollData,
|
||||
);
|
||||
mockHook(hooks.useHandleToggleDropdown, handleToggleDropdown, { isCardHook: true });
|
||||
mockHook(
|
||||
hooks.useOptionVisibility,
|
||||
{
|
||||
shouldShowUnenrollItem: !!returnVals.shouldShowUnenrollItem,
|
||||
shouldShowDropdown: !!returnVals.shouldShowDropdown,
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
|
||||
mockHook(
|
||||
reduxHooks.useCardEnrollmentData,
|
||||
{ isEmailEnabled: !!returnVals.isEmailEnabled },
|
||||
{ isCardHook: true },
|
||||
);
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<CourseCardMenu {...props} />);
|
||||
};
|
||||
|
||||
describe('CourseCardMenu', () => {
|
||||
describe('behavior', () => {
|
||||
const mockCourseCardMenu = ({
|
||||
isEnrolled,
|
||||
isEmailEnabled,
|
||||
isMasquerading,
|
||||
facebook,
|
||||
twitter,
|
||||
isEarned,
|
||||
}) => {
|
||||
useEmailSettings.mockReturnValueOnce(defaultEmailSettingsModal);
|
||||
useUnenrollData.mockReturnValueOnce(defaultUnenrollModal);
|
||||
reduxHooks.useCardCourseData.mockReturnValueOnce({ courseName });
|
||||
reduxHooks.useCardSocialSettingsData.mockReturnValueOnce({
|
||||
facebook: {
|
||||
...defaultSocialShare.facebook,
|
||||
...facebook,
|
||||
},
|
||||
twitter: {
|
||||
...defaultSocialShare.twitter,
|
||||
...twitter,
|
||||
},
|
||||
});
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
isEnrolled,
|
||||
isEmailEnabled,
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
reduxHooks.useCardCertificateData.mockReturnValueOnce({ isEarned });
|
||||
return shallow(<CourseCardMenu {...props} />);
|
||||
};
|
||||
describe('enrolled, share enabled, email setting enable', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks();
|
||||
render();
|
||||
wrapper = mockCourseCardMenu({
|
||||
isEnrolled: true,
|
||||
isEmailEnabled: true,
|
||||
isMasquerading: false,
|
||||
isEarned: false,
|
||||
});
|
||||
});
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalledWith();
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('initializes local hooks', () => {
|
||||
when(hooks.useEmailSettings).expectCalledWith();
|
||||
when(hooks.useUnenrollData).expectCalledWith();
|
||||
when(hooks.useHandleToggleDropdown).expectCalledWith(props.cardId);
|
||||
when(hooks.useOptionVisibility).expectCalledWith(props.cardId);
|
||||
it('renders share buttons', () => {
|
||||
el = wrapper.find('FacebookShareButton');
|
||||
expect(el.length).toEqual(1);
|
||||
expect(el.prop('url')).toEqual('facebook-share-url');
|
||||
el = wrapper.find('TwitterShareButton');
|
||||
expect(el.length).toEqual(1);
|
||||
expect(el.prop('url')).toEqual('twitter-share-url');
|
||||
});
|
||||
it('initializes redux hook data ', () => {
|
||||
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
||||
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
||||
it('renders enabled unenroll modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
|
||||
expect(el.props().disabled).toEqual(false);
|
||||
});
|
||||
it('renders enabled email settings modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
|
||||
expect(el.props().disabled).toEqual(false);
|
||||
});
|
||||
it('renders enabled email settings modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
|
||||
expect(el.props().disabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
it('renders null if showDropdown is false', () => {
|
||||
mockHooks();
|
||||
render();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
describe('disable and stop rendering buttons', () => {
|
||||
it('does not render unenroll dropdown item when certificate is already earned', () => {
|
||||
wrapper = mockCourseCardMenu({
|
||||
isEnrolled: true,
|
||||
isEmailEnabled: true,
|
||||
isMasquerading: false,
|
||||
isEarned: true,
|
||||
});
|
||||
el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
|
||||
expect(el.length).toEqual(0);
|
||||
});
|
||||
const testHandleToggle = () => {
|
||||
it('displays Dropdown with onToggle=handleToggleDropdown', () => {
|
||||
expect(el.instance.findByType(Dropdown)[0].props.onToggle).toEqual(handleToggleDropdown);
|
||||
it('does not render unenroll dropdown item when course is not enrolled', () => {
|
||||
wrapper = mockCourseCardMenu({
|
||||
isEnrolled: false,
|
||||
isEmailEnabled: true,
|
||||
isMasquerading: false,
|
||||
isEarned: false,
|
||||
});
|
||||
};
|
||||
const testUnenrollConfirmModal = () => {
|
||||
it('displays UnenrollConfirmModal with cardId and unenrollModal data', () => {
|
||||
const modal = el.instance.findByType(UnenrollConfirmModal)[0];
|
||||
expect(modal.props.show).toEqual(unenrollData.isVisible);
|
||||
expect(modal.props.closeModal).toEqual(unenrollData.hide);
|
||||
expect(modal.props.cardId).toEqual(props.cardId);
|
||||
el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
|
||||
expect(el.length).toEqual(0);
|
||||
});
|
||||
it('does not render email settings modal toggle when email is not enabled', () => {
|
||||
wrapper = mockCourseCardMenu({
|
||||
isEnrolled: true,
|
||||
isEmailEnabled: false,
|
||||
isMasquerading: false,
|
||||
isEarned: false,
|
||||
});
|
||||
};
|
||||
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);
|
||||
el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
|
||||
expect(el.length).toEqual(0);
|
||||
});
|
||||
it('does not render facebook share button when facebook is not enabled', () => {
|
||||
wrapper = mockCourseCardMenu({
|
||||
isEnrolled: true,
|
||||
isEmailEnabled: true,
|
||||
facebook: {
|
||||
...defaultSocialShare.facebook,
|
||||
isEnabled: false,
|
||||
},
|
||||
isMasquerading: false,
|
||||
isEarned: false,
|
||||
});
|
||||
};
|
||||
describe('show dropdown', () => {
|
||||
describe('hide unenroll item and disable email', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ shouldShowDropdown: true });
|
||||
render();
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
testHandleToggle();
|
||||
testSocialShareMenu();
|
||||
it('does not render unenroll modal toggle', () => {
|
||||
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(0);
|
||||
});
|
||||
it('does not render EmailSettingsModal', () => {
|
||||
expect(el.instance.findByType(EmailSettingsModal).length).toEqual(0);
|
||||
});
|
||||
testUnenrollConfirmModal();
|
||||
el = wrapper.find('FacebookShareButton');
|
||||
expect(el.length).toEqual(0);
|
||||
});
|
||||
it('does not render twitter share button when twitter is not enabled', () => {
|
||||
wrapper = mockCourseCardMenu({
|
||||
isEnrolled: true,
|
||||
isEmailEnabled: true,
|
||||
twitter: {
|
||||
...defaultSocialShare.twitter,
|
||||
isEnabled: false,
|
||||
},
|
||||
isMasquerading: false,
|
||||
isEarned: false,
|
||||
});
|
||||
describe('show unenroll and enable email', () => {
|
||||
const hookProps = {
|
||||
shouldShowDropdown: true,
|
||||
isEmailEnabled: true,
|
||||
shouldShowUnenrollItem: true,
|
||||
};
|
||||
beforeEach(() => {
|
||||
mockHooks(hookProps);
|
||||
render();
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
testHandleToggle();
|
||||
testSocialShareMenu();
|
||||
describe('unenroll modal toggle', () => {
|
||||
let toggle;
|
||||
describe('not masquerading', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks(hookProps);
|
||||
render();
|
||||
[toggle] = el.instance.findByTestId(testIds.unenrollModalToggle);
|
||||
});
|
||||
it('renders unenroll modal toggle', () => {
|
||||
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(1);
|
||||
});
|
||||
test('onClick from unenroll modal hook', () => {
|
||||
expect(toggle.props.onClick).toEqual(unenrollData.show);
|
||||
});
|
||||
test('disabled', () => {
|
||||
expect(toggle.props.disabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('masquerading', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ ...hookProps, isMasquerading: true });
|
||||
render();
|
||||
[toggle] = el.instance.findByTestId(testIds.unenrollModalToggle);
|
||||
});
|
||||
it('renders', () => {
|
||||
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(1);
|
||||
});
|
||||
test('onClick from unenroll modal hook', () => {
|
||||
expect(toggle.props.onClick).toEqual(unenrollData.show);
|
||||
});
|
||||
test('disabled', () => {
|
||||
expect(toggle.props.disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
testUnenrollConfirmModal();
|
||||
it('displays EmaiSettingsModal with cardId and emailSettingsModal data', () => {
|
||||
const modal = el.instance.findByType(EmailSettingsModal)[0];
|
||||
expect(modal.props.show).toEqual(emailSettings.isVisible);
|
||||
expect(modal.props.closeModal).toEqual(emailSettings.hide);
|
||||
expect(modal.props.cardId).toEqual(props.cardId);
|
||||
});
|
||||
el = wrapper.find('TwitterShareButton');
|
||||
expect(el.length).toEqual(0);
|
||||
});
|
||||
it('snapshot when no dropdown items exist', () => {
|
||||
wrapper = mockCourseCardMenu({
|
||||
isEnrolled: true,
|
||||
isEmailEnabled: true,
|
||||
isMasquerading: false,
|
||||
isEarned: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('masquerading', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mockCourseCardMenu({
|
||||
isEnrolled: true,
|
||||
isEmailEnabled: true,
|
||||
isMasquerading: true,
|
||||
isEarned: false,
|
||||
});
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('renders share buttons', () => {
|
||||
expect(wrapper.find('FacebookShareButton').length).toEqual(1);
|
||||
el = wrapper.find('TwitterShareButton');
|
||||
expect(el.length).toEqual(1);
|
||||
expect(el.prop('url')).toEqual('twitter-share-url');
|
||||
});
|
||||
it('renders disabled unenroll modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
|
||||
expect(el.props().disabled).toEqual(true);
|
||||
});
|
||||
it('renders disabled email settings modal toggle', () => {
|
||||
el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
|
||||
expect(el.props().disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,124 +63,143 @@ describe('useActionDisabledState', () => {
|
||||
});
|
||||
};
|
||||
|
||||
const runHook = () => hooks.useActionDisabledState(cardId);
|
||||
describe('disableBeginCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableBeginCourse).toBe(expected);
|
||||
};
|
||||
it('disable when homeUrl is invalid', () => {
|
||||
testDisabled({ homeUrl: null }, true);
|
||||
mockHooksData({ homeUrl: null });
|
||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableBeginCourse).toBe(true);
|
||||
});
|
||||
it('disable when isMasquerading is true', () => {
|
||||
testDisabled({ isMasquerading: true }, true);
|
||||
mockHooksData({ isMasquerading: true });
|
||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableBeginCourse).toBe(true);
|
||||
});
|
||||
it('disable when hasAccess is false', () => {
|
||||
testDisabled({ hasAccess: false }, true);
|
||||
mockHooksData({ hasAccess: false });
|
||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableBeginCourse).toBe(true);
|
||||
});
|
||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||
testDisabled({ isAudit: true, isAuditAccessExpired: true }, true);
|
||||
mockHooksData({ isAudit: true, isAuditAccessExpired: true });
|
||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableBeginCourse).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ hasAccess: true }, false);
|
||||
mockHooksData({ hasAccess: true });
|
||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableBeginCourse).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('disableResumeCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableResumeCourse).toBe(expected);
|
||||
};
|
||||
it('disable when resumeUrl is invalid', () => {
|
||||
testDisabled({ resumeUrl: null }, true);
|
||||
mockHooksData({ resumeUrl: null });
|
||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableResumeCourse).toBe(true);
|
||||
});
|
||||
it('disable when isMasquerading is true', () => {
|
||||
testDisabled({ isMasquerading: true }, true);
|
||||
mockHooksData({ isMasquerading: true });
|
||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableResumeCourse).toBe(true);
|
||||
});
|
||||
it('disable when hasAccess is false', () => {
|
||||
testDisabled({ hasAccess: false }, true);
|
||||
mockHooksData({ hasAccess: false });
|
||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableResumeCourse).toBe(true);
|
||||
});
|
||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||
testDisabled({ isAudit: true, isAuditAccessExpired: true }, true);
|
||||
mockHooksData({ isAudit: true, isAuditAccessExpired: true });
|
||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableResumeCourse).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ hasAccess: true }, false);
|
||||
mockHooksData({ hasAccess: true });
|
||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableResumeCourse).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('disableViewCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableViewCourse).toBe(expected);
|
||||
};
|
||||
it('disable when hasAccess is false', () => {
|
||||
testDisabled({ hasAccess: false }, true);
|
||||
mockHooksData({ hasAccess: false });
|
||||
const { disableViewCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableViewCourse).toBe(true);
|
||||
});
|
||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||
testDisabled({ isAudit: true, isAuditAccessExpired: true }, true);
|
||||
mockHooksData({ isAudit: true, isAuditAccessExpired: true });
|
||||
const { disableViewCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableViewCourse).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ hasAccess: true }, false);
|
||||
mockHooksData({ hasAccess: true });
|
||||
const { disableViewCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableViewCourse).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('disableUpgradeCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableUpgradeCourse).toBe(expected);
|
||||
};
|
||||
it('disable when upgradeUrl is invalid', () => {
|
||||
testDisabled({ upgradeUrl: null }, true);
|
||||
mockHooksData({ upgradeUrl: null });
|
||||
const { disableUpgradeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableUpgradeCourse).toBe(true);
|
||||
});
|
||||
it('disable when isMasquerading is true and canUpgrade is false', () => {
|
||||
testDisabled({ isMasquerading: true, canUpgrade: false }, true);
|
||||
mockHooksData({ isMasquerading: true, canUpgrade: false });
|
||||
const { disableUpgradeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableUpgradeCourse).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ canUpgrade: true }, false);
|
||||
mockHooksData({ canUpgrade: true });
|
||||
const { disableUpgradeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableUpgradeCourse).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('disableSelectSession', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableSelectSession).toBe(expected);
|
||||
};
|
||||
it('disable when isEntitlement is false', () => {
|
||||
testDisabled({ isEntitlement: false }, true);
|
||||
mockHooksData({ isEntitlement: false });
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(true);
|
||||
});
|
||||
it('disable when isMasquerading is true', () => {
|
||||
testDisabled({ isMasquerading: true }, true);
|
||||
mockHooksData({ isMasquerading: true });
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(true);
|
||||
});
|
||||
it('disable when hasAccess is false', () => {
|
||||
testDisabled({ hasAccess: false }, true);
|
||||
mockHooksData({ hasAccess: false });
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(true);
|
||||
});
|
||||
it('disable when canChange is false', () => {
|
||||
testDisabled({ canChange: false }, true);
|
||||
mockHooksData({ canChange: false });
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(true);
|
||||
});
|
||||
it('disable when hasSessions is false', () => {
|
||||
testDisabled({ hasSessions: false }, true);
|
||||
mockHooksData({ hasSessions: false });
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled(
|
||||
{
|
||||
isEntitlement: true,
|
||||
hasAccess: true,
|
||||
canChange: true,
|
||||
hasSessions: true,
|
||||
},
|
||||
false,
|
||||
);
|
||||
mockHooksData({
|
||||
isEntitlement: true, hasAccess: true, canChange: true, hasSessions: true,
|
||||
});
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('disableCourseTitle', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
mockHooksData(data);
|
||||
expect(runHook().disableCourseTitle).toBe(expected);
|
||||
};
|
||||
it('disable when isEntitlement is true and isFulfilled is false', () => {
|
||||
testDisabled({ isEntitlement: true, isFulfilled: false }, true);
|
||||
mockHooksData({ isEntitlement: true, isFulfilled: false });
|
||||
const { disableCourseTitle } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableCourseTitle).toBe(true);
|
||||
});
|
||||
it('disable when disableViewCourse is true', () => {
|
||||
testDisabled({ hasAccess: false }, true);
|
||||
mockHooksData({ hasAccess: false });
|
||||
const { disableCourseTitle } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableCourseTitle).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
testDisabled({ isEntitlement: true, isFulfilled: true, hasAccess: true }, false);
|
||||
mockHooksData({ isEntitlement: true, isFulfilled: true, hasAccess: true });
|
||||
const { disableCourseTitle } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableCourseTitle).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ export const CourseFilterControls = ({
|
||||
onClose={close}
|
||||
>
|
||||
<div className="p-1 mr-3">
|
||||
<b>{formatMessage(messages.refine)}</b>
|
||||
<b>Refine</b>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="filter-form-row">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
const messages = defineMessages({
|
||||
export const messages = StrictDict({
|
||||
inProgress: {
|
||||
id: 'learner-dash.courseListFilters.inProgress',
|
||||
description: 'in-progress filter checkbox label for course list filters',
|
||||
@@ -52,5 +52,4 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Refine',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -17,7 +17,7 @@ exports[`NoCoursesView snapshot 1`] = `
|
||||
</p>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/course-search-url"
|
||||
href="course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
variant="brand"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Image } from '@edx/paragon';
|
||||
import { Search } from '@edx/paragon/icons';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import emptyCourseSVG from 'assets/empty-course.svg';
|
||||
import { reduxHooks } from 'hooks';
|
||||
@@ -28,7 +27,7 @@ export const NoCoursesView = () => {
|
||||
<Button
|
||||
variant="brand"
|
||||
as="a"
|
||||
href={baseAppUrl(courseSearchUrl)}
|
||||
href={courseSearchUrl}
|
||||
iconBefore={Search}
|
||||
>
|
||||
{formatMessage(messages.exploreCoursesButton)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import EmptyCourse from '.';
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: '/course-search-url',
|
||||
courseSearchUrl: 'course-search-url',
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -3,19 +3,12 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { Container, Col, Row } from '@edx/paragon';
|
||||
|
||||
import WidgetFooter from 'containers/WidgetContainers/WidgetFooter';
|
||||
import hooks from './hooks';
|
||||
|
||||
export const columnConfig = {
|
||||
courseList: {
|
||||
withSidebar: {
|
||||
lg: { span: 12, offset: 0 },
|
||||
xl: { span: 8, offset: 0 },
|
||||
},
|
||||
noSidebar: {
|
||||
lg: { span: 12, offset: 0 },
|
||||
xl: { span: 12, offset: 0 },
|
||||
},
|
||||
lg: { span: 12, offset: 0 },
|
||||
xl: { span: 8, offset: 0 },
|
||||
},
|
||||
sidebar: {
|
||||
lg: { span: 12, offset: 0 },
|
||||
@@ -23,31 +16,18 @@ export const columnConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
|
||||
const {
|
||||
isCollapsed,
|
||||
sidebarShowing,
|
||||
setSidebarShowing,
|
||||
} = hooks.useDashboardLayoutData();
|
||||
|
||||
const courseListColumnProps = sidebarShowing
|
||||
? columnConfig.courseList.withSidebar
|
||||
: columnConfig.courseList.noSidebar;
|
||||
export const DashboardLayout = ({ children, sidebar }) => {
|
||||
const isCollapsed = hooks.useIsDashboardCollapsed();
|
||||
|
||||
return (
|
||||
<Container fluid size="xl">
|
||||
<Row>
|
||||
<Col {...courseListColumnProps} className="course-list-column">
|
||||
<Col {...columnConfig.courseList} className="course-list-column">
|
||||
{children}
|
||||
</Col>
|
||||
<Col {...columnConfig.sidebar} className="sidebar-column">
|
||||
{!isCollapsed && (<h2 className="course-list-title"> </h2>)}
|
||||
<Sidebar setSidebarShowing={setSidebarShowing} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
{sidebar}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -55,7 +35,7 @@ export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
|
||||
};
|
||||
DashboardLayout.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
sidebar: PropTypes.func.isRequired,
|
||||
sidebar: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default DashboardLayout;
|
||||
|
||||
@@ -1,125 +1,60 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Col, Row } from '@edx/paragon';
|
||||
|
||||
import WidgetFooter from 'containers/WidgetContainers/WidgetFooter';
|
||||
import hooks from './hooks';
|
||||
import DashboardLayout, { columnConfig } from './DashboardLayout';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useDashboardLayoutData: jest.fn(),
|
||||
useIsDashboardCollapsed: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
const hookProps = {
|
||||
isCollapsed: true,
|
||||
sidebarShowing: false,
|
||||
setSidebarShowing: jest.fn().mockName('hooks.setSidebarShowing'),
|
||||
};
|
||||
hooks.useDashboardLayoutData.mockReturnValue(hookProps);
|
||||
|
||||
const props = {
|
||||
sidebar: jest.fn(() => 'test-sidebar-content'),
|
||||
};
|
||||
|
||||
const children = 'test-children';
|
||||
|
||||
let el;
|
||||
describe('DashboardLayout', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<DashboardLayout {...props}>{children}</DashboardLayout>);
|
||||
});
|
||||
|
||||
const children = 'test-children';
|
||||
const props = {
|
||||
sidebar: 'test-sidebar-content',
|
||||
};
|
||||
const render = () => shallow(<DashboardLayout sidebar={props.sidebar}>{children}</DashboardLayout>);
|
||||
const testColumns = () => {
|
||||
it('loads courseList and sidebar column layout', () => {
|
||||
const columns = el.find(Row).find(Col);
|
||||
const columns = render().find(Row).find(Col);
|
||||
Object.keys(columnConfig.courseList).forEach(size => {
|
||||
expect(columns.at(0).props()[size]).toEqual(columnConfig.courseList[size]);
|
||||
});
|
||||
Object.keys(columnConfig.sidebar).forEach(size => {
|
||||
expect(columns.at(1).props()[size]).toEqual(columnConfig.sidebar[size]);
|
||||
});
|
||||
});
|
||||
it('displays children in first column', () => {
|
||||
const columns = el.find(Row).find(Col);
|
||||
const columns = render().find(Row).find(Col);
|
||||
expect(columns.at(0).contains(children)).toEqual(true);
|
||||
});
|
||||
it('displays sidebar prop in second column', () => {
|
||||
const columns = el.find(Row).find(Col);
|
||||
expect(columns.at(1).find(props.sidebar)).toHaveLength(1);
|
||||
});
|
||||
it('displays a footer in the second row', () => {
|
||||
const columns = el.find(Row).at(1).find(Col);
|
||||
expect(columns.at(0).containsMatchingElement(<WidgetFooter />)).toBeTruthy();
|
||||
});
|
||||
};
|
||||
const testSidebarLayout = () => {
|
||||
it('displays widthSidebar width for course list column', () => {
|
||||
const columns = el.find(Row).find(Col);
|
||||
Object.keys(columnConfig.courseList.withSidebar).forEach(size => {
|
||||
expect(columns.at(0).props()[size]).toEqual(columnConfig.courseList.withSidebar[size]);
|
||||
});
|
||||
});
|
||||
};
|
||||
const testNoSidebarLayout = () => {
|
||||
it('displays noSidebar width for course list column', () => {
|
||||
const columns = el.find(Row).find(Col);
|
||||
Object.keys(columnConfig.courseList.noSidebar).forEach(size => {
|
||||
expect(columns.at(0).props()[size]).toEqual(columnConfig.courseList.noSidebar[size]);
|
||||
});
|
||||
const columns = render().find(Row).find(Col);
|
||||
expect(columns.at(1).contains(props.sidebar)).toEqual(true);
|
||||
});
|
||||
};
|
||||
const testSnapshot = () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(render()).toMatchSnapshot();
|
||||
});
|
||||
};
|
||||
describe('collapsed', () => {
|
||||
describe('sidebar showing', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useDashboardLayoutData.mockReturnValueOnce({ ...hookProps, sidebarShowing: true });
|
||||
});
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
testSidebarLayout();
|
||||
});
|
||||
describe('sidebar not showing', () => {
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
testNoSidebarLayout();
|
||||
});
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
it('does not show spacer component above widget sidebar', () => {
|
||||
const columns = el.find(Col);
|
||||
const columns = render().find(Col);
|
||||
expect(columns.at(1).find('h2').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('not collapsed', () => {
|
||||
const testWidgetSpacing = () => {
|
||||
it('shows a blank (nbsp) h2 spacer component above widget sidebar', () => {
|
||||
const columns = el.find(Col);
|
||||
// nonbreaking space equivalent
|
||||
expect(columns.at(1).find('h2').text()).toEqual('\xA0');
|
||||
});
|
||||
};
|
||||
describe('sidebar showing', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useDashboardLayoutData.mockReturnValueOnce({
|
||||
...hookProps,
|
||||
isCollapsed: false,
|
||||
sidebarShowing: true,
|
||||
});
|
||||
});
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
testSidebarLayout();
|
||||
testWidgetSpacing();
|
||||
});
|
||||
describe('sidebar not showing', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useDashboardLayoutData.mockReturnValueOnce({ ...hookProps, isCollapsed: false });
|
||||
});
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
testNoSidebarLayout();
|
||||
testWidgetSpacing();
|
||||
beforeEach(() => { hooks.useIsDashboardCollapsed.mockReturnValueOnce(false); });
|
||||
testColumns();
|
||||
testSnapshot();
|
||||
it('shows a blank (nbsp) h2 spacer component above widget sidebar', () => {
|
||||
const columns = render().find(Col);
|
||||
// nonbreaking space equivalent
|
||||
expect(columns.at(1).find('h2').text()).toEqual('\xA0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,57 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
|
||||
<Container
|
||||
fluid={true}
|
||||
size="xl"
|
||||
>
|
||||
<Row>
|
||||
<Col
|
||||
className="course-list-column"
|
||||
lg={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
test-children
|
||||
</Col>
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
lg={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
|
||||
exports[`DashboardLayout collapsed snapshot 1`] = `
|
||||
<Container
|
||||
fluid={true}
|
||||
size="xl"
|
||||
@@ -89,76 +38,13 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
test-sidebar-content
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
|
||||
<Container
|
||||
fluid={true}
|
||||
size="xl"
|
||||
>
|
||||
<Row>
|
||||
<Col
|
||||
className="course-list-column"
|
||||
lg={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
test-children
|
||||
</Col>
|
||||
<Col
|
||||
className="sidebar-column"
|
||||
lg={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 12,
|
||||
}
|
||||
}
|
||||
xl={
|
||||
Object {
|
||||
"offset": 0,
|
||||
"span": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<h2
|
||||
className="course-list-title"
|
||||
>
|
||||
|
||||
</h2>
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
||||
exports[`DashboardLayout not collapsed snapshot 1`] = `
|
||||
<Container
|
||||
fluid={true}
|
||||
size="xl"
|
||||
@@ -201,14 +87,7 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
|
||||
>
|
||||
|
||||
</h2>
|
||||
<mockConstructor
|
||||
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<WidgetFooter />
|
||||
test-sidebar-content
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -15,7 +15,7 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
|
||||
id="dashboard-content"
|
||||
>
|
||||
<DashboardLayout
|
||||
sidebar="LoadedWidgetSidebar"
|
||||
sidebar={<LoadedWidgetSidebar />}
|
||||
>
|
||||
<CourseList />
|
||||
</DashboardLayout>
|
||||
@@ -56,7 +56,7 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
|
||||
id="dashboard-content"
|
||||
>
|
||||
<DashboardLayout
|
||||
sidebar="NoCoursesWidgetSidebar"
|
||||
sidebar={<NoCoursesWidgetSidebar />}
|
||||
>
|
||||
<CourseList />
|
||||
</DashboardLayout>
|
||||
|
||||
@@ -2,14 +2,13 @@ import React from 'react';
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { apiHooks } from 'hooks';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import appMessages from 'messages';
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
sidebarShowing: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
export const useIsDashboardCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
return width < breakpoints.large.maxWidth;
|
||||
};
|
||||
|
||||
export const useInitializeDashboard = () => {
|
||||
const initialize = apiHooks.useInitializeApp();
|
||||
@@ -24,18 +23,8 @@ export const useDashboardMessages = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useDashboardLayoutData = () => {
|
||||
const { width } = useWindowSize();
|
||||
const [sidebarShowing, setSidebarShowing] = module.state.sidebarShowing(false);
|
||||
return {
|
||||
isDashboardCollapsed: width < breakpoints.large.maxWidth,
|
||||
sidebarShowing,
|
||||
setSidebarShowing,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
useDashboardLayoutData,
|
||||
useIsDashboardCollapsed,
|
||||
useInitializeDashboard,
|
||||
useDashboardMessages,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
|
||||
import { apiHooks } from 'hooks';
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import appMessages from 'messages';
|
||||
import * as hooks from './hooks';
|
||||
@@ -20,12 +19,8 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const initializeApp = jest.fn();
|
||||
apiHooks.useInitializeApp.mockReturnValue(initializeApp);
|
||||
useWindowSize.mockReturnValue({ width: 20 });
|
||||
breakpoints.large = { maxWidth: 30 };
|
||||
describe('CourseCard hooks', () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
@@ -33,32 +28,15 @@ describe('CourseCard hooks', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('state fields', () => {
|
||||
state.testGetter(state.keys.sidebarShowing);
|
||||
});
|
||||
|
||||
describe('useDashboardLayoutData', () => {
|
||||
beforeEach(() => { state.mock(); });
|
||||
describe('behavior', () => {
|
||||
it('initializes sidebarShowing to default false value', () => {
|
||||
hooks.useDashboardLayoutData();
|
||||
state.expectInitializedWith(state.keys.sidebarShowing, false);
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('isDashboardCollapsed', () => {
|
||||
it('returns true iff windowSize width is below the xl breakpoint', () => {
|
||||
expect(hooks.useDashboardLayoutData().isDashboardCollapsed).toEqual(true);
|
||||
useWindowSize.mockReturnValueOnce({ width: 40 });
|
||||
expect(hooks.useDashboardLayoutData().isDashboardCollapsed).toEqual(false);
|
||||
});
|
||||
});
|
||||
it('forwards sidebarShowing and setSidebarShowing from state hook', () => {
|
||||
const hook = hooks.useDashboardLayoutData();
|
||||
const { sidebarShowing, setSidebarShowing } = hook;
|
||||
expect(sidebarShowing).toEqual(state.stateVals.sidebarShowing);
|
||||
expect(setSidebarShowing).toEqual(state.setState.sidebarShowing);
|
||||
});
|
||||
describe('useIsDashboardCollapsed', () => {
|
||||
it('returns true iff windowSize width is below the xl breakpoint', () => {
|
||||
useWindowSize.mockReturnValueOnce({ width: 20 });
|
||||
breakpoints.large = { maxWidth: 30 };
|
||||
expect(hooks.useIsDashboardCollapsed()).toEqual(true);
|
||||
useWindowSize.mockReturnValueOnce({ width: 40 });
|
||||
expect(hooks.useIsDashboardCollapsed()).toEqual(false);
|
||||
useWindowSize.mockReturnValueOnce({ width: 40 });
|
||||
expect(hooks.useIsDashboardCollapsed()).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('useInitializeDashboard', () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import LoadingView from './LoadingView';
|
||||
import DashboardLayout from './DashboardLayout';
|
||||
import hooks from './hooks';
|
||||
import './index.scss';
|
||||
import ProductRecommendations from '../../widgets/ProductRecommendations';
|
||||
|
||||
export const Dashboard = () => {
|
||||
hooks.useInitializeDashboard();
|
||||
@@ -22,6 +23,9 @@ export const Dashboard = () => {
|
||||
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
|
||||
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
|
||||
|
||||
// Hard coded to not show until experiment set-up logic is implemented
|
||||
const showProductRecommendations = false && !initIsPending && !hasAvailableDashboards && hasCourses;
|
||||
|
||||
return (
|
||||
<div id="dashboard-container" className="d-flex flex-column p-2 pt-0">
|
||||
<h1 className="sr-only">{pageTitle}</h1>
|
||||
@@ -35,11 +39,12 @@ export const Dashboard = () => {
|
||||
{initIsPending
|
||||
? (<LoadingView />)
|
||||
: (
|
||||
<DashboardLayout sidebar={hasCourses ? LoadedSidebar : NoCoursesSidebar}>
|
||||
<DashboardLayout sidebar={hasCourses ? <LoadedSidebar /> : <NoCoursesSidebar />}>
|
||||
<CourseList />
|
||||
</DashboardLayout>
|
||||
)}
|
||||
</div>
|
||||
{showProductRecommendations && <ProductRecommendations />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('Dashboard', () => {
|
||||
showSelectSessionModal: true,
|
||||
},
|
||||
content: ['LoadedView', (
|
||||
<DashboardLayout sidebar={LoadedWidgetSidebar}><CourseList /></DashboardLayout>
|
||||
<DashboardLayout sidebar={<LoadedWidgetSidebar />}><CourseList /></DashboardLayout>
|
||||
)],
|
||||
showEnterpriseModal: false,
|
||||
showSelectSessionModal: true,
|
||||
@@ -132,7 +132,7 @@ describe('Dashboard', () => {
|
||||
showSelectSessionModal: false,
|
||||
},
|
||||
content: ['Dashboard layout with no courses sidebar and content', (
|
||||
<DashboardLayout sidebar={NoCoursesWidgetSidebar}><CourseList /></DashboardLayout>
|
||||
<DashboardLayout sidebar={<NoCoursesWidgetSidebar />}><CourseList /></DashboardLayout>
|
||||
)],
|
||||
showEnterpriseModal: true,
|
||||
showSelectSessionModal: false,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
export const BrandLogo = () => {
|
||||
@@ -15,7 +14,7 @@ export const BrandLogo = () => {
|
||||
<a href={dashboard?.url || '/'} className="mx-auto">
|
||||
<img
|
||||
className="logo py-3"
|
||||
src={getConfig().LOGO_URL}
|
||||
src="https://edx-cdn.org/v3/prod/logo.svg"
|
||||
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 }) => {
|
||||
@@ -21,7 +21,7 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
|
||||
const exploreCoursesClick = findCoursesNavDropdownClicked(urls.baseAppUrl(courseSearchUrl));
|
||||
const exploreCoursesClick = findCoursesNavDropdownClicked(courseSearchUrl);
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
@@ -29,18 +29,17 @@ 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
|
||||
as="a"
|
||||
href={urls.baseAppUrl(courseSearchUrl)}
|
||||
href={courseSearchUrl}
|
||||
variant="inverse-primary"
|
||||
onClick={exploreCoursesClick}
|
||||
>
|
||||
{formatMessage(messages.discoverNew)}
|
||||
</Button>
|
||||
<WidgetNavbar placement={COLLAPSED_NAVBAR} />
|
||||
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
|
||||
{formatMessage(messages.help)}
|
||||
</Button>
|
||||
|
||||
@@ -17,7 +17,7 @@ jest.mock('hooks', () => ({
|
||||
url: 'url',
|
||||
}),
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: '/courseSearchUrl',
|
||||
courseSearchUrl: 'courseSearchUrl',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -20,15 +20,12 @@ exports[`CollapseMenuBody render 1`] = `
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
href="courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<WidgetNavbar
|
||||
placement="collapsedNavbar"
|
||||
/>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/support"
|
||||
@@ -89,15 +86,12 @@ exports[`CollapseMenuBody render unauthenticated 1`] = `
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
href="courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
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 { MenuIcon, Close } from '@edx/paragon/icons';
|
||||
import { Menu, 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 : MenuIcon}
|
||||
src={isOpen ? Close : Menu}
|
||||
iconAs={Icon}
|
||||
alt={
|
||||
isOpen
|
||||
|
||||
@@ -29,21 +29,16 @@ export const AuthenticatedUserDropdown = () => {
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
{ 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.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>
|
||||
)}
|
||||
|
||||
<Dropdown.Divider />
|
||||
{!dashboard && getConfig().CAREER_LINK_URL && (
|
||||
<Dropdown.Item href={`${getConfig().CAREER_LINK_URL}`}>
|
||||
{formatMessage(messages.career)}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useIsCollapsed } from '../hooks';
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppContext: {
|
||||
authenticatedUser: {
|
||||
@@ -18,13 +17,11 @@ jest.mock('@edx/frontend-platform/react', () => ({
|
||||
},
|
||||
},
|
||||
}));
|
||||
const COURSE_SEARCH_URL = 'test-course-search-url';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: COURSE_SEARCH_URL,
|
||||
courseSearchUrl: 'test-course-search-url',
|
||||
})),
|
||||
},
|
||||
}));
|
||||
@@ -33,11 +30,6 @@ jest.mock('../hooks', () => ({
|
||||
findCoursesNavDropdownClicked: (href) => jest.fn().mockName(`findCoursesNavDropdownClicked('${href}')`),
|
||||
}));
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
baseAppUrl: (url) => (url),
|
||||
programsUrl: 'http://localhost:18000/dashboard/programs',
|
||||
}));
|
||||
|
||||
const config = {
|
||||
ACCOUNT_PROFILE_URL: 'http://account-profile-url.test',
|
||||
ACCOUNT_SETTINGS_URL: 'http://account-settings-url.test',
|
||||
@@ -45,8 +37,6 @@ const config = {
|
||||
ORDER_HISTORY_URL: 'http://order-history-url.test',
|
||||
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);
|
||||
|
||||
|
||||
@@ -27,15 +27,12 @@ exports[`ExpandedHeader render 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4"
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
href="courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavClicked("courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<WidgetNavbar
|
||||
placement="expendedNavbar"
|
||||
/>
|
||||
<span
|
||||
className="flex-grow-1"
|
||||
/>
|
||||
|
||||
@@ -4,12 +4,11 @@ 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';
|
||||
@@ -19,7 +18,7 @@ export const ExpandedHeader = () => {
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
|
||||
const exploreCoursesClick = findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
|
||||
const exploreCoursesClick = findCoursesNavClicked(courseSearchUrl);
|
||||
|
||||
return (
|
||||
!isCollapsed && (
|
||||
@@ -37,7 +36,7 @@ export const ExpandedHeader = () => {
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={urls.programsUrl()}
|
||||
href={urls.programsUrl}
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
>
|
||||
@@ -45,14 +44,13 @@ export const ExpandedHeader = () => {
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={urls.baseAppUrl(courseSearchUrl)}
|
||||
href={courseSearchUrl}
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
onClick={exploreCoursesClick}
|
||||
>
|
||||
{formatMessage(messages.discoverNew)}
|
||||
</Button>
|
||||
<WidgetNavbar placement={EXPANDED_NAVBAR} />
|
||||
<span className="flex-grow-1" />
|
||||
<Button
|
||||
as="a"
|
||||
|
||||
@@ -5,14 +5,13 @@ import ExpandedHeader from '.';
|
||||
import { useIsCollapsed } from '../hooks';
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
programsUrl: () => 'programsUrl',
|
||||
baseAppUrl: url => (`http://localhost:18000${url}`),
|
||||
programsUrl: 'programsUrl',
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: '/courseSearchUrl',
|
||||
courseSearchUrl: 'courseSearchUrl',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -8,7 +8,7 @@ exports[`BrandLogo dashboard defined 1`] = `
|
||||
<img
|
||||
alt="edX, Inc. Dashboard"
|
||||
className="logo py-3"
|
||||
src="https://edx-cdn.org/v3/default/logo.svg"
|
||||
src="https://edx-cdn.org/v3/prod/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/default/logo.svg"
|
||||
src="https://edx-cdn.org/v3/prod/logo.svg"
|
||||
/>
|
||||
</a>
|
||||
`;
|
||||
|
||||
@@ -6,16 +6,6 @@ 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',
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
@@ -1,56 +0,0 @@
|
||||
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 flex-column"
|
||||
className="d-flex"
|
||||
>
|
||||
<RecommendationsPanel />
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import RecommendationsPanel from 'widgets/RecommendationsPanel';
|
||||
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||
|
||||
export const WidgetSidebar = ({ setSidebarShowing }) => {
|
||||
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
|
||||
|
||||
if (!inRecommendationsVariant && isExperimentActive) {
|
||||
setSidebarShowing(true);
|
||||
|
||||
return (
|
||||
<div className="widget-sidebar">
|
||||
<div className="d-flex flex-column">
|
||||
<RecommendationsPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
WidgetSidebar.propTypes = {
|
||||
setSidebarShowing: PropTypes.func.isRequired,
|
||||
};
|
||||
export const WidgetSidebar = () => (
|
||||
<div className="widget-sidebar">
|
||||
<div className="d-flex">
|
||||
<RecommendationsPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default WidgetSidebar;
|
||||
|
||||
@@ -1,49 +1,14 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
|
||||
import WidgetSidebar from '.';
|
||||
|
||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||
jest.mock('widgets/ProductRecommendations/hooks', () => ({
|
||||
useShowRecommendationsFooter: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('WidgetSidebar', () => {
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
const props = {
|
||||
setSidebarShowing: jest.fn(),
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
test('default', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.activeControl,
|
||||
);
|
||||
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||
|
||||
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
|
||||
const wrapper = shallow(<WidgetSidebar />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('is hidden when the has the default values', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.default,
|
||||
);
|
||||
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||
|
||||
expect(props.setSidebarShowing).not.toHaveBeenCalled();
|
||||
expect(wrapper.type()).toBeNull();
|
||||
});
|
||||
|
||||
test('is hidden when the has the treatment values', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.activeTreatment,
|
||||
);
|
||||
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||
|
||||
expect(props.setSidebarShowing).not.toHaveBeenCalled();
|
||||
expect(wrapper.type()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import RecommendationsPanel from 'widgets/RecommendationsPanel';
|
||||
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||
|
||||
export const WidgetSidebar = ({ setSidebarShowing }) => {
|
||||
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
|
||||
|
||||
if (!inRecommendationsVariant && isExperimentActive) {
|
||||
setSidebarShowing(true);
|
||||
|
||||
return (
|
||||
<div className="widget-sidebar px-2">
|
||||
<div className="d-flex">
|
||||
<RecommendationsPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
WidgetSidebar.propTypes = {
|
||||
setSidebarShowing: PropTypes.func.isRequired,
|
||||
};
|
||||
export const WidgetSidebar = () => (
|
||||
<div className="widget-sidebar px-2">
|
||||
<div className="d-flex">
|
||||
<RecommendationsPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default WidgetSidebar;
|
||||
|
||||
@@ -1,49 +1,14 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
|
||||
import WidgetSidebar from '.';
|
||||
|
||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||
jest.mock('widgets/ProductRecommendations/hooks', () => ({
|
||||
useShowRecommendationsFooter: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('WidgetSidebar', () => {
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
const props = {
|
||||
setSidebarShowing: jest.fn(),
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
test('default', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.activeControl,
|
||||
);
|
||||
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||
|
||||
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
|
||||
const wrapper = shallow(<WidgetSidebar />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('is hidden when the has the default values', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.default,
|
||||
);
|
||||
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||
|
||||
expect(props.setSidebarShowing).not.toHaveBeenCalled();
|
||||
expect(wrapper.type()).toBeNull();
|
||||
});
|
||||
|
||||
test('is hidden when the has the treatment values', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.activeTreatment,
|
||||
);
|
||||
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||
|
||||
expect(props.setSidebarShowing).not.toHaveBeenCalled();
|
||||
expect(wrapper.type()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WidgetFooter snapshots default 1`] = `
|
||||
<div
|
||||
className="widget-footer"
|
||||
>
|
||||
<ProductRecommendations />
|
||||
</div>
|
||||
`;
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import ProductRecommendations from 'widgets/ProductRecommendations';
|
||||
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||
|
||||
export const WidgetFooter = () => {
|
||||
hooks.useActivateRecommendationsExperiment();
|
||||
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
|
||||
|
||||
if (inRecommendationsVariant && isExperimentActive) {
|
||||
return (
|
||||
<div className="widget-footer">
|
||||
<ProductRecommendations />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default WidgetFooter;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
|
||||
import WidgetFooter from '.';
|
||||
|
||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||
jest.mock('widgets/ProductRecommendations/hooks', () => ({
|
||||
useActivateRecommendationsExperiment: jest.fn(),
|
||||
useShowRecommendationsFooter: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('WidgetFooter', () => {
|
||||
describe('snapshots', () => {
|
||||
test('default', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.activeTreatment,
|
||||
);
|
||||
const wrapper = shallow(<WidgetFooter />);
|
||||
|
||||
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('is hidden when the experiment has the default values', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.default,
|
||||
);
|
||||
const wrapper = shallow(<WidgetFooter />);
|
||||
|
||||
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
|
||||
expect(wrapper.type()).toBeNull();
|
||||
});
|
||||
|
||||
test('is hidden when the experiment has the control values', () => {
|
||||
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||
mockFooterRecommendationsHook.activeControl,
|
||||
);
|
||||
const wrapper = shallow(<WidgetFooter />);
|
||||
|
||||
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
|
||||
expect(wrapper.type()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WidgetNavbar snapshots default 1`] = `
|
||||
<RecommendationsPaintedDoorBtn
|
||||
experimentVariation=""
|
||||
placement="expendedNavbar"
|
||||
/>
|
||||
`;
|
||||
@@ -1,29 +0,0 @@
|
||||
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;
|
||||
@@ -1,65 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
// Constants related to courses
|
||||
export const COURSE_MODES = {
|
||||
VERIFIED: 'verified',
|
||||
PROFESSIONAL: 'professional',
|
||||
NO_ID_PROFESSIONAL: 'no-id-professional',
|
||||
AUDIT: 'audit',
|
||||
HONOR: 'honor',
|
||||
EXECUTIVE_EDUCATION: 'executive-education',
|
||||
PAID_EXECUTIVE_EDUCATION: 'paid-executive-education',
|
||||
UNPAID_EXECUTIVE_EDUCATION: 'unpaid-executive-education',
|
||||
};
|
||||
|
||||
export const EXECUTIVE_EDUCATION_COURSE_MODES = [
|
||||
COURSE_MODES.EXECUTIVE_EDUCATION,
|
||||
COURSE_MODES.PAID_EXECUTIVE_EDUCATION,
|
||||
COURSE_MODES.UNPAID_EXECUTIVE_EDUCATION,
|
||||
];
|
||||
@@ -1,6 +1,5 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||
|
||||
import * as module from './courseCard';
|
||||
import * as simpleSelectors from './simpleSelectors';
|
||||
@@ -16,14 +15,18 @@ export const loadDateVal = (date) => (date ? new Date(date) : null);
|
||||
export const courseCard = StrictDict({
|
||||
certificate: mkCardSelector(
|
||||
cardSimpleSelectors.certificate,
|
||||
(certificate) => (certificate === null ? {} : ({
|
||||
availableDate: new Date(certificate.availableDate),
|
||||
certPreviewUrl: baseAppUrl(certificate.certPreviewUrl),
|
||||
isDownloadable: certificate.isDownloadable,
|
||||
isEarnedButUnavailable: certificate.isEarned && new Date(certificate.availableDate) > new Date(),
|
||||
isRestricted: certificate.isRestricted,
|
||||
isEarned: certificate.isEarned,
|
||||
})),
|
||||
(certificate) => {
|
||||
const availableDate = new Date(certificate.availableDate);
|
||||
const isAvailable = availableDate <= new Date();
|
||||
return {
|
||||
availableDate,
|
||||
certPreviewUrl: baseAppUrl(certificate.certPreviewUrl),
|
||||
isDownloadable: certificate.isDownloadable,
|
||||
isEarnedButUnavailable: certificate.isEarned && !isAvailable,
|
||||
isRestricted: certificate.isRestricted,
|
||||
isEarned: certificate.isEarned,
|
||||
};
|
||||
},
|
||||
),
|
||||
course: mkCardSelector(
|
||||
cardSimpleSelectors.course,
|
||||
@@ -98,8 +101,6 @@ export const courseCard = StrictDict({
|
||||
|
||||
isEmailEnabled: enrollment.isEmailEnabled,
|
||||
hasOptedOutOfEmail: enrollment.hasOptedOutOfEmail,
|
||||
mode: enrollment.mode,
|
||||
isExecEd2UCourse: EXECUTIVE_EDUCATION_COURSE_MODES.includes(enrollment.mode),
|
||||
};
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { keyStore } from 'utils';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||
|
||||
import simpleSelectors from './simpleSelectors';
|
||||
import * as module from './courseCard';
|
||||
@@ -80,9 +79,6 @@ describe('courseCard selectors module', () => {
|
||||
it('returns a card selector based on certificate cardSimpleSelector', () => {
|
||||
expect(simpleSelector).toEqual(cardSimpleSelectors.certificate);
|
||||
});
|
||||
it('returns {} object if null certificate received', () => {
|
||||
expect(selector(null)).toEqual({});
|
||||
});
|
||||
it('passes availableDate, converted to a date', () => {
|
||||
expect(selected.availableDate).toMatchObject(new Date(testData.availableDate));
|
||||
});
|
||||
@@ -166,9 +162,6 @@ describe('courseCard selectors module', () => {
|
||||
it('returns a card selector based on courseRun cardSimpleSelector', () => {
|
||||
expect(simpleSelector).toEqual(cardSimpleSelectors.courseRun);
|
||||
});
|
||||
it('returns {} object if null courseRun received', () => {
|
||||
expect(selector(null)).toEqual({});
|
||||
});
|
||||
it('passes [endDate, startDate], converted to dates', () => {
|
||||
expect(selected.endDate).toEqual(new Date(testData.endDate));
|
||||
expect(selected.startDate).toEqual(new Date(testData.startDate));
|
||||
@@ -229,25 +222,23 @@ describe('courseCard selectors module', () => {
|
||||
});
|
||||
});
|
||||
describe('enrollment selector', () => {
|
||||
const defaultData = {
|
||||
coursewareAccess: {
|
||||
isStaff: false,
|
||||
hasUnmetPrereqs: false,
|
||||
isTooEarly: false,
|
||||
},
|
||||
isEnrolled: 'test-is-enrolled',
|
||||
lastEnrolled: 'test-last-enrolled',
|
||||
hasStarted: 'test-has-started',
|
||||
accessExpirationDate: '3000-10-20',
|
||||
canUpgrade: 'test-can-upgrade',
|
||||
isAudit: 'test-is-audit',
|
||||
isAuditAccessExpired: 'test-is-audit-access-expired',
|
||||
isVerified: 'test-is-verified',
|
||||
isEmailEnabled: 'test-is-email-enabled',
|
||||
mode: 'default',
|
||||
};
|
||||
beforeEach(() => {
|
||||
loadSelector(courseCard.enrollment, defaultData);
|
||||
loadSelector(courseCard.enrollment, {
|
||||
coursewareAccess: {
|
||||
isStaff: false,
|
||||
hasUnmetPrereqs: false,
|
||||
isTooEarly: false,
|
||||
},
|
||||
isEnrolled: 'test-is-enrolled',
|
||||
lastEnrolled: 'test-last-enrolled',
|
||||
hasStarted: 'test-has-started',
|
||||
accessExpirationDate: '3000-10-20',
|
||||
canUpgrade: 'test-can-upgrade',
|
||||
isAudit: 'test-is-audit',
|
||||
isAuditAccessExpired: 'test-is-audit-access-expired',
|
||||
isVerified: 'test-is-verified',
|
||||
isEmailEnabled: 'test-is-email-enabled',
|
||||
});
|
||||
});
|
||||
it('returns a card selector based on enrollment cardSimpleSelector', () => {
|
||||
expect(simpleSelector).toEqual(cardSimpleSelectors.enrollment);
|
||||
@@ -283,13 +274,6 @@ describe('courseCard selectors module', () => {
|
||||
it('passes isEmailEnabled', () => {
|
||||
expect(selected.isEmailEnabled).toEqual(testData.isEmailEnabled);
|
||||
});
|
||||
it('returns isExecEd2UCourse: false if mode is not in EXECUTIVE_EDUCATION_COURSE_MODES', () => {
|
||||
expect(selected.isExecEd2UCourse).toEqual(false);
|
||||
});
|
||||
it('returns isExecEd2UCourse: true if mode is in EXECUTIVE_EDUCATION_COURSE_MODES', () => {
|
||||
loadSelector(courseCard.enrollment, { ...defaultData, mode: EXECUTIVE_EDUCATION_COURSE_MODES[0] });
|
||||
expect(selected.isExecEd2UCourse).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('entitlement selector', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const simpleSelectors = StrictDict({
|
||||
platformSettings: mkSimpleSelector(app => app.platformSettings),
|
||||
suggestedCourses: mkSimpleSelector(app => app.suggestedCourses),
|
||||
emailConfirmation: mkSimpleSelector(app => app.emailConfirmation),
|
||||
enterpriseDashboard: mkSimpleSelector(app => app.enterpriseDashboard || {}),
|
||||
enterpriseDashboard: mkSimpleSelector(app => app.enterpriseDashboard),
|
||||
selectSessionModal: mkSimpleSelector(app => app.selectSessionModal),
|
||||
pageNumber: mkSimpleSelector(app => app.pageNumber),
|
||||
socialShareSettings: mkSimpleSelector(app => app.socialShareSettings),
|
||||
|
||||
@@ -35,12 +35,6 @@ describe('app simple selectors', () => {
|
||||
expect(preSelectors).toEqual([appSelector]);
|
||||
expect(cb(testState.app)).toEqual(testString);
|
||||
});
|
||||
test('enterpriseDashboard returns empty object if data returns null', () => {
|
||||
testState = { app: { enterpriseDashboard: null } };
|
||||
const { preSelectors, cb } = simpleSelectors.enterpriseDashboard;
|
||||
expect(preSelectors).toEqual([appSelector]);
|
||||
expect(cb(testState.app)).toEqual({});
|
||||
});
|
||||
describe('cardSimpleSelectors', () => {
|
||||
keys = keyStore(cardSimpleSelectors);
|
||||
test.each([
|
||||
|
||||
@@ -55,12 +55,6 @@ export const useCardSocialSettingsData = (cardId) => {
|
||||
return { facebook: loadSettings(facebook), twitter: loadSettings(twitter) };
|
||||
};
|
||||
|
||||
export const useCardExecEdTrackingParam = (cardId) => {
|
||||
const { isExecEd2UCourse } = module.useCardEnrollmentData(cardId);
|
||||
const { authOrgId } = module.useEnterpriseDashboardData(cardId);
|
||||
return isExecEd2UCourse ? `?org_id=${authOrgId}` : '';
|
||||
};
|
||||
|
||||
/** Events **/
|
||||
export const useUpdateSelectSessionModalCallback = (cardId) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -17,7 +17,7 @@ import * as module from './api';
|
||||
* GET Actions
|
||||
*********************************************************************************/
|
||||
export const initializeList = ({ user } = {}) => get(
|
||||
stringifyUrl(urls.getInitApiUrl(), { [apiKeys.user]: user }),
|
||||
stringifyUrl(urls.init, { [apiKeys.user]: user }),
|
||||
);
|
||||
|
||||
export const updateEntitlementEnrollment = ({ uuid, courseId }) => post(
|
||||
@@ -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,
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('lms api methods', () => {
|
||||
[apiKeys.user]: testUser,
|
||||
};
|
||||
expect(api.initializeList(userArg)).toEqual(
|
||||
utils.get(utils.stringifyUrl(urls.getInitApiUrl(), userArg)),
|
||||
utils.get(utils.stringifyUrl(urls.init, userArg)),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,13 +761,10 @@ export const compileCourseRunData = ({ courseName, ...data }, index) => {
|
||||
credit: {},
|
||||
...data,
|
||||
certificate: genCertificateData(data.certificate),
|
||||
enrollment: genEnrollmentData({
|
||||
lastEnrolled,
|
||||
...getOption(emailOptions, index),
|
||||
...data.enrollment,
|
||||
}),
|
||||
enrollment: genEnrollmentData({ lastEnrolled, ...data.enrollment }),
|
||||
courseRun: genCourseRunData({
|
||||
...data.courseRun,
|
||||
...getOption(emailOptions, index),
|
||||
courseId,
|
||||
}),
|
||||
course: {
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { configuration } from 'config';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
const baseUrl = `${configuration.LMS_BASE_URL}`;
|
||||
export const ecommerceUrl = `${configuration.ECOMMERCE_BASE_URL}`;
|
||||
|
||||
export const getEcommerceUrl = () => getConfig().ECOMMERCE_BASE_URL;
|
||||
export const api = `${baseUrl}/api`;
|
||||
|
||||
const getBaseUrl = () => getConfig().LMS_BASE_URL;
|
||||
// const init = `${api}learner_home/mock/init`; // mock endpoint for testing
|
||||
const init = `${api}/learner_home/init`;
|
||||
|
||||
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 entitlementEnrollment = (uuid) => `${getApiUrl()}/entitlements/v1/entitlements/${uuid}/enrollments`;
|
||||
const event = `${baseUrl}/event`;
|
||||
const courseUnenroll = `${baseUrl}/change_enrollment`;
|
||||
const updateEmailSettings = `${api}/change_email_settings`;
|
||||
const entitlementEnrollment = (uuid) => `${api}/entitlements/v1/entitlements/${uuid}/enrollments`;
|
||||
|
||||
// if url is null or absolute, return it as is
|
||||
export const updateUrl = (base, url) => ((url == null || url.startsWith('http://') || url.startsWith('https://')) ? url : `${base}${url}`);
|
||||
const updateUrl = (base, url) => ((url == null || url.startsWith('http://') || url.startsWith('https://')) ? url : `${base}${url}`);
|
||||
|
||||
export const baseAppUrl = (url) => updateUrl(getBaseUrl(), url);
|
||||
export const learningMfeUrl = (url) => updateUrl(getConfig().LEARNING_BASE_URL, url);
|
||||
export const baseAppUrl = (url) => updateUrl(baseUrl, url);
|
||||
export const learningMfeUrl = (url) => updateUrl(configuration.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/`;
|
||||
export const creditPurchaseUrl = (courseId) => `${ecommerceUrl}/credit/checkout/${courseId}/`;
|
||||
export const creditRequestUrl = (providerId) => `${api}/credit/v1/providers/${providerId}/request/`;
|
||||
|
||||
export default StrictDict({
|
||||
getApiUrl,
|
||||
api,
|
||||
baseAppUrl,
|
||||
courseUnenroll,
|
||||
creditPurchaseUrl,
|
||||
creditRequestUrl,
|
||||
entitlementEnrollment,
|
||||
event,
|
||||
getInitApiUrl,
|
||||
init,
|
||||
learningMfeUrl,
|
||||
programsUrl,
|
||||
updateEmailSettings,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { configuration } from 'config';
|
||||
import * as urls from './urls';
|
||||
|
||||
describe('urls', () => {
|
||||
@@ -10,7 +10,7 @@ describe('urls', () => {
|
||||
it('returns the url if it is relative', () => {
|
||||
const url = '/edx.org';
|
||||
expect(urls.baseAppUrl(url)).toEqual(
|
||||
`${getConfig().LMS_BASE_URL}${url}`,
|
||||
`${configuration.LMS_BASE_URL}${url}`,
|
||||
);
|
||||
});
|
||||
it('return null if url is null', () => {
|
||||
@@ -25,7 +25,7 @@ describe('urls', () => {
|
||||
it('returns the url if it is relative', () => {
|
||||
const url = '/edx.org';
|
||||
expect(urls.learningMfeUrl(url)).toEqual(
|
||||
`${getConfig().LEARNING_BASE_URL}${url}`,
|
||||
`${configuration.LEARNING_BASE_URL}${url}`,
|
||||
);
|
||||
});
|
||||
it('return null if url is null', () => {
|
||||
@@ -36,6 +36,7 @@ describe('urls', () => {
|
||||
it('builds from ecommerce url and loads courseId', () => {
|
||||
const courseId = 'test-course-id';
|
||||
const url = urls.creditPurchaseUrl(courseId);
|
||||
expect(url.startsWith(urls.ecommerceUrl)).toEqual(true);
|
||||
expect(url).toEqual(expect.stringContaining(courseId));
|
||||
});
|
||||
});
|
||||
@@ -43,7 +44,7 @@ describe('urls', () => {
|
||||
it('builds from api url and loads providerId', () => {
|
||||
const providerId = 'test-provider-id';
|
||||
const url = urls.creditRequestUrl(providerId);
|
||||
expect(url.startsWith(urls.getApiUrl())).toEqual(true);
|
||||
expect(url.startsWith(urls.api)).toEqual(true);
|
||||
expect(url).toEqual(expect.stringContaining(providerId));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
|
||||
import arMessages from './messages/ar.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
@@ -21,5 +20,4 @@ const appMessages = {
|
||||
export default [
|
||||
footerMessages,
|
||||
appMessages,
|
||||
paragonMessages,
|
||||
];
|
||||
|
||||
@@ -14,12 +14,25 @@
|
||||
"leanerDashboard.confirmEmailModalHeader": "قم بتأكيد بريدك الإلكتروني",
|
||||
"leanerDashboard.confirmEmailModalBody": "لقد أرسلنا إليك بريدًا إلكترونيًا للتحقق من حسابك. يرجى التحقق من بريدك الوارد والنقر على الزر الأحمر الكبير للتأكيد ومتابعة التعلم.",
|
||||
"leanerDashboard.confirmEmailImageAlt": "ايقونة زر تأكيد البريد الإلكتروني",
|
||||
"leanerDashboard.menu.dashboard.label": "لوحة التحكم",
|
||||
"leanerDashboard.help.label": "المساعدة",
|
||||
"leanerDashboard.menu.profile.label": "الملف الشخصي",
|
||||
"leanerDashboard.menu.viewPrograms.label": "عرض البرامج",
|
||||
"leanerDashboard.menu.account.label": "الحساب",
|
||||
"leanerDashboard.menu.orderHistory.label": "المشتريات السابقة",
|
||||
"leanerDashboard.menu.signOut.label": "تسجيل الخروج",
|
||||
"greeting.morning": "صباح الخير!",
|
||||
"greeting.afternoon": "مساء الخير!",
|
||||
"greeting.evening": "مساء الخير!",
|
||||
"leanerDashboard.switchToProgram": "انتقل إلى لائحة البرامج",
|
||||
"leanerDashboard.exploreCourses": "استكشف المساقات",
|
||||
"leanerDashboard.courseSearchAlt": "البحث عن مساق",
|
||||
"learnerVariantDashboard.menu.dashboard.label": "لوحة التحكم",
|
||||
"learnerVariantDashboard.help.label": "المساعدة",
|
||||
"learnerVariantDashboard.menu.profile.label": "الملف الشخصي",
|
||||
"learnerVariantDashboard.menu.viewPrograms.label": "عرض البرامج",
|
||||
"learnerVariantDashboard.menu.account.label": "الحساب",
|
||||
"learnerVariantDashboard.menu.orderHistory.label": "Order History",
|
||||
"learnerVariantDashboard.menu.orderHistory.label": "المشتريات السابقة",
|
||||
"learnerVariantDashboard.menu.signOut.label": "تسجيل الخروج",
|
||||
"learnerVariantDashboard.course": "المساقات",
|
||||
"learnerVariantDashboard.program": "البرامج",
|
||||
@@ -27,8 +40,6 @@
|
||||
"learnerVariantDashboard.logoAltText": ". لوحة القيادة",
|
||||
"learnerVariantDashboard.collapseMenuOpenAltText": "القائمة",
|
||||
"learnerVariantDashboard.collapseMenuClosedAltText": "إغلاق",
|
||||
"leanerDashboard.menu.career.label": "Career",
|
||||
"header.menu.new.label": "New",
|
||||
"MasqueradeBar.ViewAs": "عرض كـ :",
|
||||
"MasqueradeBar.ViewingAs": "يتم عرض كـ:",
|
||||
"MasqueradeBar.SubmitButton": "تأكيد",
|
||||
@@ -37,20 +48,6 @@
|
||||
"MasqueradeBar.UnknownError": "حدث خطأ غير معروف",
|
||||
"WidgetSidebar.lookingForChallengePrompt": "هل تبحث عن تحد جديد؟",
|
||||
"WidgetSidebar.findCoursesButton": "ابحث عن مساق {arrow}",
|
||||
"ProductRecommendations.recommendationsHeading": "You might also like",
|
||||
"ProductRecommendations.executiveEducationHeading": "Executive Education",
|
||||
"ProductRecommendations.executiveEducationDescription": "Short Courses to develop leadership skills",
|
||||
"ProductRecommendations.bootcampHeading": "Boot Camp",
|
||||
"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": "استكشف المساقات"
|
||||
|
||||
@@ -7,13 +7,26 @@
|
||||
"leanerDashboard.enterpriseDialogHeader": "Tienes acceso al panel {label}",
|
||||
"leanerDashboard.enterpriseDialogBody": "Para acceder a los cursos disponibles para usted a través de {label}, visite el panel {label} ahora.",
|
||||
"leanerDashboard.enterpriseDialogDismissButton": "Despedir",
|
||||
"leanerDashboard.enterpriseDialogConfirmButton": "Ir al panel principal",
|
||||
"leanerDashboard.enterpriseDialogConfirmButton": "Go to dashboard",
|
||||
"leanerDashboard.confirmEmailBanner": "Confirmar ahora",
|
||||
"leanerDashboard.confirmEmailTextReminderBanner": "¡Recuerda confirmar tu correo electrónico para que puedas seguir aprendiendo en edX! {confirmNowButton}.",
|
||||
"leanerDashboard.verifiedConfirmEmailButton": "He confirmado mi correo electrónico.",
|
||||
"leanerDashboard.confirmEmailModalHeader": "confirme su email",
|
||||
"leanerDashboard.confirmEmailModalBody": "Te hemos enviado un correo electrónico para verificar tu cuenta. Revise su bandeja de entrada y haga clic en el botón rojo grande para confirmar y seguir aprendiendo.",
|
||||
"leanerDashboard.confirmEmailImageAlt": "confirmar fondo de correo electrónico",
|
||||
"leanerDashboard.menu.dashboard.label": "Panel de Control",
|
||||
"leanerDashboard.help.label": "Ayuda",
|
||||
"leanerDashboard.menu.profile.label": "Perfil",
|
||||
"leanerDashboard.menu.viewPrograms.label": "Ver programas",
|
||||
"leanerDashboard.menu.account.label": "Cuenta",
|
||||
"leanerDashboard.menu.orderHistory.label": "Historial de órdenes",
|
||||
"leanerDashboard.menu.signOut.label": "Cerrar sesión",
|
||||
"greeting.morning": "¡Buen día!",
|
||||
"greeting.afternoon": "¡Buenas tardes!",
|
||||
"greeting.evening": "¡Buenas noches!",
|
||||
"leanerDashboard.switchToProgram": "Cambiar a Programas",
|
||||
"leanerDashboard.exploreCourses": "Explorar cursos",
|
||||
"leanerDashboard.courseSearchAlt": "Búsqueda de cursos",
|
||||
"learnerVariantDashboard.menu.dashboard.label": "Panel de Control",
|
||||
"learnerVariantDashboard.help.label": "Ayuda",
|
||||
"learnerVariantDashboard.menu.profile.label": "Perfil",
|
||||
@@ -27,8 +40,6 @@
|
||||
"learnerVariantDashboard.logoAltText": "Tablero de edX, Inc.",
|
||||
"learnerVariantDashboard.collapseMenuOpenAltText": "Menú",
|
||||
"learnerVariantDashboard.collapseMenuClosedAltText": "Cerrar",
|
||||
"leanerDashboard.menu.career.label": "Carrera",
|
||||
"header.menu.new.label": "Nuevo",
|
||||
"MasqueradeBar.ViewAs": "Ver como:",
|
||||
"MasqueradeBar.ViewingAs": "Viendo como:",
|
||||
"MasqueradeBar.SubmitButton": "Enviar",
|
||||
@@ -37,20 +48,6 @@
|
||||
"MasqueradeBar.UnknownError": "Un error desconocido ocurrió",
|
||||
"WidgetSidebar.lookingForChallengePrompt": "¿Buscando un nuevo reto?",
|
||||
"WidgetSidebar.findCoursesButton": "Encuentra un curso {arrow}",
|
||||
"ProductRecommendations.recommendationsHeading": "También podría ser de interés",
|
||||
"ProductRecommendations.executiveEducationHeading": "Formación Ejecutiva",
|
||||
"ProductRecommendations.executiveEducationDescription": "Cursos cortos para desarrollar habilidades de liderazgo",
|
||||
"ProductRecommendations.bootcampHeading": "Capacitación intensiva de corta duración",
|
||||
"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"
|
||||
|
||||
@@ -14,12 +14,25 @@
|
||||
"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.",
|
||||
"leanerDashboard.confirmEmailImageAlt": "confirmer l'arrière-plan du courriel",
|
||||
"leanerDashboard.menu.dashboard.label": "Tableau de bord",
|
||||
"leanerDashboard.help.label": "Aide",
|
||||
"leanerDashboard.menu.profile.label": "Profil",
|
||||
"leanerDashboard.menu.viewPrograms.label": "Voir les programmes",
|
||||
"leanerDashboard.menu.account.label": "Compte",
|
||||
"leanerDashboard.menu.orderHistory.label": "Historique des commandes",
|
||||
"leanerDashboard.menu.signOut.label": "Se déconnecter",
|
||||
"greeting.morning": "Bonjour!",
|
||||
"greeting.afternoon": "Bon après-midi!",
|
||||
"greeting.evening": "Bonne soirée!",
|
||||
"leanerDashboard.switchToProgram": "Passer aux programmes",
|
||||
"leanerDashboard.exploreCourses": "Explorer les cours",
|
||||
"leanerDashboard.courseSearchAlt": "Recherche de cours",
|
||||
"learnerVariantDashboard.menu.dashboard.label": "Tableau de bord",
|
||||
"learnerVariantDashboard.help.label": "Aide",
|
||||
"learnerVariantDashboard.menu.profile.label": "Profil",
|
||||
"learnerVariantDashboard.menu.viewPrograms.label": "Voir les programmes",
|
||||
"learnerVariantDashboard.menu.account.label": "Compte",
|
||||
"learnerVariantDashboard.menu.orderHistory.label": "Order History",
|
||||
"learnerVariantDashboard.menu.orderHistory.label": "Historique des commandes",
|
||||
"learnerVariantDashboard.menu.signOut.label": "Se déconnecter",
|
||||
"learnerVariantDashboard.course": "Cours",
|
||||
"learnerVariantDashboard.program": "Programmes",
|
||||
@@ -27,8 +40,6 @@
|
||||
"learnerVariantDashboard.logoAltText": "Tableau de bord edX, Inc.",
|
||||
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
|
||||
"learnerVariantDashboard.collapseMenuClosedAltText": "Fermer",
|
||||
"leanerDashboard.menu.career.label": "Career",
|
||||
"header.menu.new.label": "New",
|
||||
"MasqueradeBar.ViewAs": "Vue de :",
|
||||
"MasqueradeBar.ViewingAs": "Voir comme : ",
|
||||
"MasqueradeBar.SubmitButton": "Envoyez",
|
||||
@@ -37,20 +48,6 @@
|
||||
"MasqueradeBar.UnknownError": "Une erreur d'origine inconnue s'est produite.",
|
||||
"WidgetSidebar.lookingForChallengePrompt": "A la recherche d'un nouveau défi?",
|
||||
"WidgetSidebar.findCoursesButton": "Trouver un cours {arrow}",
|
||||
"ProductRecommendations.recommendationsHeading": "You might also like",
|
||||
"ProductRecommendations.executiveEducationHeading": "Executive Education",
|
||||
"ProductRecommendations.executiveEducationDescription": "Short Courses to develop leadership skills",
|
||||
"ProductRecommendations.bootcampHeading": "Boot Camp",
|
||||
"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,11 +9,24 @@
|
||||
"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 EDUlib! {confirmNowButton}.",
|
||||
"leanerDashboard.confirmEmailTextReminderBanner": "N'oubliez pas de confirmer votre courriel afin de pouvoir continuer à apprendre sur edX ! {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.",
|
||||
"leanerDashboard.confirmEmailImageAlt": "confirmer l'arrière-plan du courriel",
|
||||
"leanerDashboard.menu.dashboard.label": "Tableau de bord",
|
||||
"leanerDashboard.help.label": "Aide",
|
||||
"leanerDashboard.menu.profile.label": "Profil",
|
||||
"leanerDashboard.menu.viewPrograms.label": "Voir les programmes",
|
||||
"leanerDashboard.menu.account.label": "Compte",
|
||||
"leanerDashboard.menu.orderHistory.label": "Historique des commandes",
|
||||
"leanerDashboard.menu.signOut.label": "Se déconnecter",
|
||||
"greeting.morning": "Bonjour!",
|
||||
"greeting.afternoon": "Bon après-midi!",
|
||||
"greeting.evening": "Bonne soirée!",
|
||||
"leanerDashboard.switchToProgram": "Passer aux programmes",
|
||||
"leanerDashboard.exploreCourses": "Explorer les cours",
|
||||
"leanerDashboard.courseSearchAlt": "Recherche de cours",
|
||||
"learnerVariantDashboard.menu.dashboard.label": "Tableau de bord",
|
||||
"learnerVariantDashboard.help.label": "Aide",
|
||||
"learnerVariantDashboard.menu.profile.label": "Profil",
|
||||
@@ -24,33 +37,17 @@
|
||||
"learnerVariantDashboard.course": "Cours",
|
||||
"learnerVariantDashboard.program": "Programmes",
|
||||
"learnerVariantDashboard.discoverNew": "Découvrir les nouveautés",
|
||||
"learnerVariantDashboard.logoAltText": "Tableau de bord EDUlib, Inc.",
|
||||
"learnerVariantDashboard.logoAltText": "Tableau de bord edX, Inc.",
|
||||
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
|
||||
"learnerVariantDashboard.collapseMenuClosedAltText": "Fermer",
|
||||
"leanerDashboard.menu.career.label": "Carrière",
|
||||
"header.menu.new.label": "Nouveau",
|
||||
"MasqueradeBar.ViewAs": "Consulter comme :",
|
||||
"MasqueradeBar.ViewingAs": "Affichage en tant que :",
|
||||
"MasqueradeBar.SubmitButton": "Soumettre",
|
||||
"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",
|
||||
"ProductRecommendations.executiveEducationDescription": "Cours abrégés pour développer les compétences en leadership",
|
||||
"ProductRecommendations.bootcampHeading": "Camp d'entraînement",
|
||||
"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"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user