Compare commits
47 Commits
open-relea
...
schen/upse
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdb0f48f08 | ||
|
|
3cdcc1fe61 | ||
|
|
82ff0d7ddb | ||
|
|
1478956e34 | ||
|
|
f049712430 | ||
|
|
c977de2df9 | ||
|
|
4b20c5bbdd | ||
|
|
0c1fa2f030 | ||
|
|
91117cce6a | ||
|
|
e6dba8bdc2 | ||
|
|
1d67ac5f24 | ||
|
|
60d2f22c50 | ||
|
|
5dc89d7404 | ||
|
|
0f24d3a52d | ||
|
|
fc885d02dc | ||
|
|
2e09d3632e | ||
|
|
d8cb46da60 | ||
|
|
199d6e7c60 | ||
|
|
64563d58f9 | ||
|
|
1e9a0a87b6 | ||
|
|
d42d0cdc59 | ||
|
|
8fef92d94d | ||
|
|
b41eee47c9 | ||
|
|
909f3f1f47 | ||
|
|
ce269e8c8f | ||
|
|
86a4573405 | ||
|
|
be2258e409 | ||
|
|
be8cb85773 | ||
|
|
a2c003e542 | ||
|
|
f1cfe3de68 | ||
|
|
d43c17a663 | ||
|
|
c01042f1df | ||
|
|
ed2368222f | ||
|
|
103a67654c | ||
|
|
58c3720087 | ||
|
|
4e47018a81 | ||
|
|
e7d9255fe5 | ||
|
|
2c7e10ffc2 | ||
|
|
43aa5b088e | ||
|
|
86b1f5df1a | ||
|
|
5c52b6861e | ||
|
|
a358a6014f | ||
|
|
6ebc94506b | ||
|
|
59ab63807f | ||
|
|
322a79afaa | ||
|
|
c458f4942f | ||
|
|
93a4dfb4d9 |
2
.env
2
.env
@@ -40,3 +40,5 @@ ACCOUNT_SETTINGS_URL=''
|
|||||||
ACCOUNT_PROFILE_URL=''
|
ACCOUNT_PROFILE_URL=''
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
CAREER_LINK_URL=''
|
CAREER_LINK_URL=''
|
||||||
|
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||||
|
EXPERIMENT_08_23_VAN_PAINTED_DOOR='true'
|
||||||
|
|||||||
@@ -47,3 +47,5 @@ ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
|||||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
CAREER_LINK_URL=''
|
CAREER_LINK_URL=''
|
||||||
|
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||||
|
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true
|
||||||
|
|||||||
@@ -46,3 +46,5 @@ ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
|
|||||||
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
|
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
|
||||||
ENABLE_NOTICES=''
|
ENABLE_NOTICES=''
|
||||||
CAREER_LINK_URL=''
|
CAREER_LINK_URL=''
|
||||||
|
OPTIMIZELY_FULL_STACK_SDK_KEY='SDK Key'
|
||||||
|
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true
|
||||||
|
|||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -49,9 +49,9 @@ jobs:
|
|||||||
server_port: 465
|
server_port: 465
|
||||||
username: ${{ secrets.EDX_SMTP_USERNAME }}
|
username: ${{ secrets.EDX_SMTP_USERNAME }}
|
||||||
password: ${{ secrets.EDX_SMTP_PASSWORD }}
|
password: ${{ secrets.EDX_SMTP_PASSWORD }}
|
||||||
subject: Upgrade python requirements workflow failed in ${{github.repository}}
|
subject: CI workflow failed in ${{github.repository}}
|
||||||
to: masters-grades@edx.org
|
to: masters-grades@edx.org
|
||||||
from: github-actions <github-actions@edx.org>
|
from: github-actions <github-actions@edx.org>
|
||||||
body: Upgrade python requirements workflow in ${{github.repository}} failed!
|
body: CI workflow in ${{github.repository}} failed!
|
||||||
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
|
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
|
||||||
}}"
|
}}"
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -64,10 +64,11 @@ pull_translations:
|
|||||||
mkdir src/i18n/messages
|
mkdir src/i18n/messages
|
||||||
cd src/i18n/messages \
|
cd src/i18n/messages \
|
||||||
&& atlas pull --filter=$(transifex_langs) \
|
&& atlas pull --filter=$(transifex_langs) \
|
||||||
|
translations/paragon/src/i18n/messages:paragon \
|
||||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||||
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
|
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
|
||||||
|
|
||||||
$(intl_imports) frontend-component-footer frontend-app-learner-dashboard
|
$(intl_imports) paragon frontend-component-footer frontend-app-learner-dashboard
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# This target is used by CI.
|
# This target is used by CI.
|
||||||
|
|||||||
8420
package-lock.json
generated
8420
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -28,14 +28,16 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
|
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
|
||||||
"@edx/browserslist-config": "^1.1.0",
|
"@edx/browserslist-config": "^1.1.0",
|
||||||
"@edx/frontend-component-footer": "^12.0.0",
|
"@edx/frontend-component-footer": "^12.2.1",
|
||||||
"@edx/frontend-enterprise-hotjar": "^1.2.0",
|
"@edx/frontend-enterprise-hotjar": "^2.0.0",
|
||||||
"@edx/frontend-platform": "^4.2.0",
|
"@edx/frontend-platform": "^5.5.4",
|
||||||
"@edx/paragon": "^20.32.0",
|
"@edx/paragon": "^20.44.0",
|
||||||
|
"@edx/react-unit-test-utils": "^1.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.15",
|
"@fortawesome/react-fontawesome": "^0.1.15",
|
||||||
|
"@optimizely/react-sdk": "^2.9.2",
|
||||||
"@redux-beacon/segment": "^1.1.0",
|
"@redux-beacon/segment": "^1.1.0",
|
||||||
"@reduxjs/toolkit": "^1.6.1",
|
"@reduxjs/toolkit": "^1.6.1",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
@@ -52,17 +54,18 @@
|
|||||||
"history": "5.0.1",
|
"history": "5.0.1",
|
||||||
"html-react-parser": "^1.3.0",
|
"html-react-parser": "^1.3.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
|
"jest-when": "^3.6.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"prop-types": "15.7.2",
|
"prop-types": "15.7.2",
|
||||||
"query-string": "7.0.1",
|
"query-string": "7.0.1",
|
||||||
"react": "^16.14.0",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^17.0.2",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-intl": "^5.20.9",
|
"react-intl": "^5.20.9",
|
||||||
"react-pdf": "^5.5.0",
|
"react-pdf": "^5.5.0",
|
||||||
"react-redux": "^7.2.4",
|
"react-redux": "^7.2.4",
|
||||||
"react-router-dom": "5.3.3",
|
"react-router-dom": "6.15.0",
|
||||||
"react-share": "^4.4.0",
|
"react-share": "^4.4.0",
|
||||||
"react-zendesk": "^0.1.13",
|
"react-zendesk": "^0.1.13",
|
||||||
"redux": "4.1.1",
|
"redux": "4.1.1",
|
||||||
@@ -81,15 +84,15 @@
|
|||||||
"@edx/reactifex": "^2.1.1",
|
"@edx/reactifex": "^2.1.1",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^12.1.0",
|
"@testing-library/react": "^12.1.0",
|
||||||
|
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
|
||||||
"axios-mock-adapter": "^1.20.0",
|
"axios-mock-adapter": "^1.20.0",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.6",
|
|
||||||
"fetch-mock": "^9.11.0",
|
"fetch-mock": "^9.11.0",
|
||||||
"husky": "^7.0.0",
|
"husky": "^7.0.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest-expect-message": "^1.0.2",
|
"jest-expect-message": "^1.0.2",
|
||||||
"react-dev-utils": "^11.0.4",
|
"react-dev-utils": "^11.0.4",
|
||||||
"react-test-renderer": "^16.14.0",
|
"react-test-renderer": "^17.0.2",
|
||||||
"redux-mock-store": "^1.5.4",
|
"redux-mock-store": "^1.5.4",
|
||||||
"semantic-release": "^20.1.3"
|
"semantic-release": "^20.1.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,15 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||||
|
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||||
|
<script
|
||||||
|
src="<%= process.env.OPTIMIZELY_URL %>"
|
||||||
|
></script>
|
||||||
|
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||||
|
<script
|
||||||
|
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||||
|
></script>
|
||||||
|
<% } %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
35
src/App.jsx
35
src/App.jsx
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
@@ -19,14 +18,16 @@ import {
|
|||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
import Dashboard from 'containers/Dashboard';
|
import Dashboard from 'containers/Dashboard';
|
||||||
import ZendeskFab from 'components/ZendeskFab';
|
import ZendeskFab from 'components/ZendeskFab';
|
||||||
|
import { ExperimentProvider } from 'ExperimentContext';
|
||||||
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
|
|
||||||
import fakeData from 'data/services/lms/fakeData/courses';
|
import fakeData from 'data/services/lms/fakeData/courses';
|
||||||
import LearnerDashboardHeader from './containers/LearnerDashboardHeader';
|
|
||||||
|
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
||||||
|
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
@@ -72,24 +73,30 @@ export const App = () => {
|
|||||||
}
|
}
|
||||||
}, [authenticatedUser, loadData]);
|
}, [authenticatedUser, loadData]);
|
||||||
return (
|
return (
|
||||||
<Router>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{formatMessage(messages.pageTitle)}</title>
|
<title>{formatMessage(messages.pageTitle)}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div>
|
<div>
|
||||||
<LearnerDashboardHeader />
|
<AppWrapper>
|
||||||
<main>
|
<LearnerDashboardHeader />
|
||||||
{hasNetworkFailure
|
<main>
|
||||||
? (
|
{hasNetworkFailure
|
||||||
<Alert variant="danger">
|
? (
|
||||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
<Alert variant="danger">
|
||||||
</Alert>
|
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||||
) : (<Dashboard />)}
|
</Alert>
|
||||||
</main>
|
) : (
|
||||||
|
<ExperimentProvider>
|
||||||
|
<Dashboard />
|
||||||
|
</ExperimentProvider>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</AppWrapper>
|
||||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||||
<ZendeskFab />
|
<ZendeskFab />
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
import { shallow } from '@edx/react-unit-test-utils';
|
||||||
|
|
||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Footer from '@edx/frontend-component-footer';
|
import Footer from '@edx/frontend-component-footer';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Alert } from '@edx/paragon';
|
|
||||||
|
|
||||||
import { RequestKeys } from 'data/constants/requests';
|
import { RequestKeys } from 'data/constants/requests';
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
import Dashboard from 'containers/Dashboard';
|
import Dashboard from 'containers/Dashboard';
|
||||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||||
|
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
||||||
|
import { ExperimentProvider } from 'ExperimentContext';
|
||||||
import { App } from './App';
|
import { App } from './App';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
@@ -21,6 +19,10 @@ jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
|||||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||||
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
|
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
|
||||||
|
jest.mock('ExperimentContext', () => ({
|
||||||
|
ExperimentProvider: 'ExperimentProvider',
|
||||||
|
}));
|
||||||
|
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
|
||||||
jest.mock('data/redux', () => ({
|
jest.mock('data/redux', () => ({
|
||||||
selectors: 'redux.selectors',
|
selectors: 'redux.selectors',
|
||||||
actions: 'redux.actions',
|
actions: 'redux.actions',
|
||||||
@@ -49,18 +51,23 @@ describe('App router component', () => {
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
describe('component', () => {
|
describe('component', () => {
|
||||||
const runBasicTests = () => {
|
const runBasicTests = () => {
|
||||||
test('snapshot', () => { expect(el).toMatchSnapshot(); });
|
test('snapshot', () => { expect(el.snapshot).toMatchSnapshot(); });
|
||||||
it('displays title in helmet component', () => {
|
it('displays title in helmet component', () => {
|
||||||
expect(el.find(Helmet).find('title').text()).toEqual(useIntl().formatMessage(messages.pageTitle));
|
const control = el.instance
|
||||||
|
.findByType(Helmet)[0]
|
||||||
|
.findByType('title')[0];
|
||||||
|
expect(control.children[0].el).toEqual(formatMessage(messages.pageTitle));
|
||||||
});
|
});
|
||||||
it('displays learner dashboard header', () => {
|
it('displays learner dashboard header', () => {
|
||||||
expect(el.find(LearnerDashboardHeader).length).toEqual(1);
|
expect(el.instance.findByType(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', () => {
|
test('Footer logo drawn from env variable', () => {
|
||||||
expect(el.find(Footer).props().logo).toEqual(logo);
|
expect(el.instance.findByType(Footer)[0].props.logo).toEqual(logo);
|
||||||
|
});
|
||||||
|
it('wraps the header and main components in an AppWrapper widget container', () => {
|
||||||
|
const container = el.instance.findByType(AppWrapper)[0];
|
||||||
|
expect(container.children[0].type).toEqual('LearnerDashboardHeader');
|
||||||
|
expect(container.children[1].type).toEqual('main');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
describe('no network failure', () => {
|
describe('no network failure', () => {
|
||||||
@@ -70,9 +77,14 @@ describe('App router component', () => {
|
|||||||
});
|
});
|
||||||
runBasicTests();
|
runBasicTests();
|
||||||
it('loads dashboard', () => {
|
it('loads dashboard', () => {
|
||||||
expect(el.find('main')).toMatchObject(shallow(
|
const main = el.instance.findByType('main')[0];
|
||||||
<main><Dashboard /></main>,
|
expect(main.children.length).toEqual(1);
|
||||||
));
|
const expProvider = main.children[0];
|
||||||
|
expect(expProvider.type).toEqual('ExperimentProvider');
|
||||||
|
expect(expProvider.children.length).toEqual(1);
|
||||||
|
expect(
|
||||||
|
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
|
||||||
|
).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('initialize failure', () => {
|
describe('initialize failure', () => {
|
||||||
@@ -82,13 +94,14 @@ describe('App router component', () => {
|
|||||||
});
|
});
|
||||||
runBasicTests();
|
runBasicTests();
|
||||||
it('loads error page', () => {
|
it('loads error page', () => {
|
||||||
expect(el.find('main')).toEqual(shallow(
|
const main = el.instance.findByType('main')[0];
|
||||||
<main>
|
expect(main.children.length).toEqual(1);
|
||||||
<Alert variant="danger">
|
const alert = main.children[0];
|
||||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
expect(alert.type).toEqual('Alert');
|
||||||
</Alert>
|
expect(alert.children.length).toEqual(1);
|
||||||
</main>,
|
const errorPage = alert.children[0];
|
||||||
));
|
expect(errorPage.type).toEqual('ErrorPage');
|
||||||
|
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('refresh failure', () => {
|
describe('refresh failure', () => {
|
||||||
@@ -98,13 +111,14 @@ describe('App router component', () => {
|
|||||||
});
|
});
|
||||||
runBasicTests();
|
runBasicTests();
|
||||||
it('loads error page', () => {
|
it('loads error page', () => {
|
||||||
expect(el.find('main')).toEqual(shallow(
|
const main = el.instance.findByType('main')[0];
|
||||||
<main>
|
expect(main.children.length).toEqual(1);
|
||||||
<Alert variant="danger">
|
const alert = main.children[0];
|
||||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
expect(alert.type).toEqual('Alert');
|
||||||
</Alert>
|
expect(alert.children.length).toEqual(1);
|
||||||
</main>,
|
const errorPage = alert.children[0];
|
||||||
));
|
expect(errorPage.type).toEqual('ErrorPage');
|
||||||
|
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
64
src/ExperimentContext.jsx
Normal file
64
src/ExperimentContext.jsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
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 };
|
||||||
123
src/ExperimentContext.test.jsx
Normal file
123
src/ExperimentContext.test.jsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
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
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`App router component component initialize failure snapshot 1`] = `
|
exports[`App router component component initialize failure snapshot 1`] = `
|
||||||
<BrowserRouter>
|
<Fragment>
|
||||||
<HelmetWrapper
|
<HelmetWrapper
|
||||||
defer={true}
|
defer={true}
|
||||||
encodeSpecialCharacters={true}
|
encodeSpecialCharacters={true}
|
||||||
@@ -11,26 +11,28 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
|||||||
</title>
|
</title>
|
||||||
</HelmetWrapper>
|
</HelmetWrapper>
|
||||||
<div>
|
<div>
|
||||||
<LearnerDashboardHeader />
|
<AppWrapper>
|
||||||
<main>
|
<LearnerDashboardHeader />
|
||||||
<Alert
|
<main>
|
||||||
variant="danger"
|
<Alert
|
||||||
>
|
variant="danger"
|
||||||
<ErrorPage
|
>
|
||||||
message="If you experience repeated failures, please email support at test-support-url"
|
<ErrorPage
|
||||||
/>
|
message="If you experience repeated failures, please email support at test-support-url"
|
||||||
</Alert>
|
/>
|
||||||
</main>
|
</Alert>
|
||||||
|
</main>
|
||||||
|
</AppWrapper>
|
||||||
<Footer
|
<Footer
|
||||||
logo="fakeLogo.png"
|
logo="fakeLogo.png"
|
||||||
/>
|
/>
|
||||||
<ZendeskFab />
|
<ZendeskFab />
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`App router component component no network failure snapshot 1`] = `
|
exports[`App router component component no network failure snapshot 1`] = `
|
||||||
<BrowserRouter>
|
<Fragment>
|
||||||
<HelmetWrapper
|
<HelmetWrapper
|
||||||
defer={true}
|
defer={true}
|
||||||
encodeSpecialCharacters={true}
|
encodeSpecialCharacters={true}
|
||||||
@@ -40,20 +42,24 @@ exports[`App router component component no network failure snapshot 1`] = `
|
|||||||
</title>
|
</title>
|
||||||
</HelmetWrapper>
|
</HelmetWrapper>
|
||||||
<div>
|
<div>
|
||||||
<LearnerDashboardHeader />
|
<AppWrapper>
|
||||||
<main>
|
<LearnerDashboardHeader />
|
||||||
<Dashboard />
|
<main>
|
||||||
</main>
|
<ExperimentProvider>
|
||||||
|
<Dashboard />
|
||||||
|
</ExperimentProvider>
|
||||||
|
</main>
|
||||||
|
</AppWrapper>
|
||||||
<Footer
|
<Footer
|
||||||
logo="fakeLogo.png"
|
logo="fakeLogo.png"
|
||||||
/>
|
/>
|
||||||
<ZendeskFab />
|
<ZendeskFab />
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`App router component component refresh failure snapshot 1`] = `
|
exports[`App router component component refresh failure snapshot 1`] = `
|
||||||
<BrowserRouter>
|
<Fragment>
|
||||||
<HelmetWrapper
|
<HelmetWrapper
|
||||||
defer={true}
|
defer={true}
|
||||||
encodeSpecialCharacters={true}
|
encodeSpecialCharacters={true}
|
||||||
@@ -63,20 +69,22 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
|||||||
</title>
|
</title>
|
||||||
</HelmetWrapper>
|
</HelmetWrapper>
|
||||||
<div>
|
<div>
|
||||||
<LearnerDashboardHeader />
|
<AppWrapper>
|
||||||
<main>
|
<LearnerDashboardHeader />
|
||||||
<Alert
|
<main>
|
||||||
variant="danger"
|
<Alert
|
||||||
>
|
variant="danger"
|
||||||
<ErrorPage
|
>
|
||||||
message="If you experience repeated failures, please email support at test-support-url"
|
<ErrorPage
|
||||||
/>
|
message="If you experience repeated failures, please email support at test-support-url"
|
||||||
</Alert>
|
/>
|
||||||
</main>
|
</Alert>
|
||||||
|
</main>
|
||||||
|
</AppWrapper>
|
||||||
<Footer
|
<Footer
|
||||||
logo="fakeLogo.png"
|
logo="fakeLogo.png"
|
||||||
/>
|
/>
|
||||||
<ZendeskFab />
|
<ZendeskFab />
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</Fragment>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -13,18 +13,28 @@ exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
|||||||
"redux": "store",
|
"redux": "store",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
wrapWithRouter={true}
|
||||||
>
|
>
|
||||||
<NoticesWrapper>
|
<NoticesWrapper>
|
||||||
<Switch>
|
<Routes>
|
||||||
<PageRoute
|
<Route
|
||||||
|
element={
|
||||||
|
<PageWrap>
|
||||||
|
<App />
|
||||||
|
</PageWrap>
|
||||||
|
}
|
||||||
path="/"
|
path="/"
|
||||||
>
|
|
||||||
<App />
|
|
||||||
</PageRoute>
|
|
||||||
<Redirect
|
|
||||||
to="/"
|
|
||||||
/>
|
/>
|
||||||
</Switch>
|
<Route
|
||||||
|
element={
|
||||||
|
<Navigate
|
||||||
|
replace={true}
|
||||||
|
to="/"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
path="*"
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
</NoticesWrapper>
|
</NoticesWrapper>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const configuration = {
|
|||||||
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
||||||
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
||||||
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
|
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
|
||||||
|
LOGO_URL: process.env.LOGO_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
const features = {};
|
const features = {};
|
||||||
|
|||||||
29
src/containers/CertificatePreviewModal/hooks.js
Normal file
29
src/containers/CertificatePreviewModal/hooks.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { reduxHooks } from 'hooks';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
export const useCertificatePreviewData = () => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const selectedCardId = reduxHooks.useCertificatePreviewData().cardId;
|
||||||
|
|
||||||
|
const { courseId } = reduxHooks.useCardCourseRunData(selectedCardId) || {};
|
||||||
|
|
||||||
|
const courseTitle = courseId;
|
||||||
|
const header = formatMessage(messages.previewTitle, { courseTitle });
|
||||||
|
|
||||||
|
const closePreviewModal = reduxHooks.useUpdateCertificatePreviewModalCallback(null);
|
||||||
|
|
||||||
|
const getCertificatePreviewUrl = () => `${getConfig().LMS_BASE_URL}/certificates/upsell/course/${courseId}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
showModal: selectedCardId != null,
|
||||||
|
header,
|
||||||
|
closePreviewModal,
|
||||||
|
getCertificatePreviewUrl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCertificatePreviewData;
|
||||||
48
src/containers/CertificatePreviewModal/index.jsx
Normal file
48
src/containers/CertificatePreviewModal/index.jsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
ModalDialog,
|
||||||
|
} from '@edx/paragon';
|
||||||
|
import { UpgradeButton } from '../CourseCard/components/CourseCardActions/UpgradeButton';
|
||||||
|
import useCertificatePreviewData from './hooks';
|
||||||
|
|
||||||
|
export const CertificatePreviewModal = ({
|
||||||
|
cardId,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
showModal,
|
||||||
|
header,
|
||||||
|
closePreviewModal,
|
||||||
|
getCertificatePreviewUrl,
|
||||||
|
} = useCertificatePreviewData();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalDialog
|
||||||
|
isOpen={showModal}
|
||||||
|
onClose={closePreviewModal}
|
||||||
|
hasCloseButton
|
||||||
|
isFullscreenOnMobile
|
||||||
|
size="lg"
|
||||||
|
className="p-4 px-4.5"
|
||||||
|
title={header}
|
||||||
|
>
|
||||||
|
<h3>{header}</h3>
|
||||||
|
<div>
|
||||||
|
<iframe
|
||||||
|
title={header}
|
||||||
|
src={getCertificatePreviewUrl()}
|
||||||
|
width={725}
|
||||||
|
height={400}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UpgradeButton
|
||||||
|
cardId={cardId}
|
||||||
|
/>
|
||||||
|
</ModalDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
CertificatePreviewModal.propTypes = {
|
||||||
|
cardId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CertificatePreviewModal;
|
||||||
12
src/containers/CertificatePreviewModal/messages.js
Normal file
12
src/containers/CertificatePreviewModal/messages.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* eslint-disable quotes */
|
||||||
|
import { StrictDict } from 'utils';
|
||||||
|
|
||||||
|
export const messages = StrictDict({
|
||||||
|
previewTitle: {
|
||||||
|
id: 'learner-dash.certificatePreview.title',
|
||||||
|
description: 'The title of the email settings modal',
|
||||||
|
defaultMessage: 'Your certificate preview for {courseTitle} ',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -12,11 +12,13 @@ import messages from './messages';
|
|||||||
export const BeginCourseButton = ({ cardId }) => {
|
export const BeginCourseButton = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||||
|
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||||
const { disableBeginCourse } = useActionDisabledState(cardId);
|
const { disableBeginCourse } = useActionDisabledState(cardId);
|
||||||
|
|
||||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||||
track.course.enterCourseClicked,
|
track.course.enterCourseClicked,
|
||||||
cardId,
|
cardId,
|
||||||
homeUrl,
|
homeUrl + execEdTrackingParam,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
|||||||
@@ -14,17 +14,22 @@ jest.mock('tracking', () => ({
|
|||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
reduxHooks: {
|
||||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'home-url' })),
|
useCardCourseRunData: jest.fn(),
|
||||||
useTrackCourseEvent: jest.fn(
|
useCardExecEdTrackingParam: jest.fn(),
|
||||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
useTrackCourseEvent: jest.fn(),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
||||||
jest.mock('./ActionButton', () => 'ActionButton');
|
jest.mock('./ActionButton', () => 'ActionButton');
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const { homeUrl } = reduxHooks.useCardCourseRunData();
|
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 } }),
|
||||||
|
);
|
||||||
|
|
||||||
describe('BeginCourseButton', () => {
|
describe('BeginCourseButton', () => {
|
||||||
const props = {
|
const props = {
|
||||||
@@ -33,27 +38,50 @@ describe('BeginCourseButton', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
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', () => {
|
describe('behavior', () => {
|
||||||
it('initializes course run data with cardId', () => {
|
it('initializes course run data with cardId', () => {
|
||||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||||
});
|
});
|
||||||
test('disabled states', () => {
|
it('loads exec education path param', () => {
|
||||||
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
|
|
||||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
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),
|
||||||
|
));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import messages from './messages';
|
|||||||
export const ResumeButton = ({ cardId }) => {
|
export const ResumeButton = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||||
|
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||||
const { disableResumeCourse } = useActionDisabledState(cardId);
|
const { disableResumeCourse } = useActionDisabledState(cardId);
|
||||||
|
|
||||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||||
track.course.enterCourseClicked,
|
track.course.enterCourseClicked,
|
||||||
cardId,
|
cardId,
|
||||||
resumeUrl,
|
resumeUrl + execEdTrackingParam,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
|||||||
@@ -6,52 +6,80 @@ import track from 'tracking';
|
|||||||
import useActionDisabledState from '../hooks';
|
import useActionDisabledState from '../hooks';
|
||||||
import ResumeButton from './ResumeButton';
|
import ResumeButton from './ResumeButton';
|
||||||
|
|
||||||
|
jest.mock('tracking', () => ({
|
||||||
|
course: {
|
||||||
|
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
reduxHooks: {
|
||||||
useCardCourseRunData: jest.fn(() => ({ resumeUrl: 'resumeUrl' })),
|
useCardCourseRunData: jest.fn(),
|
||||||
useTrackCourseEvent: (eventName, cardId, url) => jest
|
useCardExecEdTrackingParam: jest.fn(),
|
||||||
.fn()
|
useTrackCourseEvent: jest.fn(),
|
||||||
.mockName(`useTrackCourseEvent('${eventName}', '${cardId}', '${url}')`),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
||||||
jest.mock('tracking', () => ({
|
|
||||||
course: {
|
|
||||||
enterCourseClicked: 'enterCourseClicked',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
jest.mock('./ActionButton', () => 'ActionButton');
|
jest.mock('./ActionButton', () => 'ActionButton');
|
||||||
|
|
||||||
const { resumeUrl } = reduxHooks.useCardCourseRunData();
|
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;
|
||||||
|
|
||||||
describe('ResumeButton', () => {
|
describe('ResumeButton', () => {
|
||||||
const props = {
|
const props = {
|
||||||
cardId: 'cardId',
|
cardId: 'cardId',
|
||||||
};
|
};
|
||||||
describe('snapshot', () => {
|
describe('behavior', () => {
|
||||||
test('renders default button when learner has access to the course', () => {
|
it('initializes course run data with cardId', () => {
|
||||||
const wrapper = shallow(<ResumeButton {...props} />);
|
wrapper = shallow(<ResumeButton {...props} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
});
|
||||||
expect(wrapper.prop(htmlProps.onClick).getMockName()).toContain(
|
it('loads exec education path param', () => {
|
||||||
'useTrackCourseEvent',
|
wrapper = shallow(<ResumeButton {...props} />);
|
||||||
track.course.enterCourseClicked,
|
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||||
props.cardId,
|
});
|
||||||
resumeUrl,
|
it('loads disabled states for resume action from action hooks', () => {
|
||||||
);
|
wrapper = shallow(<ResumeButton {...props} />);
|
||||||
|
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('behavior', () => {
|
describe('snapshot', () => {
|
||||||
it('initializes course run data based on cardId', () => {
|
describe('disabled', () => {
|
||||||
shallow(<ResumeButton {...props} />);
|
beforeEach(() => {
|
||||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
|
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
|
||||||
props.cardId,
|
wrapper = shallow(<ResumeButton {...props} />);
|
||||||
);
|
});
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
it('should be disabled', () => {
|
||||||
|
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
test('disabled states', () => {
|
describe('enabled', () => {
|
||||||
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
|
beforeEach(() => {
|
||||||
const wrapper = shallow(<ResumeButton {...props} />);
|
wrapper = shallow(<ResumeButton {...props} />);
|
||||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
});
|
||||||
|
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),
|
||||||
|
));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`BeginCourseButton snapshot renders default button when learner has access to the course 1`] = `
|
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`] = `
|
||||||
<ActionButton
|
<ActionButton
|
||||||
as="a"
|
as="a"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
@@ -10,7 +29,7 @@ exports[`BeginCourseButton snapshot renders default button when learner has acce
|
|||||||
"trackCourseEvent": Object {
|
"trackCourseEvent": Object {
|
||||||
"cardId": "cardId",
|
"cardId": "cardId",
|
||||||
"eventName": [MockFunction segment.enterCourseClicked],
|
"eventName": [MockFunction segment.enterCourseClicked],
|
||||||
"upgradeUrl": "home-url",
|
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,38 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`ResumeButton snapshot renders default button when learner has access to the course 1`] = `
|
exports[`ResumeButton snapshot disabled snapshot 1`] = `
|
||||||
<ActionButton
|
<ActionButton
|
||||||
as="a"
|
as="a"
|
||||||
disabled={false}
|
disabled={true}
|
||||||
href="#"
|
href="#"
|
||||||
onClick={[MockFunction useTrackCourseEvent('enterCourseClicked', 'cardId', 'resumeUrl')]}
|
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`] = `
|
||||||
|
<ActionButton
|
||||||
|
as="a"
|
||||||
|
disabled={false}
|
||||||
|
href="#"
|
||||||
|
onClick={
|
||||||
|
Object {
|
||||||
|
"trackCourseEvent": Object {
|
||||||
|
"cardId": "cardId",
|
||||||
|
"eventName": [MockFunction segment.enterCourseClicked],
|
||||||
|
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Resume
|
Resume
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
// 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,21 +13,27 @@ import ViewCourseButton from './ViewCourseButton';
|
|||||||
|
|
||||||
export const CourseCardActions = ({ cardId }) => {
|
export const CourseCardActions = ({ cardId }) => {
|
||||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
||||||
const { isVerified, hasStarted } = reduxHooks.useCardEnrollmentData(cardId);
|
const {
|
||||||
|
isVerified,
|
||||||
|
hasStarted,
|
||||||
|
isExecEd2UCourse,
|
||||||
|
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||||
const { isArchived } = reduxHooks.useCardCourseRunData(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 (
|
return (
|
||||||
<ActionRow data-test-id="CourseCardActions">
|
<ActionRow data-test-id="CourseCardActions">
|
||||||
{!(isEntitlement || isVerified) && <UpgradeButton cardId={cardId} />}
|
{!(isEntitlement || isVerified || isExecEd2UCourse) && <UpgradeButton cardId={cardId} />}
|
||||||
<PrimaryButton cardId={cardId} />
|
{isEntitlement && (isFulfilled
|
||||||
|
? <ViewCourseButton cardId={cardId} />
|
||||||
|
: <SelectSessionButton cardId={cardId} />
|
||||||
|
)}
|
||||||
|
{(isArchived && !isEntitlement) && (
|
||||||
|
<ViewCourseButton cardId={cardId} />
|
||||||
|
)}
|
||||||
|
{!(isArchived || isEntitlement) && (hasStarted
|
||||||
|
? <ResumeButton cardId={cardId} />
|
||||||
|
: <BeginCourseButton cardId={cardId} />
|
||||||
|
)}
|
||||||
</ActionRow>
|
</ActionRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from '@edx/react-unit-test-utils';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
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 '.';
|
import CourseCardActions from '.';
|
||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
@@ -9,6 +15,7 @@ jest.mock('hooks', () => ({
|
|||||||
useCardCourseRunData: jest.fn(),
|
useCardCourseRunData: jest.fn(),
|
||||||
useCardEnrollmentData: jest.fn(),
|
useCardEnrollmentData: jest.fn(),
|
||||||
useCardEntitlementData: jest.fn(),
|
useCardEntitlementData: jest.fn(),
|
||||||
|
useMasqueradeData: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -18,87 +25,92 @@ jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
|
|||||||
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
|
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
|
||||||
jest.mock('./ResumeButton', () => 'ResumeButton');
|
jest.mock('./ResumeButton', () => 'ResumeButton');
|
||||||
|
|
||||||
|
const cardId = 'test-card-id';
|
||||||
|
const props = { cardId };
|
||||||
|
|
||||||
|
let el;
|
||||||
describe('CourseCardActions', () => {
|
describe('CourseCardActions', () => {
|
||||||
const props = {
|
const mockHooks = ({
|
||||||
cardId: 'cardId',
|
isEntitlement = false,
|
||||||
};
|
isExecEd2UCourse = false,
|
||||||
const createWrapper = ({
|
isFulfilled = false,
|
||||||
isEntitlement, isFulfilled, isArchived, isVerified, hasStarted,
|
isArchived = false,
|
||||||
}) => {
|
isVerified = false,
|
||||||
|
hasStarted = false,
|
||||||
|
isMasquerading = false,
|
||||||
|
} = {}) => {
|
||||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
|
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
|
||||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
|
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isVerified, hasStarted });
|
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted });
|
||||||
return shallow(<CourseCardActions {...props} />);
|
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||||
};
|
};
|
||||||
describe('snapshot', () => {
|
const render = () => {
|
||||||
test('show upgrade button when not verified and not entitlement', () => {
|
el = shallow(<CourseCardActions {...props} />);
|
||||||
const wrapper = createWrapper({
|
};
|
||||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
describe('behavior', () => {
|
||||||
});
|
it('initializes redux hooks', () => {
|
||||||
expect(wrapper).toMatchSnapshot();
|
mockHooks();
|
||||||
});
|
render();
|
||||||
test('show select session button when not verified and entitlement', () => {
|
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
|
||||||
const wrapper = createWrapper({
|
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
||||||
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||||
});
|
|
||||||
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('behavior', () => {
|
describe('Exec Ed course', () => {
|
||||||
it('show upgrade button when not verified and not entitlement', () => {
|
it('does not render upgrade button', () => {
|
||||||
const wrapper = createWrapper({
|
mockHooks({ isExecEd2UCourse: true });
|
||||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
render();
|
||||||
|
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('UpgradeButton')).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
it('show select session button when not verified and entitlement', () => {
|
describe('entitlement course', () => {
|
||||||
const wrapper = createWrapper({
|
it('does not render upgrade button', () => {
|
||||||
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
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);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('SelectSessionButton')).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
it('show begin course button when verified and not entitlement and has started', () => {
|
describe('verified course', () => {
|
||||||
const wrapper = createWrapper({
|
it('does not render upgrade button', () => {
|
||||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: false,
|
mockHooks({ isVerified: true });
|
||||||
|
render();
|
||||||
|
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('BeginCourseButton')).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
it('show resume button when verified and not entitlement and has started', () => {
|
describe('not entielement, verified, or exec ed', () => {
|
||||||
const wrapper = createWrapper({
|
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
|
||||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: true,
|
mockHooks({ isArchived: true });
|
||||||
|
render();
|
||||||
|
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||||
|
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
|
||||||
});
|
});
|
||||||
expect(wrapper.find('ResumeButton')).toHaveLength(1);
|
describe('unstarted courses', () => {
|
||||||
});
|
it('renders UpgradeButton and BeginCourseButton', () => {
|
||||||
it('show view course button when not verified and entitlement and fulfilled', () => {
|
mockHooks();
|
||||||
const wrapper = createWrapper({
|
render();
|
||||||
isEntitlement: true, isFulfilled: true, isArchived: false, isVerified: false, hasStarted: false,
|
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
|
||||||
|
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
expect(wrapper.find('ViewCourseButton')).toHaveLength(1);
|
describe('active courses (started, and not archived)', () => {
|
||||||
});
|
it('renders UpgradeButton and ResumeButton', () => {
|
||||||
it('show view course button when not verified and entitlement and fulfilled and archived', () => {
|
mockHooks({ hasStarted: true });
|
||||||
const wrapper = createWrapper({
|
render();
|
||||||
isEntitlement: true, isFulfilled: true, isArchived: true, isVerified: false, hasStarted: false,
|
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const useCardDetailsData = ({ cardId }) => {
|
|||||||
} = reduxHooks.useCardEntitlementData(cardId);
|
} = reduxHooks.useCardEntitlementData(cardId);
|
||||||
|
|
||||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||||
|
const openCertificatePreview = reduxHooks.useUpdateCertificatePreviewModalCallback(cardId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
providerName: providerName || formatMessage(messages.unknownProviderName),
|
providerName: providerName || formatMessage(messages.unknownProviderName),
|
||||||
@@ -57,6 +58,7 @@ export const useCardDetailsData = ({ cardId }) => {
|
|||||||
openSessionModal,
|
openSessionModal,
|
||||||
courseNumber,
|
courseNumber,
|
||||||
changeOrLeaveSessionMessage: formatMessage(messages.changeOrLeaveSessionButton),
|
changeOrLeaveSessionMessage: formatMessage(messages.changeOrLeaveSessionButton),
|
||||||
|
openCertificatePreview,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,23 +16,35 @@ export const CourseCardDetails = ({ cardId }) => {
|
|||||||
openSessionModal,
|
openSessionModal,
|
||||||
courseNumber,
|
courseNumber,
|
||||||
changeOrLeaveSessionMessage,
|
changeOrLeaveSessionMessage,
|
||||||
|
openCertificatePreview,
|
||||||
} = useCardDetailsData({ cardId });
|
} = useCardDetailsData({ cardId });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="small" data-testid="CourseCardDetails">
|
<div>
|
||||||
{providerName} • {courseNumber}
|
<span className="small" data-testid="CourseCardDetails">
|
||||||
{!(isEntitlement && !isFulfilled) && accessMessage && (
|
{providerName} • {courseNumber}
|
||||||
` • ${accessMessage}`
|
{!(isEntitlement && !isFulfilled) && accessMessage && (
|
||||||
)}
|
` • ${accessMessage}`
|
||||||
{isEntitlement && isFulfilled && canChange ? (
|
)}
|
||||||
<>
|
{isEntitlement && isFulfilled && canChange ? (
|
||||||
{' • '}
|
<>
|
||||||
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
|
{' • '}
|
||||||
{changeOrLeaveSessionMessage}
|
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
|
||||||
</Button>
|
{changeOrLeaveSessionMessage}
|
||||||
</>
|
</Button>
|
||||||
) : null}
|
</>
|
||||||
</span>
|
) : null}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="inline"
|
||||||
|
className="float-right"
|
||||||
|
data-testid="certificate-preview"
|
||||||
|
onClick={openCertificatePreview}
|
||||||
|
>
|
||||||
|
Preview Your Certificate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
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,8 +1,44 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1`] = `
|
exports[`CourseCardMenu render show dropdown hide unenroll item and disable email snapshot 1`] = `
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Dropdown>
|
<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.Toggle
|
<Dropdown.Toggle
|
||||||
alt="Course actions dropdown"
|
alt="Course actions dropdown"
|
||||||
as="IconButton"
|
as="IconButton"
|
||||||
@@ -19,31 +55,16 @@ exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1
|
|||||||
>
|
>
|
||||||
Unenroll
|
Unenroll
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<SocialShareMenu
|
||||||
data-testid="emailSettingsModalToggle"
|
cardId="test-card-id"
|
||||||
disabled={false}
|
emailSettings={
|
||||||
onClick={[MockFunction emailSettingShow]}
|
Object {
|
||||||
>
|
"hide": [MockFunction emailSettingHide],
|
||||||
Email settings
|
"isVisible": false,
|
||||||
</Dropdown.Item>
|
"show": [MockFunction emailSettingShow],
|
||||||
<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.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<UnenrollConfirmModal
|
<UnenrollConfirmModal
|
||||||
@@ -58,83 +79,3 @@ exports[`CourseCardMenu enrolled, share enabled, email setting enable snapshot 1
|
|||||||
/>
|
/>
|
||||||
</Fragment>
|
</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
|
|
||||||
cardId="test-card-id"
|
|
||||||
closeModal={[MockFunction unenrollHide]}
|
|
||||||
show={false}
|
|
||||||
/>
|
|
||||||
<EmailSettingsModal
|
|
||||||
cardId="test-card-id"
|
|
||||||
closeModal={[MockFunction emailSettingHide]}
|
|
||||||
show={false}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`CourseCardMenu not enrolled, share disabled, email setting disabled 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>
|
|
||||||
<UnenrollConfirmModal
|
|
||||||
cardId="test-card-id"
|
|
||||||
closeModal={[MockFunction unenrollHide]}
|
|
||||||
show={false}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import React from 'react';
|
import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils';
|
||||||
import { StrictDict } from 'utils';
|
|
||||||
|
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
|
|
||||||
import * as module from './hooks';
|
export const stateKeys = StrictDict({
|
||||||
|
isUnenrollConfirmVisible: 'isUnenrollConfirmVisible',
|
||||||
export const state = StrictDict({
|
isEmailSettingsVisible: 'isEmailSettingsVisible',
|
||||||
isUnenrollConfirmVisible: (val) => React.useState(val), // eslint-disable-line
|
|
||||||
isEmailSettingsVisible: (val) => React.useState(val), // eslint-disable-line
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useUnenrollData = () => {
|
export const useUnenrollData = () => {
|
||||||
const [isVisible, setIsVisible] = module.state.isUnenrollConfirmVisible(false);
|
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isUnenrollConfirmVisible, false);
|
||||||
return {
|
return {
|
||||||
show: () => setIsVisible(true),
|
show: () => setIsVisible(true),
|
||||||
hide: () => setIsVisible(false),
|
hide: () => setIsVisible(false),
|
||||||
@@ -21,7 +18,7 @@ export const useUnenrollData = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useEmailSettings = () => {
|
export const useEmailSettings = () => {
|
||||||
const [isVisible, setIsVisible] = module.state.isEmailSettingsVisible(false);
|
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isEmailSettingsVisible, false);
|
||||||
return {
|
return {
|
||||||
show: () => setIsVisible(true),
|
show: () => setIsVisible(true),
|
||||||
hide: () => setIsVisible(false),
|
hide: () => setIsVisible(false),
|
||||||
@@ -30,9 +27,30 @@ export const useEmailSettings = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useHandleToggleDropdown = (cardId) => {
|
export const useHandleToggleDropdown = (cardId) => {
|
||||||
const eventName = track.course.courseOptionsDropdownClicked;
|
const trackCourseEvent = reduxHooks.useTrackCourseEvent(
|
||||||
const trackCourseEvent = reduxHooks.useTrackCourseEvent(eventName, cardId);
|
track.course.courseOptionsDropdownClicked,
|
||||||
|
cardId,
|
||||||
|
);
|
||||||
return (isOpen) => {
|
return (isOpen) => {
|
||||||
if (isOpen) { trackCourseEvent(); }
|
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,4 +1,5 @@
|
|||||||
import { MockUseState } from 'testUtils';
|
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
import track from 'tracking';
|
import track from 'tracking';
|
||||||
|
|
||||||
@@ -6,71 +7,77 @@ import * as hooks from './hooks';
|
|||||||
|
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
reduxHooks: {
|
||||||
|
useCardCertificateData: jest.fn(),
|
||||||
|
useCardEnrollmentData: jest.fn(),
|
||||||
|
useCardSocialSettingsData: jest.fn(),
|
||||||
useTrackCourseEvent: jest.fn(),
|
useTrackCourseEvent: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const trackCourseEvent = jest.fn();
|
const trackCourseEvent = jest.fn();
|
||||||
reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
|
reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
|
||||||
const state = new MockUseState(hooks);
|
|
||||||
|
|
||||||
const cardId = 'test-card-id';
|
const cardId = 'test-card-id';
|
||||||
let out;
|
let out;
|
||||||
|
|
||||||
describe('CourseCardMenu hooks', () => {
|
const state = mockUseKeyedState(hooks.stateKeys);
|
||||||
describe('state values', () => {
|
|
||||||
state.testGetter(state.keys.isUnenrollConfirmVisible);
|
|
||||||
state.testGetter(state.keys.isEmailSettingsVisible);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
describe('CourseCardMenu hooks', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
state.mock();
|
||||||
|
});
|
||||||
describe('useUnenrollData', () => {
|
describe('useUnenrollData', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
state.mock();
|
state.mockVals({ isUnenrollConfirmVisible: true });
|
||||||
out = hooks.useUnenrollData();
|
out = hooks.useUnenrollData();
|
||||||
});
|
});
|
||||||
afterEach(state.restore);
|
describe('behavior', () => {
|
||||||
|
it('initializes isUnenrollConfirmVisible state to false', () => {
|
||||||
test('default state', () => {
|
state.expectInitializedWith(state.keys.isUnenrollConfirmVisible, false);
|
||||||
expect(out.isVisible).toEqual(state.stateVals.isUnenrollConfirmVisible);
|
});
|
||||||
});
|
});
|
||||||
|
describe('output', () => {
|
||||||
test('show', () => {
|
test('state is loaded from current state value', () => {
|
||||||
out.show();
|
expect(out.isVisible).toEqual(true);
|
||||||
state.expectSetStateCalledWith(state.keys.isUnenrollConfirmVisible, true);
|
});
|
||||||
});
|
test('show sets state value to true', () => {
|
||||||
|
out.show();
|
||||||
test('hide', () => {
|
expect(state.setState.isUnenrollConfirmVisible).toHaveBeenCalledWith(true);
|
||||||
out.hide();
|
});
|
||||||
state.expectSetStateCalledWith(state.keys.isUnenrollConfirmVisible, false);
|
test('hide sets state value to false', () => {
|
||||||
|
out.hide();
|
||||||
|
expect(state.setState.isUnenrollConfirmVisible).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useEmailSettings', () => {
|
describe('useEmailSettings', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
state.mock();
|
state.mockVals({ isEmailSettingsVisible: true });
|
||||||
out = hooks.useEmailSettings();
|
out = hooks.useEmailSettings();
|
||||||
});
|
});
|
||||||
afterEach(state.restore);
|
describe('behavior', () => {
|
||||||
|
it('initializes isEmailSettingsVisible state to false', () => {
|
||||||
test('default state', () => {
|
state.expectInitializedWith(state.keys.isEmailSettingsVisible, false);
|
||||||
expect(out.isVisible).toEqual(state.stateVals.isEmailSettingsVisible);
|
});
|
||||||
});
|
});
|
||||||
|
describe('output', () => {
|
||||||
test('show', () => {
|
test('state is loaded from current state value', () => {
|
||||||
out.show();
|
expect(out.isVisible).toEqual(state.values.isEmailSettingsVisible);
|
||||||
state.expectSetStateCalledWith(state.keys.isEmailSettingsVisible, true);
|
});
|
||||||
});
|
test('show sets state value to true', () => {
|
||||||
|
out.show();
|
||||||
test('hide', () => {
|
expect(state.setState.isEmailSettingsVisible).toHaveBeenCalledWith(true);
|
||||||
out.hide();
|
});
|
||||||
state.expectSetStateCalledWith(state.keys.isEmailSettingsVisible, false);
|
test('hide sets state value to false', () => {
|
||||||
|
out.hide();
|
||||||
|
expect(state.setState.isEmailSettingsVisible).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useHandleToggleDropdown', () => {
|
describe('useHandleToggleDropdown', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => { out = hooks.useHandleToggleDropdown(cardId); });
|
||||||
out = hooks.useHandleToggleDropdown(cardId);
|
|
||||||
});
|
|
||||||
describe('behavior', () => {
|
describe('behavior', () => {
|
||||||
it('initializes course event tracker with event name and card ID', () => {
|
it('initializes course event tracker with event name and card ID', () => {
|
||||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
||||||
@@ -88,4 +95,59 @@ 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,44 +1,43 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import * as ReactShare from 'react-share';
|
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Dropdown, Icon, IconButton } from '@edx/paragon';
|
import { Dropdown, Icon, IconButton } from '@edx/paragon';
|
||||||
import { MoreVert } from '@edx/paragon/icons';
|
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 EmailSettingsModal from 'containers/EmailSettingsModal';
|
||||||
|
import CertificatePreviewModal from 'containers/CertificatePreviewModal';
|
||||||
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
||||||
|
import { reduxHooks } from 'hooks';
|
||||||
|
import SocialShareMenu from './SocialShareMenu';
|
||||||
import {
|
import {
|
||||||
useEmailSettings,
|
useEmailSettings,
|
||||||
useUnenrollData,
|
useUnenrollData,
|
||||||
useHandleToggleDropdown,
|
useHandleToggleDropdown,
|
||||||
|
useOptionVisibility,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
|
export const testIds = StrictDict({
|
||||||
|
unenrollModalToggle: 'unenrollModalToggle',
|
||||||
|
});
|
||||||
|
|
||||||
export const CourseCardMenu = ({ cardId }) => {
|
export const CourseCardMenu = ({ cardId }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
const emailSettings = useEmailSettings();
|
||||||
const { isEnrolled, isEmailEnabled } = 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',
|
|
||||||
);
|
|
||||||
|
|
||||||
const emailSettingsModal = useEmailSettings();
|
|
||||||
const unenrollModal = useUnenrollData();
|
const unenrollModal = useUnenrollData();
|
||||||
const handleToggleDropdown = useHandleToggleDropdown(cardId);
|
const handleToggleDropdown = useHandleToggleDropdown(cardId);
|
||||||
|
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
|
||||||
|
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||||
|
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||||
|
const showCertificatePreviewModal = reduxHooks.useShowCertificatePreviewModal(cardId);
|
||||||
|
|
||||||
|
if (!shouldShowDropdown) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -52,52 +51,16 @@ export const CourseCardMenu = ({ cardId }) => {
|
|||||||
alt={formatMessage(messages.dropdownAlt)}
|
alt={formatMessage(messages.dropdownAlt)}
|
||||||
/>
|
/>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
{isEnrolled && (
|
{shouldShowUnenrollItem && (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
disabled={isMasquerading}
|
disabled={isMasquerading}
|
||||||
onClick={unenrollModal.show}
|
onClick={unenrollModal.show}
|
||||||
data-testid="unenrollModalToggle"
|
data-testid={testIds.unenrollModalToggle}
|
||||||
>
|
>
|
||||||
{formatMessage(messages.unenroll)}
|
{formatMessage(messages.unenroll)}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
)}
|
)}
|
||||||
{isEmailEnabled && (
|
<SocialShareMenu cardId={cardId} emailSettings={emailSettings} />
|
||||||
<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.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<UnenrollConfirmModal
|
<UnenrollConfirmModal
|
||||||
@@ -107,8 +70,13 @@ export const CourseCardMenu = ({ cardId }) => {
|
|||||||
/>
|
/>
|
||||||
{isEmailEnabled && (
|
{isEmailEnabled && (
|
||||||
<EmailSettingsModal
|
<EmailSettingsModal
|
||||||
show={emailSettingsModal.isVisible}
|
show={emailSettings.isVisible}
|
||||||
closeModal={emailSettingsModal.hide}
|
closeModal={emailSettings.hide}
|
||||||
|
cardId={cardId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showCertificatePreviewModal && (
|
||||||
|
<CertificatePreviewModal
|
||||||
cardId={cardId}
|
cardId={cardId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,141 +1,213 @@
|
|||||||
import { shallow } from 'enzyme';
|
import { when } from 'jest-when';
|
||||||
|
|
||||||
|
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 { reduxHooks } from 'hooks';
|
||||||
import { useEmailSettings, useUnenrollData } from './hooks';
|
import SocialShareMenu from './SocialShareMenu';
|
||||||
import CourseCardMenu from '.';
|
import * as hooks from './hooks';
|
||||||
|
import CourseCardMenu, { testIds } from '.';
|
||||||
|
|
||||||
jest.mock('react-share', () => ({
|
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||||
FacebookShareButton: () => 'FacebookShareButton',
|
useIntl: jest.fn().mockReturnValue({
|
||||||
TwitterShareButton: () => 'TwitterShareButton',
|
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
jest.mock('hooks', () => ({
|
jest.mock('hooks', () => ({
|
||||||
reduxHooks: {
|
reduxHooks: { useMasqueradeData: jest.fn(), useCardEnrollmentData: jest.fn() },
|
||||||
useCardCourseData: jest.fn(),
|
|
||||||
useCardEnrollmentData: jest.fn(),
|
|
||||||
useCardSocialSettingsData: jest.fn(),
|
|
||||||
useMasqueradeData: jest.fn(),
|
|
||||||
useTrackCourseEvent: (_, __, site) => jest.fn().mockName(`${site}ShareClick`),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
jest.mock('./SocialShareMenu', () => 'SocialShareMenu');
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('./hooks', () => ({
|
||||||
useEmailSettings: jest.fn(),
|
useEmailSettings: jest.fn(),
|
||||||
useUnenrollData: jest.fn(),
|
useUnenrollData: jest.fn(),
|
||||||
useHandleToggleDropdown: jest.fn(),
|
useHandleToggleDropdown: jest.fn(),
|
||||||
|
useOptionVisibility: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
cardId: 'test-card-id',
|
cardId: 'test-card-id',
|
||||||
};
|
};
|
||||||
const defaultEmailSettingsModal = {
|
|
||||||
|
const emailSettings = {
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
show: jest.fn().mockName('emailSettingShow'),
|
show: jest.fn().mockName('emailSettingShow'),
|
||||||
hide: jest.fn().mockName('emailSettingHide'),
|
hide: jest.fn().mockName('emailSettingHide'),
|
||||||
};
|
};
|
||||||
const defaultUnenrollModal = {
|
|
||||||
|
const unenrollData = {
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
show: jest.fn().mockName('unenrollShow'),
|
show: jest.fn().mockName('unenrollShow'),
|
||||||
hide: jest.fn().mockName('unenrollHide'),
|
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;
|
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('CourseCardMenu', () => {
|
||||||
beforeEach(() => {
|
describe('behavior', () => {
|
||||||
useEmailSettings.mockReturnValue(defaultEmailSettingsModal);
|
|
||||||
useUnenrollData.mockReturnValue(defaultUnenrollModal);
|
|
||||||
reduxHooks.useCardSocialSettingsData.mockReturnValue(defaultSocialShare);
|
|
||||||
reduxHooks.useCardCourseData.mockReturnValue({ courseName });
|
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: true, isEmailEnabled: true });
|
|
||||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
|
||||||
});
|
|
||||||
describe('enrolled, share enabled, email setting enable', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = shallow(<CourseCardMenu {...props} />);
|
mockHooks();
|
||||||
|
render();
|
||||||
});
|
});
|
||||||
test('snapshot', () => {
|
it('initializes intl hook', () => {
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(useIntl).toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
it('renders share buttons', () => {
|
it('initializes local hooks', () => {
|
||||||
el = wrapper.find('FacebookShareButton');
|
when(hooks.useEmailSettings).expectCalledWith();
|
||||||
expect(el.length).toEqual(1);
|
when(hooks.useUnenrollData).expectCalledWith();
|
||||||
expect(el.prop('url')).toEqual('facebook-share-url');
|
when(hooks.useHandleToggleDropdown).expectCalledWith(props.cardId);
|
||||||
el = wrapper.find('TwitterShareButton');
|
when(hooks.useOptionVisibility).expectCalledWith(props.cardId);
|
||||||
expect(el.length).toEqual(1);
|
|
||||||
expect(el.prop('url')).toEqual('twitter-share-url');
|
|
||||||
});
|
});
|
||||||
it('renders enabled unenroll modal toggle', () => {
|
it('initializes redux hook data ', () => {
|
||||||
el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
|
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
||||||
expect(el.props().disabled).toEqual(false);
|
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
||||||
});
|
|
||||||
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('not enrolled, share disabled, email setting disabled', () => {
|
describe('render', () => {
|
||||||
beforeEach(() => {
|
it('renders null if showDropdown is false', () => {
|
||||||
reduxHooks.useCardSocialSettingsData.mockReturnValueOnce({
|
mockHooks();
|
||||||
...defaultSocialShare,
|
render();
|
||||||
twitter: { ...defaultSocialShare.twitter, isEnabled: false },
|
expect(el.isEmptyRender()).toEqual(true);
|
||||||
facebook: { ...defaultSocialShare.facebook, isEnabled: false },
|
});
|
||||||
|
const testHandleToggle = () => {
|
||||||
|
it('displays Dropdown with onToggle=handleToggleDropdown', () => {
|
||||||
|
expect(el.instance.findByType(Dropdown)[0].props.onToggle).toEqual(handleToggleDropdown);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const testSocialShareMenu = () => {
|
||||||
|
it('displays SocialShareMenu with cardID and emailSettings', () => {
|
||||||
|
const menu = el.instance.findByType(SocialShareMenu)[0];
|
||||||
|
expect(menu.props.cardId).toEqual(props.cardId);
|
||||||
|
expect(menu.props.emailSettings).toEqual(emailSettings);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
describe('show dropdown', () => {
|
||||||
|
describe('hide unenroll item and disable email', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false, isEmailEnabled: false });
|
|
||||||
wrapper = shallow(<CourseCardMenu {...props} />);
|
|
||||||
});
|
|
||||||
test('snapshot', () => {
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
it('does not renders share buttons', () => {
|
|
||||||
expect(wrapper.find('FacebookShareButton').length).toEqual(0);
|
|
||||||
expect(wrapper.find('TwitterShareButton').length).toEqual(0);
|
|
||||||
});
|
|
||||||
it('does not render unenroll modal toggle', () => {
|
|
||||||
el = wrapper.find({ 'data-testid': 'unenrollModalToggle' });
|
|
||||||
expect(el.length).toEqual(0);
|
|
||||||
});
|
|
||||||
it('does not render email settings modal toggle', () => {
|
|
||||||
el = wrapper.find({ 'data-testid': 'emailSettingsModalToggle' });
|
|
||||||
expect(el.length).toEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('masquerading', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
|
||||||
wrapper = shallow(<CourseCardMenu {...props} />);
|
|
||||||
});
|
|
||||||
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,143 +63,124 @@ describe('useActionDisabledState', () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runHook = () => hooks.useActionDisabledState(cardId);
|
||||||
describe('disableBeginCourse', () => {
|
describe('disableBeginCourse', () => {
|
||||||
|
const testDisabled = (data, expected) => {
|
||||||
|
mockHooksData(data);
|
||||||
|
expect(runHook().disableBeginCourse).toBe(expected);
|
||||||
|
};
|
||||||
it('disable when homeUrl is invalid', () => {
|
it('disable when homeUrl is invalid', () => {
|
||||||
mockHooksData({ homeUrl: null });
|
testDisabled({ homeUrl: null }, true);
|
||||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableBeginCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when isMasquerading is true', () => {
|
it('disable when isMasquerading is true', () => {
|
||||||
mockHooksData({ isMasquerading: true });
|
testDisabled({ isMasquerading: true }, true);
|
||||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableBeginCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when hasAccess is false', () => {
|
it('disable when hasAccess is false', () => {
|
||||||
mockHooksData({ hasAccess: false });
|
testDisabled({ hasAccess: false }, true);
|
||||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableBeginCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||||
mockHooksData({ isAudit: true, isAuditAccessExpired: true });
|
testDisabled({ isAudit: true, isAuditAccessExpired: true }, true);
|
||||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableBeginCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('enable when all conditions are met', () => {
|
it('enable when all conditions are met', () => {
|
||||||
mockHooksData({ hasAccess: true });
|
testDisabled({ hasAccess: true }, false);
|
||||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableBeginCourse).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('disableResumeCourse', () => {
|
describe('disableResumeCourse', () => {
|
||||||
|
const testDisabled = (data, expected) => {
|
||||||
|
mockHooksData(data);
|
||||||
|
expect(runHook().disableResumeCourse).toBe(expected);
|
||||||
|
};
|
||||||
it('disable when resumeUrl is invalid', () => {
|
it('disable when resumeUrl is invalid', () => {
|
||||||
mockHooksData({ resumeUrl: null });
|
testDisabled({ resumeUrl: null }, true);
|
||||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableResumeCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when isMasquerading is true', () => {
|
it('disable when isMasquerading is true', () => {
|
||||||
mockHooksData({ isMasquerading: true });
|
testDisabled({ isMasquerading: true }, true);
|
||||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableResumeCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when hasAccess is false', () => {
|
it('disable when hasAccess is false', () => {
|
||||||
mockHooksData({ hasAccess: false });
|
testDisabled({ hasAccess: false }, true);
|
||||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableResumeCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||||
mockHooksData({ isAudit: true, isAuditAccessExpired: true });
|
testDisabled({ isAudit: true, isAuditAccessExpired: true }, true);
|
||||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableResumeCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('enable when all conditions are met', () => {
|
it('enable when all conditions are met', () => {
|
||||||
mockHooksData({ hasAccess: true });
|
testDisabled({ hasAccess: true }, false);
|
||||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableResumeCourse).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('disableViewCourse', () => {
|
describe('disableViewCourse', () => {
|
||||||
|
const testDisabled = (data, expected) => {
|
||||||
|
mockHooksData(data);
|
||||||
|
expect(runHook().disableViewCourse).toBe(expected);
|
||||||
|
};
|
||||||
it('disable when hasAccess is false', () => {
|
it('disable when hasAccess is false', () => {
|
||||||
mockHooksData({ hasAccess: false });
|
testDisabled({ hasAccess: false }, true);
|
||||||
const { disableViewCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableViewCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||||
mockHooksData({ isAudit: true, isAuditAccessExpired: true });
|
testDisabled({ isAudit: true, isAuditAccessExpired: true }, true);
|
||||||
const { disableViewCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableViewCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('enable when all conditions are met', () => {
|
it('enable when all conditions are met', () => {
|
||||||
mockHooksData({ hasAccess: true });
|
testDisabled({ hasAccess: true }, false);
|
||||||
const { disableViewCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableViewCourse).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('disableUpgradeCourse', () => {
|
describe('disableUpgradeCourse', () => {
|
||||||
|
const testDisabled = (data, expected) => {
|
||||||
|
mockHooksData(data);
|
||||||
|
expect(runHook().disableUpgradeCourse).toBe(expected);
|
||||||
|
};
|
||||||
it('disable when upgradeUrl is invalid', () => {
|
it('disable when upgradeUrl is invalid', () => {
|
||||||
mockHooksData({ upgradeUrl: null });
|
testDisabled({ upgradeUrl: null }, true);
|
||||||
const { disableUpgradeCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableUpgradeCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when isMasquerading is true and canUpgrade is false', () => {
|
it('disable when isMasquerading is true and canUpgrade is false', () => {
|
||||||
mockHooksData({ isMasquerading: true, canUpgrade: false });
|
testDisabled({ isMasquerading: true, canUpgrade: false }, true);
|
||||||
const { disableUpgradeCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableUpgradeCourse).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('enable when all conditions are met', () => {
|
it('enable when all conditions are met', () => {
|
||||||
mockHooksData({ canUpgrade: true });
|
testDisabled({ canUpgrade: true }, false);
|
||||||
const { disableUpgradeCourse } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableUpgradeCourse).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('disableSelectSession', () => {
|
describe('disableSelectSession', () => {
|
||||||
|
const testDisabled = (data, expected) => {
|
||||||
|
mockHooksData(data);
|
||||||
|
expect(runHook().disableSelectSession).toBe(expected);
|
||||||
|
};
|
||||||
it('disable when isEntitlement is false', () => {
|
it('disable when isEntitlement is false', () => {
|
||||||
mockHooksData({ isEntitlement: false });
|
testDisabled({ isEntitlement: false }, true);
|
||||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableSelectSession).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when isMasquerading is true', () => {
|
it('disable when isMasquerading is true', () => {
|
||||||
mockHooksData({ isMasquerading: true });
|
testDisabled({ isMasquerading: true }, true);
|
||||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableSelectSession).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when hasAccess is false', () => {
|
it('disable when hasAccess is false', () => {
|
||||||
mockHooksData({ hasAccess: false });
|
testDisabled({ hasAccess: false }, true);
|
||||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableSelectSession).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when canChange is false', () => {
|
it('disable when canChange is false', () => {
|
||||||
mockHooksData({ canChange: false });
|
testDisabled({ canChange: false }, true);
|
||||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableSelectSession).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when hasSessions is false', () => {
|
it('disable when hasSessions is false', () => {
|
||||||
mockHooksData({ hasSessions: false });
|
testDisabled({ hasSessions: false }, true);
|
||||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableSelectSession).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('enable when all conditions are met', () => {
|
it('enable when all conditions are met', () => {
|
||||||
mockHooksData({
|
testDisabled(
|
||||||
isEntitlement: true, hasAccess: true, canChange: true, hasSessions: true,
|
{
|
||||||
});
|
isEntitlement: true,
|
||||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
hasAccess: true,
|
||||||
expect(disableSelectSession).toBe(false);
|
canChange: true,
|
||||||
|
hasSessions: true,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('disableCourseTitle', () => {
|
describe('disableCourseTitle', () => {
|
||||||
|
const testDisabled = (data, expected) => {
|
||||||
|
mockHooksData(data);
|
||||||
|
expect(runHook().disableCourseTitle).toBe(expected);
|
||||||
|
};
|
||||||
it('disable when isEntitlement is true and isFulfilled is false', () => {
|
it('disable when isEntitlement is true and isFulfilled is false', () => {
|
||||||
mockHooksData({ isEntitlement: true, isFulfilled: false });
|
testDisabled({ isEntitlement: true, isFulfilled: false }, true);
|
||||||
const { disableCourseTitle } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableCourseTitle).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('disable when disableViewCourse is true', () => {
|
it('disable when disableViewCourse is true', () => {
|
||||||
mockHooksData({ hasAccess: false });
|
testDisabled({ hasAccess: false }, true);
|
||||||
const { disableCourseTitle } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableCourseTitle).toBe(true);
|
|
||||||
});
|
});
|
||||||
it('enable when all conditions are met', () => {
|
it('enable when all conditions are met', () => {
|
||||||
mockHooksData({ isEntitlement: true, isFulfilled: true, hasAccess: true });
|
testDisabled({ isEntitlement: true, isFulfilled: true, hasAccess: true }, false);
|
||||||
const { disableCourseTitle } = hooks.useActionDisabledState(cardId);
|
|
||||||
expect(disableCourseTitle).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,19 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { Container, Col, Row } from '@edx/paragon';
|
import { Container, Col, Row } from '@edx/paragon';
|
||||||
|
|
||||||
|
import WidgetFooter from 'containers/WidgetContainers/WidgetFooter';
|
||||||
import hooks from './hooks';
|
import hooks from './hooks';
|
||||||
|
|
||||||
export const columnConfig = {
|
export const columnConfig = {
|
||||||
courseList: {
|
courseList: {
|
||||||
lg: { span: 12, offset: 0 },
|
withSidebar: {
|
||||||
xl: { span: 8, offset: 0 },
|
lg: { span: 12, offset: 0 },
|
||||||
|
xl: { span: 8, offset: 0 },
|
||||||
|
},
|
||||||
|
noSidebar: {
|
||||||
|
lg: { span: 12, offset: 0 },
|
||||||
|
xl: { span: 12, offset: 0 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
lg: { span: 12, offset: 0 },
|
lg: { span: 12, offset: 0 },
|
||||||
@@ -16,18 +23,31 @@ export const columnConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DashboardLayout = ({ children, sidebar }) => {
|
export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
|
||||||
const isCollapsed = hooks.useIsDashboardCollapsed();
|
const {
|
||||||
|
isCollapsed,
|
||||||
|
sidebarShowing,
|
||||||
|
setSidebarShowing,
|
||||||
|
} = hooks.useDashboardLayoutData();
|
||||||
|
|
||||||
|
const courseListColumnProps = sidebarShowing
|
||||||
|
? columnConfig.courseList.withSidebar
|
||||||
|
: columnConfig.courseList.noSidebar;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container fluid size="xl">
|
<Container fluid size="xl">
|
||||||
<Row>
|
<Row>
|
||||||
<Col {...columnConfig.courseList} className="course-list-column">
|
<Col {...courseListColumnProps} className="course-list-column">
|
||||||
{children}
|
{children}
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...columnConfig.sidebar} className="sidebar-column">
|
<Col {...columnConfig.sidebar} className="sidebar-column">
|
||||||
{!isCollapsed && (<h2 className="course-list-title"> </h2>)}
|
{!isCollapsed && (<h2 className="course-list-title"> </h2>)}
|
||||||
{sidebar}
|
<Sidebar setSidebarShowing={setSidebarShowing} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<WidgetFooter />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
@@ -35,7 +55,7 @@ export const DashboardLayout = ({ children, sidebar }) => {
|
|||||||
};
|
};
|
||||||
DashboardLayout.propTypes = {
|
DashboardLayout.propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
sidebar: PropTypes.node.isRequired,
|
sidebar: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DashboardLayout;
|
export default DashboardLayout;
|
||||||
|
|||||||
@@ -1,60 +1,125 @@
|
|||||||
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { Col, Row } from '@edx/paragon';
|
import { Col, Row } from '@edx/paragon';
|
||||||
|
|
||||||
|
import WidgetFooter from 'containers/WidgetContainers/WidgetFooter';
|
||||||
import hooks from './hooks';
|
import hooks from './hooks';
|
||||||
import DashboardLayout, { columnConfig } from './DashboardLayout';
|
import DashboardLayout, { columnConfig } from './DashboardLayout';
|
||||||
|
|
||||||
jest.mock('./hooks', () => ({
|
jest.mock('./hooks', () => ({
|
||||||
useIsDashboardCollapsed: jest.fn(() => true),
|
useDashboardLayoutData: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
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', () => {
|
describe('DashboardLayout', () => {
|
||||||
const children = 'test-children';
|
beforeEach(() => {
|
||||||
const props = {
|
jest.clearAllMocks();
|
||||||
sidebar: 'test-sidebar-content',
|
el = shallow(<DashboardLayout {...props}>{children}</DashboardLayout>);
|
||||||
};
|
});
|
||||||
const render = () => shallow(<DashboardLayout sidebar={props.sidebar}>{children}</DashboardLayout>);
|
|
||||||
const testColumns = () => {
|
const testColumns = () => {
|
||||||
it('loads courseList and sidebar column layout', () => {
|
it('loads courseList and sidebar column layout', () => {
|
||||||
const columns = render().find(Row).find(Col);
|
const columns = el.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 => {
|
Object.keys(columnConfig.sidebar).forEach(size => {
|
||||||
expect(columns.at(1).props()[size]).toEqual(columnConfig.sidebar[size]);
|
expect(columns.at(1).props()[size]).toEqual(columnConfig.sidebar[size]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('displays children in first column', () => {
|
it('displays children in first column', () => {
|
||||||
const columns = render().find(Row).find(Col);
|
const columns = el.find(Row).find(Col);
|
||||||
expect(columns.at(0).contains(children)).toEqual(true);
|
expect(columns.at(0).contains(children)).toEqual(true);
|
||||||
});
|
});
|
||||||
it('displays sidebar prop in second column', () => {
|
it('displays sidebar prop in second column', () => {
|
||||||
const columns = render().find(Row).find(Col);
|
const columns = el.find(Row).find(Col);
|
||||||
expect(columns.at(1).contains(props.sidebar)).toEqual(true);
|
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 testSnapshot = () => {
|
const testSnapshot = () => {
|
||||||
test('snapshot', () => {
|
test('snapshot', () => {
|
||||||
expect(render()).toMatchSnapshot();
|
expect(el).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
describe('collapsed', () => {
|
describe('collapsed', () => {
|
||||||
testColumns();
|
describe('sidebar showing', () => {
|
||||||
testSnapshot();
|
beforeEach(() => {
|
||||||
|
hooks.useDashboardLayoutData.mockReturnValueOnce({ ...hookProps, sidebarShowing: true });
|
||||||
|
});
|
||||||
|
testColumns();
|
||||||
|
testSnapshot();
|
||||||
|
testSidebarLayout();
|
||||||
|
});
|
||||||
|
describe('sidebar not showing', () => {
|
||||||
|
testColumns();
|
||||||
|
testSnapshot();
|
||||||
|
testNoSidebarLayout();
|
||||||
|
});
|
||||||
it('does not show spacer component above widget sidebar', () => {
|
it('does not show spacer component above widget sidebar', () => {
|
||||||
const columns = render().find(Col);
|
const columns = el.find(Col);
|
||||||
expect(columns.at(1).find('h2').length).toEqual(0);
|
expect(columns.at(1).find('h2').length).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('not collapsed', () => {
|
describe('not collapsed', () => {
|
||||||
beforeEach(() => { hooks.useIsDashboardCollapsed.mockReturnValueOnce(false); });
|
const testWidgetSpacing = () => {
|
||||||
testColumns();
|
it('shows a blank (nbsp) h2 spacer component above widget sidebar', () => {
|
||||||
testSnapshot();
|
const columns = el.find(Col);
|
||||||
it('shows a blank (nbsp) h2 spacer component above widget sidebar', () => {
|
// nonbreaking space equivalent
|
||||||
const columns = render().find(Col);
|
expect(columns.at(1).find('h2').text()).toEqual('\xA0');
|
||||||
// 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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,57 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`DashboardLayout collapsed snapshot 1`] = `
|
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`] = `
|
||||||
<Container
|
<Container
|
||||||
fluid={true}
|
fluid={true}
|
||||||
size="xl"
|
size="xl"
|
||||||
@@ -38,13 +89,76 @@ exports[`DashboardLayout collapsed snapshot 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
test-sidebar-content
|
<mockConstructor
|
||||||
|
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<WidgetFooter />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`DashboardLayout not collapsed snapshot 1`] = `
|
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`] = `
|
||||||
<Container
|
<Container
|
||||||
fluid={true}
|
fluid={true}
|
||||||
size="xl"
|
size="xl"
|
||||||
@@ -87,7 +201,14 @@ exports[`DashboardLayout not collapsed snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
|
|
||||||
</h2>
|
</h2>
|
||||||
test-sidebar-content
|
<mockConstructor
|
||||||
|
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<WidgetFooter />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
|
|||||||
id="dashboard-content"
|
id="dashboard-content"
|
||||||
>
|
>
|
||||||
<DashboardLayout
|
<DashboardLayout
|
||||||
sidebar={<LoadedWidgetSidebar />}
|
sidebar="LoadedWidgetSidebar"
|
||||||
>
|
>
|
||||||
<CourseList />
|
<CourseList />
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
@@ -56,7 +56,7 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
|
|||||||
id="dashboard-content"
|
id="dashboard-content"
|
||||||
>
|
>
|
||||||
<DashboardLayout
|
<DashboardLayout
|
||||||
sidebar={<NoCoursesWidgetSidebar />}
|
sidebar="NoCoursesWidgetSidebar"
|
||||||
>
|
>
|
||||||
<CourseList />
|
<CourseList />
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import React from 'react';
|
|||||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { apiHooks } from 'hooks';
|
import { apiHooks } from 'hooks';
|
||||||
|
import { StrictDict } from 'utils';
|
||||||
|
|
||||||
import appMessages from 'messages';
|
import appMessages from 'messages';
|
||||||
|
import * as module from './hooks';
|
||||||
|
|
||||||
export const useIsDashboardCollapsed = () => {
|
export const state = StrictDict({
|
||||||
const { width } = useWindowSize();
|
sidebarShowing: (val) => React.useState(val), // eslint-disable-line
|
||||||
return width < breakpoints.large.maxWidth;
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export const useInitializeDashboard = () => {
|
export const useInitializeDashboard = () => {
|
||||||
const initialize = apiHooks.useInitializeApp();
|
const initialize = apiHooks.useInitializeApp();
|
||||||
@@ -23,8 +24,18 @@ 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 {
|
export default {
|
||||||
useIsDashboardCollapsed,
|
useDashboardLayoutData,
|
||||||
useInitializeDashboard,
|
useInitializeDashboard,
|
||||||
useDashboardMessages,
|
useDashboardMessages,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
|||||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||||
|
|
||||||
import { apiHooks } from 'hooks';
|
import { apiHooks } from 'hooks';
|
||||||
|
import { MockUseState } from 'testUtils';
|
||||||
|
|
||||||
import appMessages from 'messages';
|
import appMessages from 'messages';
|
||||||
import * as hooks from './hooks';
|
import * as hooks from './hooks';
|
||||||
@@ -19,8 +20,12 @@ jest.mock('hooks', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const state = new MockUseState(hooks);
|
||||||
|
|
||||||
const initializeApp = jest.fn();
|
const initializeApp = jest.fn();
|
||||||
apiHooks.useInitializeApp.mockReturnValue(initializeApp);
|
apiHooks.useInitializeApp.mockReturnValue(initializeApp);
|
||||||
|
useWindowSize.mockReturnValue({ width: 20 });
|
||||||
|
breakpoints.large = { maxWidth: 30 };
|
||||||
describe('CourseCard hooks', () => {
|
describe('CourseCard hooks', () => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
@@ -28,15 +33,32 @@ describe('CourseCard hooks', () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useIsDashboardCollapsed', () => {
|
describe('state fields', () => {
|
||||||
it('returns true iff windowSize width is below the xl breakpoint', () => {
|
state.testGetter(state.keys.sidebarShowing);
|
||||||
useWindowSize.mockReturnValueOnce({ width: 20 });
|
});
|
||||||
breakpoints.large = { maxWidth: 30 };
|
|
||||||
expect(hooks.useIsDashboardCollapsed()).toEqual(true);
|
describe('useDashboardLayoutData', () => {
|
||||||
useWindowSize.mockReturnValueOnce({ width: 40 });
|
beforeEach(() => { state.mock(); });
|
||||||
expect(hooks.useIsDashboardCollapsed()).toEqual(false);
|
describe('behavior', () => {
|
||||||
useWindowSize.mockReturnValueOnce({ width: 40 });
|
it('initializes sidebarShowing to default false value', () => {
|
||||||
expect(hooks.useIsDashboardCollapsed()).toEqual(false);
|
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('useInitializeDashboard', () => {
|
describe('useInitializeDashboard', () => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const Dashboard = () => {
|
|||||||
const hasAvailableDashboards = reduxHooks.useHasAvailableDashboards();
|
const hasAvailableDashboards = reduxHooks.useHasAvailableDashboards();
|
||||||
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
|
const initIsPending = reduxHooks.useRequestIsPending(RequestKeys.initialize);
|
||||||
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
|
const showSelectSessionModal = reduxHooks.useShowSelectSessionModal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="dashboard-container" className="d-flex flex-column p-2 pt-0">
|
<div id="dashboard-container" className="d-flex flex-column p-2 pt-0">
|
||||||
<h1 className="sr-only">{pageTitle}</h1>
|
<h1 className="sr-only">{pageTitle}</h1>
|
||||||
@@ -34,7 +35,7 @@ export const Dashboard = () => {
|
|||||||
{initIsPending
|
{initIsPending
|
||||||
? (<LoadingView />)
|
? (<LoadingView />)
|
||||||
: (
|
: (
|
||||||
<DashboardLayout sidebar={hasCourses ? <LoadedSidebar /> : <NoCoursesSidebar />}>
|
<DashboardLayout sidebar={hasCourses ? LoadedSidebar : NoCoursesSidebar}>
|
||||||
<CourseList />
|
<CourseList />
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ describe('Dashboard', () => {
|
|||||||
showSelectSessionModal: true,
|
showSelectSessionModal: true,
|
||||||
},
|
},
|
||||||
content: ['LoadedView', (
|
content: ['LoadedView', (
|
||||||
<DashboardLayout sidebar={<LoadedWidgetSidebar />}><CourseList /></DashboardLayout>
|
<DashboardLayout sidebar={LoadedWidgetSidebar}><CourseList /></DashboardLayout>
|
||||||
)],
|
)],
|
||||||
showEnterpriseModal: false,
|
showEnterpriseModal: false,
|
||||||
showSelectSessionModal: true,
|
showSelectSessionModal: true,
|
||||||
@@ -132,7 +132,7 @@ describe('Dashboard', () => {
|
|||||||
showSelectSessionModal: false,
|
showSelectSessionModal: false,
|
||||||
},
|
},
|
||||||
content: ['Dashboard layout with no courses sidebar and content', (
|
content: ['Dashboard layout with no courses sidebar and content', (
|
||||||
<DashboardLayout sidebar={<NoCoursesWidgetSidebar />}><CourseList /></DashboardLayout>
|
<DashboardLayout sidebar={NoCoursesWidgetSidebar}><CourseList /></DashboardLayout>
|
||||||
)],
|
)],
|
||||||
showEnterpriseModal: true,
|
showEnterpriseModal: true,
|
||||||
showSelectSessionModal: false,
|
showSelectSessionModal: false,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
|
import { configuration } from '../../config';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ export const BrandLogo = () => {
|
|||||||
<a href={dashboard?.url || '/'} className="mx-auto">
|
<a href={dashboard?.url || '/'} className="mx-auto">
|
||||||
<img
|
<img
|
||||||
className="logo py-3"
|
className="logo py-3"
|
||||||
src="https://edx-cdn.org/v3/prod/logo.svg"
|
src={configuration.LOGO_URL}
|
||||||
alt={formatMessage(messages.logoAltText)}
|
alt={formatMessage(messages.logoAltText)}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import PropTypes from 'prop-types';
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { AppContext } from '@edx/frontend-platform/react';
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
import { Button, Badge } from '@edx/paragon';
|
import { Button, Badge } from '@edx/paragon';
|
||||||
|
|
||||||
|
import WidgetNavbar from 'containers/WidgetContainers/WidgetNavbar';
|
||||||
import urls from 'data/services/lms/urls';
|
import urls from 'data/services/lms/urls';
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
|
import { COLLAPSED_NAVBAR } from 'widgets/RecommendationsPaintedDoorBtn/constants';
|
||||||
|
|
||||||
import { findCoursesNavDropdownClicked } from '../hooks';
|
import { findCoursesNavDropdownClicked } from '../hooks';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
export const CollapseMenuBody = ({ isOpen }) => {
|
export const CollapseMenuBody = ({ isOpen }) => {
|
||||||
@@ -40,6 +40,7 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
|||||||
>
|
>
|
||||||
{formatMessage(messages.discoverNew)}
|
{formatMessage(messages.discoverNew)}
|
||||||
</Button>
|
</Button>
|
||||||
|
<WidgetNavbar placement={COLLAPSED_NAVBAR} />
|
||||||
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
|
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
|
||||||
{formatMessage(messages.help)}
|
{formatMessage(messages.help)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ exports[`CollapseMenuBody render 1`] = `
|
|||||||
>
|
>
|
||||||
Discover New
|
Discover New
|
||||||
</Button>
|
</Button>
|
||||||
|
<WidgetNavbar
|
||||||
|
placement="collapsedNavbar"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/support"
|
href="http://localhost:18000/support"
|
||||||
@@ -92,6 +95,9 @@ exports[`CollapseMenuBody render unauthenticated 1`] = `
|
|||||||
>
|
>
|
||||||
Discover New
|
Discover New
|
||||||
</Button>
|
</Button>
|
||||||
|
<WidgetNavbar
|
||||||
|
placement="collapsedNavbar"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
href="http://localhost:18000/support"
|
href="http://localhost:18000/support"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Menu, Close } from '@edx/paragon/icons';
|
import { MenuIcon, Close } from '@edx/paragon/icons';
|
||||||
import { IconButton, Icon } from '@edx/paragon';
|
import { IconButton, Icon } from '@edx/paragon';
|
||||||
|
|
||||||
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
|
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
|
||||||
@@ -23,7 +23,7 @@ export const CollapsedHeader = () => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
invertColors
|
invertColors
|
||||||
isActive
|
isActive
|
||||||
src={isOpen ? Close : Menu}
|
src={isOpen ? Close : MenuIcon}
|
||||||
iconAs={Icon}
|
iconAs={Icon}
|
||||||
alt={
|
alt={
|
||||||
isOpen
|
isOpen
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ exports[`ExpandedHeader render 1`] = `
|
|||||||
>
|
>
|
||||||
Discover New
|
Discover New
|
||||||
</Button>
|
</Button>
|
||||||
|
<WidgetNavbar
|
||||||
|
placement="expendedNavbar"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
className="flex-grow-1"
|
className="flex-grow-1"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { getConfig } from '@edx/frontend-platform';
|
|||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Button } from '@edx/paragon';
|
import { Button } from '@edx/paragon';
|
||||||
|
|
||||||
|
import WidgetNavbar from 'containers/WidgetContainers/WidgetNavbar';
|
||||||
import urls from 'data/services/lms/urls';
|
import urls from 'data/services/lms/urls';
|
||||||
import { reduxHooks } from 'hooks';
|
import { reduxHooks } from 'hooks';
|
||||||
|
import { EXPANDED_NAVBAR } from 'widgets/RecommendationsPaintedDoorBtn/constants';
|
||||||
|
|
||||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||||
|
|
||||||
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
|
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import BrandLogo from '../BrandLogo';
|
import BrandLogo from '../BrandLogo';
|
||||||
@@ -51,6 +52,7 @@ export const ExpandedHeader = () => {
|
|||||||
>
|
>
|
||||||
{formatMessage(messages.discoverNew)}
|
{formatMessage(messages.discoverNew)}
|
||||||
</Button>
|
</Button>
|
||||||
|
<WidgetNavbar placement={EXPANDED_NAVBAR} />
|
||||||
<span className="flex-grow-1" />
|
<span className="flex-grow-1" />
|
||||||
<Button
|
<Button
|
||||||
as="a"
|
as="a"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ exports[`BrandLogo dashboard defined 1`] = `
|
|||||||
<img
|
<img
|
||||||
alt="edX, Inc. Dashboard"
|
alt="edX, Inc. Dashboard"
|
||||||
className="logo py-3"
|
className="logo py-3"
|
||||||
src="https://edx-cdn.org/v3/prod/logo.svg"
|
src="https://edx-cdn.org/v3/default/logo.svg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
@@ -21,7 +21,7 @@ exports[`BrandLogo dashboard undefined 1`] = `
|
|||||||
<img
|
<img
|
||||||
alt="edX, Inc. Dashboard"
|
alt="edX, Inc. Dashboard"
|
||||||
className="logo py-3"
|
className="logo py-3"
|
||||||
src="https://edx-cdn.org/v3/prod/logo.svg"
|
src="https://edx-cdn.org/v3/default/logo.svg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`AppWrapper WidgetContainer component output no experiments are active snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
This is some
|
||||||
|
<b>
|
||||||
|
test
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<i>
|
||||||
|
content
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`AppWrapper WidgetContainer component output painted door experiment is active (08/23) snapshot 1`] = `
|
||||||
|
<PaintedDoorExperimentProvider>
|
||||||
|
<div>
|
||||||
|
This is some
|
||||||
|
<b>
|
||||||
|
test
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<i>
|
||||||
|
content
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</PaintedDoorExperimentProvider>
|
||||||
|
`;
|
||||||
23
src/containers/WidgetContainers/AppWrapper/index.jsx
Normal file
23
src/containers/WidgetContainers/AppWrapper/index.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import PaintedDoorExperimentProvider from 'widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
|
||||||
|
|
||||||
|
export const AppWrapper = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
console.log(`process.env.EXPERIMENT_08_23_VAN_PAINTED_DOOR = ${Boolean(process.env.EXPERIMENT_08_23_VAN_PAINTED_DOOR)}`);
|
||||||
|
return (
|
||||||
|
<PaintedDoorExperimentProvider>
|
||||||
|
{children}
|
||||||
|
</PaintedDoorExperimentProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
AppWrapper.propTypes = {
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.node,
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
]).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppWrapper;
|
||||||
56
src/containers/WidgetContainers/AppWrapper/index.test.jsx
Normal file
56
src/containers/WidgetContainers/AppWrapper/index.test.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { shallow } from '@edx/react-unit-test-utils';
|
||||||
|
|
||||||
|
import PaintedDoorExperimentProvider from 'widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
|
||||||
|
|
||||||
|
import AppWrapper from '.';
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext',
|
||||||
|
() => 'PaintedDoorExperimentProvider',
|
||||||
|
);
|
||||||
|
|
||||||
|
let el;
|
||||||
|
|
||||||
|
const children = (<div>This is some <b>test</b> <i>content</i></div>);
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
el = shallow(<AppWrapper>{children}</AppWrapper>);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockAndRenderForBlock = (newVal) => {
|
||||||
|
const oldVal = process.env;
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env = { ...oldVal, ...newVal };
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = oldVal;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('AppWrapper WidgetContainer component', () => {
|
||||||
|
describe('output', () => {
|
||||||
|
describe('painted door experiment is active (08/23)', () => {
|
||||||
|
mockAndRenderForBlock({ EXPERIMENT_08_23_VAN_PAINTED_DOOR: true });
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el.snapshot).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
it('renders children wrapped in PaintedDoorExperimentProvider', () => {
|
||||||
|
const control = el.instance.findByType(PaintedDoorExperimentProvider)[0];
|
||||||
|
expect(el.instance).toEqual(control);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('no experiments are active', () => {
|
||||||
|
mockAndRenderForBlock({ EXPERIMENT_08_23_VAN_PAINTED_DOOR: false });
|
||||||
|
test('snapshot', () => {
|
||||||
|
expect(el.snapshot).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
it('renders children wrapped in PaintedDoorExperimentProvider', () => {
|
||||||
|
expect(el.instance.matches(shallow(children))).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ exports[`WidgetSidebar snapshots default 1`] = `
|
|||||||
className="widget-sidebar"
|
className="widget-sidebar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="d-flex"
|
className="d-flex flex-column"
|
||||||
>
|
>
|
||||||
<RecommendationsPanel />
|
<RecommendationsPanel />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import RecommendationsPanel from 'widgets/RecommendationsPanel';
|
import RecommendationsPanel from 'widgets/RecommendationsPanel';
|
||||||
|
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||||
|
|
||||||
export const WidgetSidebar = () => (
|
export const WidgetSidebar = ({ setSidebarShowing }) => {
|
||||||
<div className="widget-sidebar">
|
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
|
||||||
<div className="d-flex">
|
|
||||||
<RecommendationsPanel />
|
if (!inRecommendationsVariant && isExperimentActive) {
|
||||||
</div>
|
setSidebarShowing(true);
|
||||||
</div>
|
|
||||||
);
|
return (
|
||||||
|
<div className="widget-sidebar">
|
||||||
|
<div className="d-flex flex-column">
|
||||||
|
<RecommendationsPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
WidgetSidebar.propTypes = {
|
||||||
|
setSidebarShowing: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default WidgetSidebar;
|
export default WidgetSidebar;
|
||||||
|
|||||||
@@ -1,14 +1,49 @@
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||||
|
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
|
||||||
import WidgetSidebar from '.';
|
import WidgetSidebar from '.';
|
||||||
|
|
||||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||||
|
jest.mock('widgets/ProductRecommendations/hooks', () => ({
|
||||||
|
useShowRecommendationsFooter: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('WidgetSidebar', () => {
|
describe('WidgetSidebar', () => {
|
||||||
|
beforeEach(() => jest.resetAllMocks());
|
||||||
|
const props = {
|
||||||
|
setSidebarShowing: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
describe('snapshots', () => {
|
describe('snapshots', () => {
|
||||||
test('default', () => {
|
test('default', () => {
|
||||||
const wrapper = shallow(<WidgetSidebar />);
|
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||||
|
mockFooterRecommendationsHook.activeControl,
|
||||||
|
);
|
||||||
|
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||||
|
|
||||||
|
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
|
||||||
expect(wrapper).toMatchSnapshot();
|
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,13 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import RecommendationsPanel from 'widgets/RecommendationsPanel';
|
import RecommendationsPanel from 'widgets/RecommendationsPanel';
|
||||||
|
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||||
|
|
||||||
export const WidgetSidebar = () => (
|
export const WidgetSidebar = ({ setSidebarShowing }) => {
|
||||||
<div className="widget-sidebar px-2">
|
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
|
||||||
<div className="d-flex">
|
|
||||||
<RecommendationsPanel />
|
if (!inRecommendationsVariant && isExperimentActive) {
|
||||||
</div>
|
setSidebarShowing(true);
|
||||||
</div>
|
|
||||||
);
|
return (
|
||||||
|
<div className="widget-sidebar px-2">
|
||||||
|
<div className="d-flex">
|
||||||
|
<RecommendationsPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
WidgetSidebar.propTypes = {
|
||||||
|
setSidebarShowing: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default WidgetSidebar;
|
export default WidgetSidebar;
|
||||||
|
|||||||
@@ -1,14 +1,49 @@
|
|||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import hooks from 'widgets/ProductRecommendations/hooks';
|
||||||
|
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
|
||||||
import WidgetSidebar from '.';
|
import WidgetSidebar from '.';
|
||||||
|
|
||||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||||
|
jest.mock('widgets/ProductRecommendations/hooks', () => ({
|
||||||
|
useShowRecommendationsFooter: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('WidgetSidebar', () => {
|
describe('WidgetSidebar', () => {
|
||||||
|
beforeEach(() => jest.resetAllMocks());
|
||||||
|
const props = {
|
||||||
|
setSidebarShowing: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
describe('snapshots', () => {
|
describe('snapshots', () => {
|
||||||
test('default', () => {
|
test('default', () => {
|
||||||
const wrapper = shallow(<WidgetSidebar />);
|
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
|
||||||
|
mockFooterRecommendationsHook.activeControl,
|
||||||
|
);
|
||||||
|
const wrapper = shallow(<WidgetSidebar {...props} />);
|
||||||
|
|
||||||
|
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
|
||||||
expect(wrapper).toMatchSnapshot();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`WidgetFooter snapshots default 1`] = `
|
||||||
|
<div
|
||||||
|
className="widget-footer"
|
||||||
|
>
|
||||||
|
<ProductRecommendations />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
21
src/containers/WidgetContainers/WidgetFooter/index.jsx
Normal file
21
src/containers/WidgetContainers/WidgetFooter/index.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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;
|
||||||
45
src/containers/WidgetContainers/WidgetFooter/index.test.jsx
Normal file
45
src/containers/WidgetContainers/WidgetFooter/index.test.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`WidgetNavbar snapshots default 1`] = `
|
||||||
|
<RecommendationsPaintedDoorBtn
|
||||||
|
experimentVariation=""
|
||||||
|
placement="expendedNavbar"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
29
src/containers/WidgetContainers/WidgetNavbar/index.jsx
Normal file
29
src/containers/WidgetContainers/WidgetNavbar/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import RecommendationsPaintedDoorBtn from 'widgets/RecommendationsPaintedDoorBtn';
|
||||||
|
import { COLLAPSED_NAVBAR, EXPANDED_NAVBAR } from 'widgets/RecommendationsPaintedDoorBtn/constants';
|
||||||
|
import {
|
||||||
|
usePaintedDoorExperimentContext,
|
||||||
|
} from 'widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
|
||||||
|
|
||||||
|
export const WidgetNavbar = ({ placement }) => {
|
||||||
|
const {
|
||||||
|
experimentVariation,
|
||||||
|
isPaintedDoorNavbarBtnVariation,
|
||||||
|
experimentLoading,
|
||||||
|
} = usePaintedDoorExperimentContext();
|
||||||
|
|
||||||
|
if (!experimentLoading && isPaintedDoorNavbarBtnVariation) {
|
||||||
|
return (
|
||||||
|
<RecommendationsPaintedDoorBtn placement={placement} experimentVariation={experimentVariation} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
WidgetNavbar.propTypes = {
|
||||||
|
placement: PropTypes.oneOf([COLLAPSED_NAVBAR, EXPANDED_NAVBAR]).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WidgetNavbar;
|
||||||
65
src/containers/WidgetContainers/WidgetNavbar/index.test.jsx
Normal file
65
src/containers/WidgetContainers/WidgetNavbar/index.test.jsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import {
|
||||||
|
usePaintedDoorExperimentContext,
|
||||||
|
} from '../../../widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext';
|
||||||
|
import WidgetNavbar from './index';
|
||||||
|
import { EXPANDED_NAVBAR } from '../../../widgets/RecommendationsPaintedDoorBtn/constants';
|
||||||
|
import RecommendationsPaintedDoorBtn from '../../../widgets/RecommendationsPaintedDoorBtn';
|
||||||
|
|
||||||
|
jest.mock('widgets/RecommendationsPaintedDoorBtn/PaintedDoorExperimentContext', () => ({
|
||||||
|
usePaintedDoorExperimentContext: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('WidgetNavbar', () => {
|
||||||
|
let mockExperimentContext = {
|
||||||
|
experimentVariation: '',
|
||||||
|
isPaintedDoorNavbarBtnVariation: true,
|
||||||
|
experimentLoading: false,
|
||||||
|
};
|
||||||
|
const props = {
|
||||||
|
placement: EXPANDED_NAVBAR,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('snapshots', () => {
|
||||||
|
test('default', () => {
|
||||||
|
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
|
||||||
|
const wrapper = shallow(<WidgetNavbar {...props} />);
|
||||||
|
|
||||||
|
expect(usePaintedDoorExperimentContext).toHaveBeenCalled();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders button if user in navbar variation', () => {
|
||||||
|
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
|
||||||
|
const wrapper = shallow(<WidgetNavbar {...props} />);
|
||||||
|
|
||||||
|
expect(usePaintedDoorExperimentContext).toHaveBeenCalled();
|
||||||
|
expect(wrapper.type()).toBe(RecommendationsPaintedDoorBtn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders nothing if user in not in navbar variation', () => {
|
||||||
|
mockExperimentContext = {
|
||||||
|
...mockExperimentContext,
|
||||||
|
isPaintedDoorNavbarBtnVariation: false,
|
||||||
|
};
|
||||||
|
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
|
||||||
|
const wrapper = shallow(<WidgetNavbar {...props} />);
|
||||||
|
|
||||||
|
expect(usePaintedDoorExperimentContext).toHaveBeenCalled();
|
||||||
|
expect(wrapper.type()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders nothing if experiment is loading', () => {
|
||||||
|
mockExperimentContext = {
|
||||||
|
...mockExperimentContext,
|
||||||
|
isPaintedDoorNavbarBtnVariation: false,
|
||||||
|
experimentLoading: true,
|
||||||
|
};
|
||||||
|
usePaintedDoorExperimentContext.mockReturnValueOnce(mockExperimentContext);
|
||||||
|
const wrapper = shallow(<WidgetNavbar {...props} />);
|
||||||
|
|
||||||
|
expect(usePaintedDoorExperimentContext).toHaveBeenCalled();
|
||||||
|
expect(wrapper.type()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
17
src/data/constants/course.js
Normal file
17
src/data/constants/course.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// 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,
|
||||||
|
];
|
||||||
@@ -12,6 +12,7 @@ const initialState = {
|
|||||||
suggestedCourses: [],
|
suggestedCourses: [],
|
||||||
filterState: {},
|
filterState: {},
|
||||||
selectSessionModal: {},
|
selectSessionModal: {},
|
||||||
|
certificatePreviewModal: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cardId = (val) => `card-${val}`;
|
export const cardId = (val) => `card-${val}`;
|
||||||
@@ -48,6 +49,10 @@ const app = createSlice({
|
|||||||
...state,
|
...state,
|
||||||
selectSessionModal: { cardId: payload },
|
selectSessionModal: { cardId: payload },
|
||||||
}),
|
}),
|
||||||
|
updateCertificatePreviewModal: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
certificatePreviewModal: { cardId: payload },
|
||||||
|
}),
|
||||||
setPageNumber: (state, { payload }) => ({ ...state, pageNumber: payload }),
|
setPageNumber: (state, { payload }) => ({ ...state, pageNumber: payload }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,9 +19,15 @@ export const showSelectSessionModal = createSelector(
|
|||||||
(data) => data.cardId != null,
|
(data) => data.cardId != null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const showCertificatePreviewModal = createSelector(
|
||||||
|
[simpleSelectors.certificatePreviewModal],
|
||||||
|
(data) => data.cardId != null,
|
||||||
|
);
|
||||||
|
|
||||||
export default StrictDict({
|
export default StrictDict({
|
||||||
numCourses,
|
numCourses,
|
||||||
hasCourses,
|
hasCourses,
|
||||||
hasAvailableDashboards,
|
hasAvailableDashboards,
|
||||||
showSelectSessionModal,
|
showSelectSessionModal,
|
||||||
|
showCertificatePreviewModal,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { StrictDict } from 'utils';
|
import { StrictDict } from 'utils';
|
||||||
import { baseAppUrl } from 'data/services/lms/urls';
|
import { baseAppUrl } from 'data/services/lms/urls';
|
||||||
|
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||||
|
|
||||||
import * as module from './courseCard';
|
import * as module from './courseCard';
|
||||||
import * as simpleSelectors from './simpleSelectors';
|
import * as simpleSelectors from './simpleSelectors';
|
||||||
@@ -15,17 +16,14 @@ export const loadDateVal = (date) => (date ? new Date(date) : null);
|
|||||||
export const courseCard = StrictDict({
|
export const courseCard = StrictDict({
|
||||||
certificate: mkCardSelector(
|
certificate: mkCardSelector(
|
||||||
cardSimpleSelectors.certificate,
|
cardSimpleSelectors.certificate,
|
||||||
(certificate) => {
|
(certificate) => (certificate === null ? {} : ({
|
||||||
const availableDate = new Date(certificate.availableDate);
|
availableDate: new Date(certificate.availableDate),
|
||||||
const isAvailable = availableDate <= new Date();
|
certPreviewUrl: baseAppUrl(certificate.certPreviewUrl),
|
||||||
return {
|
isDownloadable: certificate.isDownloadable,
|
||||||
availableDate,
|
isEarnedButUnavailable: certificate.isEarned && new Date(certificate.availableDate) > new Date(),
|
||||||
certPreviewUrl: baseAppUrl(certificate.certPreviewUrl),
|
isRestricted: certificate.isRestricted,
|
||||||
isDownloadable: certificate.isDownloadable,
|
isEarned: certificate.isEarned,
|
||||||
isEarnedButUnavailable: certificate.isEarned && !isAvailable,
|
})),
|
||||||
isRestricted: certificate.isRestricted,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
course: mkCardSelector(
|
course: mkCardSelector(
|
||||||
cardSimpleSelectors.course,
|
cardSimpleSelectors.course,
|
||||||
@@ -100,6 +98,8 @@ export const courseCard = StrictDict({
|
|||||||
|
|
||||||
isEmailEnabled: enrollment.isEmailEnabled,
|
isEmailEnabled: enrollment.isEmailEnabled,
|
||||||
hasOptedOutOfEmail: enrollment.hasOptedOutOfEmail,
|
hasOptedOutOfEmail: enrollment.hasOptedOutOfEmail,
|
||||||
|
mode: enrollment.mode,
|
||||||
|
isExecEd2UCourse: EXECUTIVE_EDUCATION_COURSE_MODES.includes(enrollment.mode),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { keyStore } from 'utils';
|
import { keyStore } from 'utils';
|
||||||
import { baseAppUrl } from 'data/services/lms/urls';
|
import { baseAppUrl } from 'data/services/lms/urls';
|
||||||
|
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||||
|
|
||||||
import simpleSelectors from './simpleSelectors';
|
import simpleSelectors from './simpleSelectors';
|
||||||
import * as module from './courseCard';
|
import * as module from './courseCard';
|
||||||
@@ -79,6 +80,9 @@ describe('courseCard selectors module', () => {
|
|||||||
it('returns a card selector based on certificate cardSimpleSelector', () => {
|
it('returns a card selector based on certificate cardSimpleSelector', () => {
|
||||||
expect(simpleSelector).toEqual(cardSimpleSelectors.certificate);
|
expect(simpleSelector).toEqual(cardSimpleSelectors.certificate);
|
||||||
});
|
});
|
||||||
|
it('returns {} object if null certificate received', () => {
|
||||||
|
expect(selector(null)).toEqual({});
|
||||||
|
});
|
||||||
it('passes availableDate, converted to a date', () => {
|
it('passes availableDate, converted to a date', () => {
|
||||||
expect(selected.availableDate).toMatchObject(new Date(testData.availableDate));
|
expect(selected.availableDate).toMatchObject(new Date(testData.availableDate));
|
||||||
});
|
});
|
||||||
@@ -162,6 +166,9 @@ describe('courseCard selectors module', () => {
|
|||||||
it('returns a card selector based on courseRun cardSimpleSelector', () => {
|
it('returns a card selector based on courseRun cardSimpleSelector', () => {
|
||||||
expect(simpleSelector).toEqual(cardSimpleSelectors.courseRun);
|
expect(simpleSelector).toEqual(cardSimpleSelectors.courseRun);
|
||||||
});
|
});
|
||||||
|
it('returns {} object if null courseRun received', () => {
|
||||||
|
expect(selector(null)).toEqual({});
|
||||||
|
});
|
||||||
it('passes [endDate, startDate], converted to dates', () => {
|
it('passes [endDate, startDate], converted to dates', () => {
|
||||||
expect(selected.endDate).toEqual(new Date(testData.endDate));
|
expect(selected.endDate).toEqual(new Date(testData.endDate));
|
||||||
expect(selected.startDate).toEqual(new Date(testData.startDate));
|
expect(selected.startDate).toEqual(new Date(testData.startDate));
|
||||||
@@ -222,23 +229,25 @@ describe('courseCard selectors module', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('enrollment selector', () => {
|
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(() => {
|
beforeEach(() => {
|
||||||
loadSelector(courseCard.enrollment, {
|
loadSelector(courseCard.enrollment, 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',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
it('returns a card selector based on enrollment cardSimpleSelector', () => {
|
it('returns a card selector based on enrollment cardSimpleSelector', () => {
|
||||||
expect(simpleSelector).toEqual(cardSimpleSelectors.enrollment);
|
expect(simpleSelector).toEqual(cardSimpleSelectors.enrollment);
|
||||||
@@ -274,6 +283,13 @@ describe('courseCard selectors module', () => {
|
|||||||
it('passes isEmailEnabled', () => {
|
it('passes isEmailEnabled', () => {
|
||||||
expect(selected.isEmailEnabled).toEqual(testData.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', () => {
|
describe('entitlement selector', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ export const simpleSelectors = StrictDict({
|
|||||||
platformSettings: mkSimpleSelector(app => app.platformSettings),
|
platformSettings: mkSimpleSelector(app => app.platformSettings),
|
||||||
suggestedCourses: mkSimpleSelector(app => app.suggestedCourses),
|
suggestedCourses: mkSimpleSelector(app => app.suggestedCourses),
|
||||||
emailConfirmation: mkSimpleSelector(app => app.emailConfirmation),
|
emailConfirmation: mkSimpleSelector(app => app.emailConfirmation),
|
||||||
enterpriseDashboard: mkSimpleSelector(app => app.enterpriseDashboard),
|
enterpriseDashboard: mkSimpleSelector(app => app.enterpriseDashboard || {}),
|
||||||
selectSessionModal: mkSimpleSelector(app => app.selectSessionModal),
|
selectSessionModal: mkSimpleSelector(app => app.selectSessionModal),
|
||||||
|
certificatePreviewModal: mkSimpleSelector(app => app.certificatePreviewModal),
|
||||||
pageNumber: mkSimpleSelector(app => app.pageNumber),
|
pageNumber: mkSimpleSelector(app => app.pageNumber),
|
||||||
socialShareSettings: mkSimpleSelector(app => app.socialShareSettings),
|
socialShareSettings: mkSimpleSelector(app => app.socialShareSettings),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ describe('app simple selectors', () => {
|
|||||||
expect(preSelectors).toEqual([appSelector]);
|
expect(preSelectors).toEqual([appSelector]);
|
||||||
expect(cb(testState.app)).toEqual(testString);
|
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', () => {
|
describe('cardSimpleSelectors', () => {
|
||||||
keys = keyStore(cardSimpleSelectors);
|
keys = keyStore(cardSimpleSelectors);
|
||||||
test.each([
|
test.each([
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export const useEmailConfirmationData = () => useSelector(selectors.emailConfirm
|
|||||||
export const useEnterpriseDashboardData = () => useSelector(selectors.enterpriseDashboard);
|
export const useEnterpriseDashboardData = () => useSelector(selectors.enterpriseDashboard);
|
||||||
export const usePlatformSettingsData = () => useSelector(selectors.platformSettings);
|
export const usePlatformSettingsData = () => useSelector(selectors.platformSettings);
|
||||||
export const useSelectSessionModalData = () => useSelector(selectors.selectSessionModal);
|
export const useSelectSessionModalData = () => useSelector(selectors.selectSessionModal);
|
||||||
|
export const useCertificatePreviewData = () => useSelector(selectors.certificatePreviewModal);
|
||||||
|
|
||||||
export const useSocialShareSettings = () => useSelector(selectors.socialShareSettings);
|
export const useSocialShareSettings = () => useSelector(selectors.socialShareSettings);
|
||||||
|
|
||||||
/** global-level meta-selectors **/
|
/** global-level meta-selectors **/
|
||||||
@@ -22,6 +24,7 @@ export const useCurrentCourseList = (opts) => useSelector(
|
|||||||
state => selectors.currentList(state, opts),
|
state => selectors.currentList(state, opts),
|
||||||
);
|
);
|
||||||
export const useShowSelectSessionModal = () => useSelector(selectors.showSelectSessionModal);
|
export const useShowSelectSessionModal = () => useSelector(selectors.showSelectSessionModal);
|
||||||
|
export const useShowCertificatePreviewModal = () => useSelector(selectors.showCertificatePreviewModal);
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
export const useCourseCardData = (selector) => (cardId) => useSelector(
|
export const useCourseCardData = (selector) => (cardId) => useSelector(
|
||||||
@@ -55,12 +58,23 @@ export const useCardSocialSettingsData = (cardId) => {
|
|||||||
return { facebook: loadSettings(facebook), twitter: loadSettings(twitter) };
|
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 **/
|
/** Events **/
|
||||||
export const useUpdateSelectSessionModalCallback = (cardId) => {
|
export const useUpdateSelectSessionModalCallback = (cardId) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
return () => dispatch(actions.updateSelectSessionModal(cardId));
|
return () => dispatch(actions.updateSelectSessionModal(cardId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdateCertificatePreviewModalCallback = (cardId) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return () => dispatch(actions.updateCertificatePreviewModal(cardId));
|
||||||
|
};
|
||||||
|
|
||||||
export const useTrackCourseEvent = (tracker, cardId, ...args) => {
|
export const useTrackCourseEvent = (tracker, cardId, ...args) => {
|
||||||
const { courseId } = module.useCardCourseRunData(cardId);
|
const { courseId } = module.useCardCourseRunData(cardId);
|
||||||
return (e) => tracker(courseId, ...args)(e);
|
return (e) => tracker(courseId, ...args)(e);
|
||||||
|
|||||||
@@ -761,10 +761,13 @@ export const compileCourseRunData = ({ courseName, ...data }, index) => {
|
|||||||
credit: {},
|
credit: {},
|
||||||
...data,
|
...data,
|
||||||
certificate: genCertificateData(data.certificate),
|
certificate: genCertificateData(data.certificate),
|
||||||
enrollment: genEnrollmentData({ lastEnrolled, ...data.enrollment }),
|
enrollment: genEnrollmentData({
|
||||||
|
lastEnrolled,
|
||||||
|
...getOption(emailOptions, index),
|
||||||
|
...data.enrollment,
|
||||||
|
}),
|
||||||
courseRun: genCourseRunData({
|
courseRun: genCourseRunData({
|
||||||
...data.courseRun,
|
...data.courseRun,
|
||||||
...getOption(emailOptions, index),
|
|
||||||
courseId,
|
courseId,
|
||||||
}),
|
}),
|
||||||
course: {
|
course: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||||
|
import { messages as paragonMessages } from '@edx/paragon';
|
||||||
|
|
||||||
import arMessages from './messages/ar.json';
|
import arMessages from './messages/ar.json';
|
||||||
// no need to import en messages-- they are in the defaultMessage field
|
// no need to import en messages-- they are in the defaultMessage field
|
||||||
@@ -20,4 +21,5 @@ const appMessages = {
|
|||||||
export default [
|
export default [
|
||||||
footerMessages,
|
footerMessages,
|
||||||
appMessages,
|
appMessages,
|
||||||
|
paragonMessages,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,25 +14,12 @@
|
|||||||
"leanerDashboard.confirmEmailModalHeader": "قم بتأكيد بريدك الإلكتروني",
|
"leanerDashboard.confirmEmailModalHeader": "قم بتأكيد بريدك الإلكتروني",
|
||||||
"leanerDashboard.confirmEmailModalBody": "لقد أرسلنا إليك بريدًا إلكترونيًا للتحقق من حسابك. يرجى التحقق من بريدك الوارد والنقر على الزر الأحمر الكبير للتأكيد ومتابعة التعلم.",
|
"leanerDashboard.confirmEmailModalBody": "لقد أرسلنا إليك بريدًا إلكترونيًا للتحقق من حسابك. يرجى التحقق من بريدك الوارد والنقر على الزر الأحمر الكبير للتأكيد ومتابعة التعلم.",
|
||||||
"leanerDashboard.confirmEmailImageAlt": "ايقونة زر تأكيد البريد الإلكتروني",
|
"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.menu.dashboard.label": "لوحة التحكم",
|
||||||
"learnerVariantDashboard.help.label": "المساعدة",
|
"learnerVariantDashboard.help.label": "المساعدة",
|
||||||
"learnerVariantDashboard.menu.profile.label": "الملف الشخصي",
|
"learnerVariantDashboard.menu.profile.label": "الملف الشخصي",
|
||||||
"learnerVariantDashboard.menu.viewPrograms.label": "عرض البرامج",
|
"learnerVariantDashboard.menu.viewPrograms.label": "عرض البرامج",
|
||||||
"learnerVariantDashboard.menu.account.label": "الحساب",
|
"learnerVariantDashboard.menu.account.label": "الحساب",
|
||||||
"learnerVariantDashboard.menu.orderHistory.label": "المشتريات السابقة",
|
"learnerVariantDashboard.menu.orderHistory.label": "Order History",
|
||||||
"learnerVariantDashboard.menu.signOut.label": "تسجيل الخروج",
|
"learnerVariantDashboard.menu.signOut.label": "تسجيل الخروج",
|
||||||
"learnerVariantDashboard.course": "المساقات",
|
"learnerVariantDashboard.course": "المساقات",
|
||||||
"learnerVariantDashboard.program": "البرامج",
|
"learnerVariantDashboard.program": "البرامج",
|
||||||
@@ -40,6 +27,8 @@
|
|||||||
"learnerVariantDashboard.logoAltText": ". لوحة القيادة",
|
"learnerVariantDashboard.logoAltText": ". لوحة القيادة",
|
||||||
"learnerVariantDashboard.collapseMenuOpenAltText": "القائمة",
|
"learnerVariantDashboard.collapseMenuOpenAltText": "القائمة",
|
||||||
"learnerVariantDashboard.collapseMenuClosedAltText": "إغلاق",
|
"learnerVariantDashboard.collapseMenuClosedAltText": "إغلاق",
|
||||||
|
"leanerDashboard.menu.career.label": "Career",
|
||||||
|
"header.menu.new.label": "New",
|
||||||
"MasqueradeBar.ViewAs": "عرض كـ :",
|
"MasqueradeBar.ViewAs": "عرض كـ :",
|
||||||
"MasqueradeBar.ViewingAs": "يتم عرض كـ:",
|
"MasqueradeBar.ViewingAs": "يتم عرض كـ:",
|
||||||
"MasqueradeBar.SubmitButton": "تأكيد",
|
"MasqueradeBar.SubmitButton": "تأكيد",
|
||||||
@@ -48,6 +37,20 @@
|
|||||||
"MasqueradeBar.UnknownError": "حدث خطأ غير معروف",
|
"MasqueradeBar.UnknownError": "حدث خطأ غير معروف",
|
||||||
"WidgetSidebar.lookingForChallengePrompt": "هل تبحث عن تحد جديد؟",
|
"WidgetSidebar.lookingForChallengePrompt": "هل تبحث عن تحد جديد؟",
|
||||||
"WidgetSidebar.findCoursesButton": "ابحث عن مساق {arrow}",
|
"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.recommendationsHeading": "توصيات خاصة لك",
|
||||||
"RecommendationsPanel.popularCoursesHeading": "المساقات الشائعة",
|
"RecommendationsPanel.popularCoursesHeading": "المساقات الشائعة",
|
||||||
"RecommendationsPanel.exploreCoursesButton": "استكشف المساقات"
|
"RecommendationsPanel.exploreCoursesButton": "استكشف المساقات"
|
||||||
|
|||||||
@@ -7,26 +7,13 @@
|
|||||||
"leanerDashboard.enterpriseDialogHeader": "Tienes acceso al panel {label}",
|
"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.enterpriseDialogBody": "Para acceder a los cursos disponibles para usted a través de {label}, visite el panel {label} ahora.",
|
||||||
"leanerDashboard.enterpriseDialogDismissButton": "Despedir",
|
"leanerDashboard.enterpriseDialogDismissButton": "Despedir",
|
||||||
"leanerDashboard.enterpriseDialogConfirmButton": "Go to dashboard",
|
"leanerDashboard.enterpriseDialogConfirmButton": "Ir al panel principal",
|
||||||
"leanerDashboard.confirmEmailBanner": "Confirmar ahora",
|
"leanerDashboard.confirmEmailBanner": "Confirmar ahora",
|
||||||
"leanerDashboard.confirmEmailTextReminderBanner": "¡Recuerda confirmar tu correo electrónico para que puedas seguir aprendiendo en edX! {confirmNowButton}.",
|
"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.verifiedConfirmEmailButton": "He confirmado mi correo electrónico.",
|
||||||
"leanerDashboard.confirmEmailModalHeader": "confirme su email",
|
"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.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.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.menu.dashboard.label": "Panel de Control",
|
||||||
"learnerVariantDashboard.help.label": "Ayuda",
|
"learnerVariantDashboard.help.label": "Ayuda",
|
||||||
"learnerVariantDashboard.menu.profile.label": "Perfil",
|
"learnerVariantDashboard.menu.profile.label": "Perfil",
|
||||||
@@ -40,6 +27,8 @@
|
|||||||
"learnerVariantDashboard.logoAltText": "Tablero de edX, Inc.",
|
"learnerVariantDashboard.logoAltText": "Tablero de edX, Inc.",
|
||||||
"learnerVariantDashboard.collapseMenuOpenAltText": "Menú",
|
"learnerVariantDashboard.collapseMenuOpenAltText": "Menú",
|
||||||
"learnerVariantDashboard.collapseMenuClosedAltText": "Cerrar",
|
"learnerVariantDashboard.collapseMenuClosedAltText": "Cerrar",
|
||||||
|
"leanerDashboard.menu.career.label": "Carrera",
|
||||||
|
"header.menu.new.label": "Nuevo",
|
||||||
"MasqueradeBar.ViewAs": "Ver como:",
|
"MasqueradeBar.ViewAs": "Ver como:",
|
||||||
"MasqueradeBar.ViewingAs": "Viendo como:",
|
"MasqueradeBar.ViewingAs": "Viendo como:",
|
||||||
"MasqueradeBar.SubmitButton": "Enviar",
|
"MasqueradeBar.SubmitButton": "Enviar",
|
||||||
@@ -48,6 +37,20 @@
|
|||||||
"MasqueradeBar.UnknownError": "Un error desconocido ocurrió",
|
"MasqueradeBar.UnknownError": "Un error desconocido ocurrió",
|
||||||
"WidgetSidebar.lookingForChallengePrompt": "¿Buscando un nuevo reto?",
|
"WidgetSidebar.lookingForChallengePrompt": "¿Buscando un nuevo reto?",
|
||||||
"WidgetSidebar.findCoursesButton": "Encuentra un curso {arrow}",
|
"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.recommendationsHeading": "Recomendaciones para ti",
|
||||||
"RecommendationsPanel.popularCoursesHeading": "Cursos populares",
|
"RecommendationsPanel.popularCoursesHeading": "Cursos populares",
|
||||||
"RecommendationsPanel.exploreCoursesButton": "Explorar cursos"
|
"RecommendationsPanel.exploreCoursesButton": "Explorar cursos"
|
||||||
|
|||||||
@@ -14,25 +14,12 @@
|
|||||||
"leanerDashboard.confirmEmailModalHeader": "Confirmer votre 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.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.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.menu.dashboard.label": "Tableau de bord",
|
||||||
"learnerVariantDashboard.help.label": "Aide",
|
"learnerVariantDashboard.help.label": "Aide",
|
||||||
"learnerVariantDashboard.menu.profile.label": "Profil",
|
"learnerVariantDashboard.menu.profile.label": "Profil",
|
||||||
"learnerVariantDashboard.menu.viewPrograms.label": "Voir les programmes",
|
"learnerVariantDashboard.menu.viewPrograms.label": "Voir les programmes",
|
||||||
"learnerVariantDashboard.menu.account.label": "Compte",
|
"learnerVariantDashboard.menu.account.label": "Compte",
|
||||||
"learnerVariantDashboard.menu.orderHistory.label": "Historique des commandes",
|
"learnerVariantDashboard.menu.orderHistory.label": "Order History",
|
||||||
"learnerVariantDashboard.menu.signOut.label": "Se déconnecter",
|
"learnerVariantDashboard.menu.signOut.label": "Se déconnecter",
|
||||||
"learnerVariantDashboard.course": "Cours",
|
"learnerVariantDashboard.course": "Cours",
|
||||||
"learnerVariantDashboard.program": "Programmes",
|
"learnerVariantDashboard.program": "Programmes",
|
||||||
@@ -40,6 +27,8 @@
|
|||||||
"learnerVariantDashboard.logoAltText": "Tableau de bord edX, Inc.",
|
"learnerVariantDashboard.logoAltText": "Tableau de bord edX, Inc.",
|
||||||
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
|
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
|
||||||
"learnerVariantDashboard.collapseMenuClosedAltText": "Fermer",
|
"learnerVariantDashboard.collapseMenuClosedAltText": "Fermer",
|
||||||
|
"leanerDashboard.menu.career.label": "Career",
|
||||||
|
"header.menu.new.label": "New",
|
||||||
"MasqueradeBar.ViewAs": "Vue de :",
|
"MasqueradeBar.ViewAs": "Vue de :",
|
||||||
"MasqueradeBar.ViewingAs": "Voir comme : ",
|
"MasqueradeBar.ViewingAs": "Voir comme : ",
|
||||||
"MasqueradeBar.SubmitButton": "Envoyez",
|
"MasqueradeBar.SubmitButton": "Envoyez",
|
||||||
@@ -48,6 +37,20 @@
|
|||||||
"MasqueradeBar.UnknownError": "Une erreur d'origine inconnue s'est produite.",
|
"MasqueradeBar.UnknownError": "Une erreur d'origine inconnue s'est produite.",
|
||||||
"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}",
|
"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.recommendationsHeading": "Des recommandations pour vous",
|
||||||
"RecommendationsPanel.popularCoursesHeading": "Cours populaires",
|
"RecommendationsPanel.popularCoursesHeading": "Cours populaires",
|
||||||
"RecommendationsPanel.exploreCoursesButton": "Explorer les cours"
|
"RecommendationsPanel.exploreCoursesButton": "Explorer les cours"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dashboard.mycourses": "Mes cours",
|
"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.exploreCoursesPrompt": "Explorez nos cours pour les ajouter à votre tableau de bord.",
|
||||||
"Dashboard.NoCoursesView.exploreCoursesButton": "Explorer les cours",
|
"Dashboard.NoCoursesView.exploreCoursesButton": "Explorer les cours",
|
||||||
"Dashboard.NoCoursesView.bannerAlt": "Aucune bannière de cours",
|
"Dashboard.NoCoursesView.bannerAlt": "Aucune bannière de cours",
|
||||||
@@ -9,24 +9,11 @@
|
|||||||
"leanerDashboard.enterpriseDialogDismissButton": "Rejeter",
|
"leanerDashboard.enterpriseDialogDismissButton": "Rejeter",
|
||||||
"leanerDashboard.enterpriseDialogConfirmButton": "Aller au tableau de bord",
|
"leanerDashboard.enterpriseDialogConfirmButton": "Aller au tableau de bord",
|
||||||
"leanerDashboard.confirmEmailBanner": "Confirmer maintenant",
|
"leanerDashboard.confirmEmailBanner": "Confirmer maintenant",
|
||||||
"leanerDashboard.confirmEmailTextReminderBanner": "N'oubliez pas de confirmer votre courriel afin de pouvoir continuer à apprendre sur edX ! {confirmNowButton}.",
|
"leanerDashboard.confirmEmailTextReminderBanner": "N'oubliez pas de confirmer votre courriel afin de pouvoir continuer à apprendre sur EDUlib! {confirmNowButton}.",
|
||||||
"leanerDashboard.verifiedConfirmEmailButton": "J'ai confirmé mon courriel",
|
"leanerDashboard.verifiedConfirmEmailButton": "J'ai confirmé mon courriel",
|
||||||
"leanerDashboard.confirmEmailModalHeader": "Confirmer votre 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.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.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.menu.dashboard.label": "Tableau de bord",
|
||||||
"learnerVariantDashboard.help.label": "Aide",
|
"learnerVariantDashboard.help.label": "Aide",
|
||||||
"learnerVariantDashboard.menu.profile.label": "Profil",
|
"learnerVariantDashboard.menu.profile.label": "Profil",
|
||||||
@@ -37,17 +24,33 @@
|
|||||||
"learnerVariantDashboard.course": "Cours",
|
"learnerVariantDashboard.course": "Cours",
|
||||||
"learnerVariantDashboard.program": "Programmes",
|
"learnerVariantDashboard.program": "Programmes",
|
||||||
"learnerVariantDashboard.discoverNew": "Découvrir les nouveautés",
|
"learnerVariantDashboard.discoverNew": "Découvrir les nouveautés",
|
||||||
"learnerVariantDashboard.logoAltText": "Tableau de bord edX, Inc.",
|
"learnerVariantDashboard.logoAltText": "Tableau de bord EDUlib, Inc.",
|
||||||
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
|
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
|
||||||
"learnerVariantDashboard.collapseMenuClosedAltText": "Fermer",
|
"learnerVariantDashboard.collapseMenuClosedAltText": "Fermer",
|
||||||
|
"leanerDashboard.menu.career.label": "Carrière",
|
||||||
|
"header.menu.new.label": "Nouveau",
|
||||||
"MasqueradeBar.ViewAs": "Consulter comme :",
|
"MasqueradeBar.ViewAs": "Consulter comme :",
|
||||||
"MasqueradeBar.ViewingAs": "Affichage en tant que :",
|
"MasqueradeBar.ViewingAs": "Affichage en tant que :",
|
||||||
"MasqueradeBar.SubmitButton": "Soumettre",
|
"MasqueradeBar.SubmitButton": "Soumettre",
|
||||||
"MasqueradeBar.StudentNameInput": "Nom d'utilisateur ou courriel",
|
"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.NoStudentFound": "Aucun étudiant avec ce nom d'utilisateur ou cette adresse courriel n'a pu être trouvé",
|
||||||
"MasqueradeBar.UnknownError": "Une erreur inconnue est survenue",
|
"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}",
|
"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.recommendationsHeading": "Des recommandations pour vous",
|
||||||
"RecommendationsPanel.popularCoursesHeading": "Cours populaires",
|
"RecommendationsPanel.popularCoursesHeading": "Cours populaires",
|
||||||
"RecommendationsPanel.exploreCoursesButton": "Explorer les cours"
|
"RecommendationsPanel.exploreCoursesButton": "Explorer les cours"
|
||||||
|
|||||||
@@ -14,25 +14,12 @@
|
|||||||
"leanerDashboard.confirmEmailModalHeader": "Confirm your email",
|
"leanerDashboard.confirmEmailModalHeader": "Confirm your email",
|
||||||
"leanerDashboard.confirmEmailModalBody": "We've sent you an email to verify your acccount. Please check your inbox and click on the big red button to confirm and keep learning.",
|
"leanerDashboard.confirmEmailModalBody": "We've sent you an email to verify your acccount. Please check your inbox and click on the big red button to confirm and keep learning.",
|
||||||
"leanerDashboard.confirmEmailImageAlt": "confirm email background",
|
"leanerDashboard.confirmEmailImageAlt": "confirm email background",
|
||||||
"leanerDashboard.menu.dashboard.label": "Painel de controle",
|
|
||||||
"leanerDashboard.help.label": "Ajuda",
|
|
||||||
"leanerDashboard.menu.profile.label": "Perfil",
|
|
||||||
"leanerDashboard.menu.viewPrograms.label": "View Programs",
|
|
||||||
"leanerDashboard.menu.account.label": "Conta",
|
|
||||||
"leanerDashboard.menu.orderHistory.label": "Histórico de pedidos",
|
|
||||||
"leanerDashboard.menu.signOut.label": "Sair",
|
|
||||||
"greeting.morning": "Good Morning!",
|
|
||||||
"greeting.afternoon": "Good Afternoon!",
|
|
||||||
"greeting.evening": "Good Evening!",
|
|
||||||
"leanerDashboard.switchToProgram": "Switch to Programs",
|
|
||||||
"leanerDashboard.exploreCourses": "Explorar cursos",
|
|
||||||
"leanerDashboard.courseSearchAlt": "Course search",
|
|
||||||
"learnerVariantDashboard.menu.dashboard.label": "Painel de controle",
|
"learnerVariantDashboard.menu.dashboard.label": "Painel de controle",
|
||||||
"learnerVariantDashboard.help.label": "Ajuda",
|
"learnerVariantDashboard.help.label": "Ajuda",
|
||||||
"learnerVariantDashboard.menu.profile.label": "Perfil",
|
"learnerVariantDashboard.menu.profile.label": "Perfil",
|
||||||
"learnerVariantDashboard.menu.viewPrograms.label": "View Programs",
|
"learnerVariantDashboard.menu.viewPrograms.label": "View Programs",
|
||||||
"learnerVariantDashboard.menu.account.label": "Conta",
|
"learnerVariantDashboard.menu.account.label": "Conta",
|
||||||
"learnerVariantDashboard.menu.orderHistory.label": "Histórico de pedidos",
|
"learnerVariantDashboard.menu.orderHistory.label": "Order History",
|
||||||
"learnerVariantDashboard.menu.signOut.label": "Sair",
|
"learnerVariantDashboard.menu.signOut.label": "Sair",
|
||||||
"learnerVariantDashboard.course": "Cursos",
|
"learnerVariantDashboard.course": "Cursos",
|
||||||
"learnerVariantDashboard.program": "Programas",
|
"learnerVariantDashboard.program": "Programas",
|
||||||
@@ -40,6 +27,8 @@
|
|||||||
"learnerVariantDashboard.logoAltText": "edX, Inc. Dashboard",
|
"learnerVariantDashboard.logoAltText": "edX, Inc. Dashboard",
|
||||||
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
|
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
|
||||||
"learnerVariantDashboard.collapseMenuClosedAltText": "Fechar",
|
"learnerVariantDashboard.collapseMenuClosedAltText": "Fechar",
|
||||||
|
"leanerDashboard.menu.career.label": "Career",
|
||||||
|
"header.menu.new.label": "New",
|
||||||
"MasqueradeBar.ViewAs": "Ver como: ",
|
"MasqueradeBar.ViewAs": "Ver como: ",
|
||||||
"MasqueradeBar.ViewingAs": "Viewing as: ",
|
"MasqueradeBar.ViewingAs": "Viewing as: ",
|
||||||
"MasqueradeBar.SubmitButton": "Enviar",
|
"MasqueradeBar.SubmitButton": "Enviar",
|
||||||
@@ -48,6 +37,20 @@
|
|||||||
"MasqueradeBar.UnknownError": "An unknown error occurred",
|
"MasqueradeBar.UnknownError": "An unknown error occurred",
|
||||||
"WidgetSidebar.lookingForChallengePrompt": "Looking for a new challenge?",
|
"WidgetSidebar.lookingForChallengePrompt": "Looking for a new challenge?",
|
||||||
"WidgetSidebar.findCoursesButton": "Find a course {arrow}",
|
"WidgetSidebar.findCoursesButton": "Find a course {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": "Recommendations for you",
|
"RecommendationsPanel.recommendationsHeading": "Recommendations for you",
|
||||||
"RecommendationsPanel.popularCoursesHeading": "Popular courses",
|
"RecommendationsPanel.popularCoursesHeading": "Popular courses",
|
||||||
"RecommendationsPanel.exploreCoursesButton": "Explorar cursos"
|
"RecommendationsPanel.exploreCoursesButton": "Explorar cursos"
|
||||||
|
|||||||
@@ -14,19 +14,6 @@
|
|||||||
"leanerDashboard.confirmEmailModalHeader": "Confirm your email",
|
"leanerDashboard.confirmEmailModalHeader": "Confirm your email",
|
||||||
"leanerDashboard.confirmEmailModalBody": "We've sent you an email to verify your acccount. Please check your inbox and click on the big red button to confirm and keep learning.",
|
"leanerDashboard.confirmEmailModalBody": "We've sent you an email to verify your acccount. Please check your inbox and click on the big red button to confirm and keep learning.",
|
||||||
"leanerDashboard.confirmEmailImageAlt": "confirm email background",
|
"leanerDashboard.confirmEmailImageAlt": "confirm email background",
|
||||||
"leanerDashboard.menu.dashboard.label": "Dashboard",
|
|
||||||
"leanerDashboard.help.label": "Help",
|
|
||||||
"leanerDashboard.menu.profile.label": "Profile",
|
|
||||||
"leanerDashboard.menu.viewPrograms.label": "View Programs",
|
|
||||||
"leanerDashboard.menu.account.label": "Account",
|
|
||||||
"leanerDashboard.menu.orderHistory.label": "Order History",
|
|
||||||
"leanerDashboard.menu.signOut.label": "Sign Out",
|
|
||||||
"greeting.morning": "Good Morning!",
|
|
||||||
"greeting.afternoon": "Good Afternoon!",
|
|
||||||
"greeting.evening": "Good Evening!",
|
|
||||||
"leanerDashboard.switchToProgram": "Switch to Programs",
|
|
||||||
"leanerDashboard.exploreCourses": "Explore courses",
|
|
||||||
"leanerDashboard.courseSearchAlt": "Course search",
|
|
||||||
"learnerVariantDashboard.menu.dashboard.label": "Dashboard",
|
"learnerVariantDashboard.menu.dashboard.label": "Dashboard",
|
||||||
"learnerVariantDashboard.help.label": "Help",
|
"learnerVariantDashboard.help.label": "Help",
|
||||||
"learnerVariantDashboard.menu.profile.label": "Profile",
|
"learnerVariantDashboard.menu.profile.label": "Profile",
|
||||||
@@ -40,6 +27,8 @@
|
|||||||
"learnerVariantDashboard.logoAltText": "edX, Inc. Dashboard",
|
"learnerVariantDashboard.logoAltText": "edX, Inc. Dashboard",
|
||||||
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
|
"learnerVariantDashboard.collapseMenuOpenAltText": "Menu",
|
||||||
"learnerVariantDashboard.collapseMenuClosedAltText": "Close",
|
"learnerVariantDashboard.collapseMenuClosedAltText": "Close",
|
||||||
|
"leanerDashboard.menu.career.label": "Career",
|
||||||
|
"header.menu.new.label": "New",
|
||||||
"MasqueradeBar.ViewAs": "View as: ",
|
"MasqueradeBar.ViewAs": "View as: ",
|
||||||
"MasqueradeBar.ViewingAs": "Viewing as: ",
|
"MasqueradeBar.ViewingAs": "Viewing as: ",
|
||||||
"MasqueradeBar.SubmitButton": "Submit",
|
"MasqueradeBar.SubmitButton": "Submit",
|
||||||
@@ -48,6 +37,20 @@
|
|||||||
"MasqueradeBar.UnknownError": "An unknown error occurred",
|
"MasqueradeBar.UnknownError": "An unknown error occurred",
|
||||||
"WidgetSidebar.lookingForChallengePrompt": "Looking for a new challenge?",
|
"WidgetSidebar.lookingForChallengePrompt": "Looking for a new challenge?",
|
||||||
"WidgetSidebar.findCoursesButton": "Find a course {arrow}",
|
"WidgetSidebar.findCoursesButton": "Find a course {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": "Recommendations for you",
|
"RecommendationsPanel.recommendationsHeading": "Recommendations for you",
|
||||||
"RecommendationsPanel.popularCoursesHeading": "Popular courses",
|
"RecommendationsPanel.popularCoursesHeading": "Popular courses",
|
||||||
"RecommendationsPanel.exploreCoursesButton": "Explore courses"
|
"RecommendationsPanel.exploreCoursesButton": "Explore courses"
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import 'regenerator-runtime/runtime';
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Switch, Redirect } from 'react-router-dom';
|
import {
|
||||||
|
Route, Navigate, Routes,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AppProvider,
|
AppProvider,
|
||||||
ErrorPage,
|
ErrorPage,
|
||||||
PageRoute,
|
PageWrap,
|
||||||
} from '@edx/frontend-platform/react';
|
} from '@edx/frontend-platform/react';
|
||||||
import store from 'data/store';
|
import store from 'data/store';
|
||||||
import {
|
import {
|
||||||
@@ -31,12 +33,10 @@ subscribe(APP_READY, () => {
|
|||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
<NoticesWrapper>
|
<NoticesWrapper>
|
||||||
<Switch>
|
<Routes>
|
||||||
<PageRoute path="/">
|
<Route path="/" element={<PageWrap><App /></PageWrap>} />
|
||||||
<App />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</PageRoute>
|
</Routes>
|
||||||
<Redirect to="/" />
|
|
||||||
</Switch>
|
|
||||||
</NoticesWrapper>
|
</NoticesWrapper>
|
||||||
</AppProvider>,
|
</AppProvider>,
|
||||||
document.getElementById('root'),
|
document.getElementById('root'),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import '@testing-library/jest-dom';
|
|||||||
import '@testing-library/jest-dom/extend-expect';
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
|
||||||
import Enzyme from 'enzyme';
|
import Enzyme from 'enzyme';
|
||||||
import Adapter from 'enzyme-adapter-react-16';
|
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
||||||
|
|
||||||
Enzyme.configure({ adapter: new Adapter() });
|
Enzyme.configure({ adapter: new Adapter() });
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ jest.mock('react', () => ({
|
|||||||
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
|
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
|
||||||
useMemo: jest.fn((cb, prereqs) => cb(prereqs)),
|
useMemo: jest.fn((cb, prereqs) => cb(prereqs)),
|
||||||
useContext: jest.fn(context => context),
|
useContext: jest.fn(context => context),
|
||||||
|
useState: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('reselect', () => ({
|
jest.mock('reselect', () => ({
|
||||||
@@ -144,6 +145,8 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
|||||||
Sheet: 'Sheet',
|
Sheet: 'Sheet',
|
||||||
StatefulButton: 'StatefulButton',
|
StatefulButton: 'StatefulButton',
|
||||||
TextFilter: 'TextFilter',
|
TextFilter: 'TextFilter',
|
||||||
|
Truncate: 'Truncate',
|
||||||
|
Skeleton: 'Skeleton',
|
||||||
Spinner: 'Spinner',
|
Spinner: 'Spinner',
|
||||||
PageBanner: 'PageBanner',
|
PageBanner: 'PageBanner',
|
||||||
Pagination: 'Pagination',
|
Pagination: 'Pagination',
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ jest.unmock('react-redux');
|
|||||||
jest.unmock('reselect');
|
jest.unmock('reselect');
|
||||||
jest.unmock('hooks');
|
jest.unmock('hooks');
|
||||||
|
|
||||||
jest.mock('containers/WidgetContainers/LoadedSidebar', () => 'loaded-widget-sidebar');
|
jest.mock('containers/WidgetContainers/LoadedSidebar', () => jest.fn(() => 'loaded-widget-sidebar'));
|
||||||
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => 'no-courses-widget-sidebar');
|
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => jest.fn(() => 'no-courses-widget-sidebar'));
|
||||||
|
jest.mock('containers/WidgetContainers/WidgetFooter', () => 'product-recommendations-footer');
|
||||||
jest.mock('components/NoticesWrapper', () => 'notices-wrapper');
|
jest.mock('components/NoticesWrapper', () => 'notices-wrapper');
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform', () => ({
|
jest.mock('@edx/frontend-platform', () => ({
|
||||||
@@ -60,6 +61,10 @@ jest.mock('@edx/frontend-platform/auth', () => ({
|
|||||||
getLoginRedirectUrl: jest.fn(),
|
getLoginRedirectUrl: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('ExperimentContext', () => ({
|
||||||
|
ExperimentProvider: 'div'
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('@edx/frontend-enterprise-hotjar', () => ({
|
jest.mock('@edx/frontend-enterprise-hotjar', () => ({
|
||||||
initializeHotjar: jest.fn(),
|
initializeHotjar: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ProductRecommendations matches snapshot 1`] = `
|
||||||
|
<LoadedView
|
||||||
|
crossProductCourses={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"courseRunKey": "course-v1:Test+Course+2022T2",
|
||||||
|
"courseType": "executive-education-2u",
|
||||||
|
"image": Object {
|
||||||
|
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
|
||||||
|
},
|
||||||
|
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
|
||||||
|
"owners": Array [
|
||||||
|
Object {
|
||||||
|
"key": "HarvardX",
|
||||||
|
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
|
||||||
|
"name": "Harvard University",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Introduction to Computer Science",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"courseRunKey": "course-v1:Test+Course+2022T2",
|
||||||
|
"courseType": "bootcamp-2u",
|
||||||
|
"image": Object {
|
||||||
|
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
|
||||||
|
},
|
||||||
|
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
|
||||||
|
"owners": Array [
|
||||||
|
Object {
|
||||||
|
"key": "HarvardX",
|
||||||
|
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
|
||||||
|
"name": "Harvard University",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Introduction to Computer Science",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
openCourses={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"courseRunKey": "course-v1:Test+Course+2022T2",
|
||||||
|
"courseType": "verified-audit",
|
||||||
|
"image": Object {
|
||||||
|
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
|
||||||
|
},
|
||||||
|
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
|
||||||
|
"owners": Array [
|
||||||
|
Object {
|
||||||
|
"key": "HarvardX",
|
||||||
|
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
|
||||||
|
"name": "Harvard University",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Introduction to Computer Science",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"courseRunKey": "course-v1:Test+Course+2022T2",
|
||||||
|
"courseType": "audit",
|
||||||
|
"image": Object {
|
||||||
|
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
|
||||||
|
},
|
||||||
|
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
|
||||||
|
"owners": Array [
|
||||||
|
Object {
|
||||||
|
"key": "HarvardX",
|
||||||
|
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
|
||||||
|
"name": "Harvard University",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Introduction to Computer Science",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"courseRunKey": "course-v1:Test+Course+2022T2",
|
||||||
|
"courseType": "verified",
|
||||||
|
"image": Object {
|
||||||
|
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
|
||||||
|
},
|
||||||
|
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
|
||||||
|
"owners": Array [
|
||||||
|
Object {
|
||||||
|
"key": "HarvardX",
|
||||||
|
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
|
||||||
|
"name": "Harvard University",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Introduction to Computer Science",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"courseRunKey": "course-v1:Test+Course+2022T2",
|
||||||
|
"courseType": "course",
|
||||||
|
"image": Object {
|
||||||
|
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
|
||||||
|
},
|
||||||
|
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
|
||||||
|
"owners": Array [
|
||||||
|
Object {
|
||||||
|
"key": "HarvardX",
|
||||||
|
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
|
||||||
|
"name": "Harvard University",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Introduction to Computer Science",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
19
src/widgets/ProductRecommendations/api.js
Normal file
19
src/widgets/ProductRecommendations/api.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { get, stringifyUrl } from 'data/services/lms/utils';
|
||||||
|
import urls from 'data/services/lms/urls';
|
||||||
|
|
||||||
|
export const crossProductAndAmplitudeRecommendationsUrl = (courseId) => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/cross_product/${courseId}/`;
|
||||||
|
export const amplitudeRecommendationsUrl = () => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/amplitude/v2/`;
|
||||||
|
export const recommendationsContextUrl = () => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/recommendations_context/`;
|
||||||
|
|
||||||
|
const fetchRecommendationsContext = () => get(stringifyUrl(recommendationsContextUrl()));
|
||||||
|
|
||||||
|
const fetchCrossProductRecommendations = (courseId) => (
|
||||||
|
get(stringifyUrl(crossProductAndAmplitudeRecommendationsUrl(courseId)))
|
||||||
|
);
|
||||||
|
const fetchAmplitudeRecommendations = () => get(stringifyUrl(amplitudeRecommendationsUrl()));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchCrossProductRecommendations,
|
||||||
|
fetchAmplitudeRecommendations,
|
||||||
|
fetchRecommendationsContext,
|
||||||
|
};
|
||||||
34
src/widgets/ProductRecommendations/api.test.js
Normal file
34
src/widgets/ProductRecommendations/api.test.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { get, stringifyUrl } from 'data/services/lms/utils';
|
||||||
|
|
||||||
|
import api, { crossProductAndAmplitudeRecommendationsUrl, amplitudeRecommendationsUrl, recommendationsContextUrl } from './api';
|
||||||
|
|
||||||
|
jest.mock('data/services/lms/utils', () => ({
|
||||||
|
stringifyUrl: (...args) => ({ stringifyUrl: args }),
|
||||||
|
get: (...args) => ({ get: args }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('productRecommendationCourses api', () => {
|
||||||
|
describe('fetchCrossProductRecommendations', () => {
|
||||||
|
it('calls get with the correct recommendation courses URL', () => {
|
||||||
|
expect(api.fetchCrossProductRecommendations('CourseRunKey')).toEqual(
|
||||||
|
get(stringifyUrl(crossProductAndAmplitudeRecommendationsUrl('CourseRunKey'))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchAmplitudeRecommendations', () => {
|
||||||
|
it('calls get with the correct recommendation courses URL', () => {
|
||||||
|
expect(api.fetchAmplitudeRecommendations()).toEqual(
|
||||||
|
get(stringifyUrl(amplitudeRecommendationsUrl())),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchRecommendationsContext', () => {
|
||||||
|
it('calls get with the correct recommendation courses URL', () => {
|
||||||
|
expect(api.fetchRecommendationsContext()).toEqual(
|
||||||
|
get(stringifyUrl(recommendationsContextUrl())),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
46
src/widgets/ProductRecommendations/components/LoadedView.jsx
Normal file
46
src/widgets/ProductRecommendations/components/LoadedView.jsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Container } from '@edx/paragon';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import messages from '../messages';
|
||||||
|
import { courseShape, courseTypeToProductTypeMap } from '../utils';
|
||||||
|
import ProductCardContainer from './ProductCardContainer';
|
||||||
|
|
||||||
|
const LoadedView = ({ crossProductCourses, openCourses }) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const includesCrossProductTypes = crossProductCourses.length === 2;
|
||||||
|
|
||||||
|
const finalProductList = useMemo(() => {
|
||||||
|
if (includesCrossProductTypes) {
|
||||||
|
const openCourseList = openCourses.slice(0, 2);
|
||||||
|
return crossProductCourses.concat(openCourseList);
|
||||||
|
}
|
||||||
|
return openCourses;
|
||||||
|
}, [crossProductCourses, openCourses, includesCrossProductTypes]);
|
||||||
|
|
||||||
|
const courseTypes = [...new Set(finalProductList.map((item) => courseTypeToProductTypeMap[item.courseType]))];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
size="lg"
|
||||||
|
className="recommendations-container pt-sm-5 pt-4.5 pb-2 pb-sm-4.5"
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
{formatMessage(messages.recommendationsHeading)}
|
||||||
|
</h2>
|
||||||
|
<ProductCardContainer finalProductList={finalProductList} courseTypes={courseTypes} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LoadedView.propTypes = {
|
||||||
|
crossProductCourses: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape(courseShape),
|
||||||
|
).isRequired,
|
||||||
|
openCourses: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape(courseShape),
|
||||||
|
).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadedView;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { mockCrossProductCourses, mockOpenCourses } from '../testData';
|
||||||
|
import LoadedView from './LoadedView';
|
||||||
|
|
||||||
|
describe('ProductRecommendations LoadedView', () => {
|
||||||
|
it('matches snapshot', () => {
|
||||||
|
expect(
|
||||||
|
shallow(
|
||||||
|
<LoadedView
|
||||||
|
crossProductCourses={mockCrossProductCourses}
|
||||||
|
openCourses={mockOpenCourses}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
describe('with less than 2 cross product courses', () => {
|
||||||
|
it('passes in one course type and 4 open courses to the ProductCardContainer props', () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<LoadedView
|
||||||
|
crossProductCourses={[mockCrossProductCourses[0]]}
|
||||||
|
openCourses={mockOpenCourses}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const productCardContainerProps = wrapper.find('ProductCardContainer').props();
|
||||||
|
|
||||||
|
expect(productCardContainerProps.courseTypes.length).toEqual(1);
|
||||||
|
expect(productCardContainerProps.courseTypes[0]).toEqual('Course');
|
||||||
|
expect(productCardContainerProps.finalProductList).toEqual(mockOpenCourses);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Skeleton } from '@edx/paragon';
|
||||||
|
|
||||||
|
export const LoadingView = () => (
|
||||||
|
<Skeleton height={100} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LoadingView;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import LoadingView from './LoadingView';
|
||||||
|
|
||||||
|
describe('ProductRecommendations LoadingView', () => {
|
||||||
|
it('matches snapshot', () => {
|
||||||
|
expect(shallow(<LoadingView />)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Card,
|
||||||
|
Truncate,
|
||||||
|
Hyperlink,
|
||||||
|
} from '@edx/paragon';
|
||||||
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
import { trackProductCardClicked, trackCourseCardClicked } from '../optimizelyExperiment';
|
||||||
|
import { productCardClicked, discoveryCardClicked } from '../track';
|
||||||
|
import { bootCamp, executiveEducation } from '../constants';
|
||||||
|
|
||||||
|
const ProductCard = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
headerImage,
|
||||||
|
courseRunKey,
|
||||||
|
schoolLogo,
|
||||||
|
courseType,
|
||||||
|
url,
|
||||||
|
}) => {
|
||||||
|
const handleClick = (type, link) => {
|
||||||
|
const crossProductTypes = [executiveEducation, bootCamp];
|
||||||
|
const userId = getAuthenticatedUser().userId.toString();
|
||||||
|
|
||||||
|
if (crossProductTypes.includes(type)) {
|
||||||
|
trackProductCardClicked(userId);
|
||||||
|
productCardClicked(courseRunKey, title, type, link);
|
||||||
|
} else {
|
||||||
|
trackCourseCardClicked(userId);
|
||||||
|
discoveryCardClicked(courseRunKey, title, link);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRedirectUrl = (link) => {
|
||||||
|
const urlObj = new URL(link);
|
||||||
|
const hasQueryStringParameters = urlObj.search !== '';
|
||||||
|
|
||||||
|
if (hasQueryStringParameters) {
|
||||||
|
return `${link}&linked_from=recommender`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${link}?linked_from=recommender`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectUrl = getRedirectUrl(url);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="base-card d-flex text-decoration-none"
|
||||||
|
as={Hyperlink}
|
||||||
|
destination={redirectUrl}
|
||||||
|
onClick={() => {
|
||||||
|
handleClick(courseType, redirectUrl);
|
||||||
|
}}
|
||||||
|
isClickable
|
||||||
|
>
|
||||||
|
<Card.ImageCap
|
||||||
|
src={headerImage}
|
||||||
|
srcAlt={`header image for ${title}`}
|
||||||
|
logoSrc={schoolLogo}
|
||||||
|
logoAlt={`logo for ${subtitle}`}
|
||||||
|
/>
|
||||||
|
<Card.Header
|
||||||
|
className="mt-2"
|
||||||
|
title={(
|
||||||
|
<Truncate lines={3} ellipsis="…" className="product-card-title font-weight-bold">
|
||||||
|
{title}
|
||||||
|
</Truncate>
|
||||||
|
)}
|
||||||
|
subtitle={(
|
||||||
|
<Truncate lines={1} className="product-card-subtitle font-weight-normal">
|
||||||
|
{subtitle}
|
||||||
|
</Truncate>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Card.Section>
|
||||||
|
<div className="product-badge position-absolute">
|
||||||
|
<Badge className="bg-light-500 text-dark-500">{courseType}</Badge>
|
||||||
|
</div>
|
||||||
|
</Card.Section>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProductCard.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
subtitle: PropTypes.string.isRequired,
|
||||||
|
headerImage: PropTypes.string.isRequired,
|
||||||
|
courseRunKey: PropTypes.string.isRequired,
|
||||||
|
schoolLogo: PropTypes.string.isRequired,
|
||||||
|
courseType: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductCard;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { mockCrossProductCourses, mockOpenCourses, mockFallbackOpenCourse } from '../testData';
|
||||||
|
import { trackProductCardClicked, trackCourseCardClicked } from '../optimizelyExperiment';
|
||||||
|
import { productCardClicked, discoveryCardClicked } from '../track';
|
||||||
|
import ProductCard from './ProductCard';
|
||||||
|
import { courseTypeToProductTypeMap } from '../utils';
|
||||||
|
|
||||||
|
jest.mock('../optimizelyExperiment', () => ({
|
||||||
|
trackProductCardClicked: jest.fn(),
|
||||||
|
trackCourseCardClicked: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../track', () => ({
|
||||||
|
productCardClicked: jest.fn(),
|
||||||
|
discoveryCardClicked: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
|
getAuthenticatedUser: jest.fn(() => ({ userId: '1' })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ProductRecommendations ProductCard', () => {
|
||||||
|
const getProps = (course) => {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
owners: [{ name: subtitle }],
|
||||||
|
image: { src: headerImage },
|
||||||
|
owners: [{ logoImageUrl: schoolLogo }],
|
||||||
|
} = course;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
headerImage,
|
||||||
|
schoolLogo,
|
||||||
|
courseType: courseTypeToProductTypeMap[course.courseType],
|
||||||
|
courseRunKey: course.courseRunKey,
|
||||||
|
url: course.marketingUrl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const crossProductProps = getProps(mockCrossProductCourses[0]);
|
||||||
|
const openCourseProps = getProps(mockOpenCourses[0]);
|
||||||
|
const fallbackOpenCourseProps = getProps(mockFallbackOpenCourse[0]);
|
||||||
|
|
||||||
|
it('matches snapshot', () => {
|
||||||
|
expect(shallow(<ProductCard {...crossProductProps} />)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has the query string parameter attached to a fallback recommendations url', () => {
|
||||||
|
const wrapper = shallow(<ProductCard {...fallbackOpenCourseProps} />);
|
||||||
|
const cardUrl = wrapper.find('Card').props().destination;
|
||||||
|
|
||||||
|
expect(cardUrl).toEqual('https://www.edx.org/course/some-course?linked_from=recommender');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('send outs experiment events related to open courses when clicked', () => {
|
||||||
|
const wrapper = shallow(<ProductCard {...openCourseProps} />);
|
||||||
|
const { courseRunKey, title, url } = openCourseProps;
|
||||||
|
|
||||||
|
wrapper.simulate('click');
|
||||||
|
|
||||||
|
expect(trackCourseCardClicked).toHaveBeenCalledWith('1');
|
||||||
|
expect(discoveryCardClicked).toHaveBeenCalledWith(courseRunKey, title, `${url}&linked_from=recommender`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('send outs experiment events related to cross product courses when clicked', () => {
|
||||||
|
const wrapper = shallow(<ProductCard {...crossProductProps} />);
|
||||||
|
const {
|
||||||
|
courseRunKey,
|
||||||
|
title,
|
||||||
|
courseType,
|
||||||
|
url,
|
||||||
|
} = crossProductProps;
|
||||||
|
|
||||||
|
wrapper.simulate('click');
|
||||||
|
|
||||||
|
expect(trackProductCardClicked).toHaveBeenCalledWith('1');
|
||||||
|
expect(productCardClicked).toHaveBeenCalledWith(courseRunKey, title, courseType, `${url}&linked_from=recommender`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { courseShape, courseTypeToProductTypeMap } from '../utils';
|
||||||
|
import { course } from '../constants';
|
||||||
|
import ProductCard from './ProductCard';
|
||||||
|
import ProductCardHeader from './ProductCardHeader';
|
||||||
|
|
||||||
|
const ProductCardContainer = ({ finalProductList, courseTypes }) => (
|
||||||
|
<div className="product-card-container d-flex">
|
||||||
|
{finalProductList
|
||||||
|
&& courseTypes.map((type) => (
|
||||||
|
<div key={type}>
|
||||||
|
<ProductCardHeader courseType={type} />
|
||||||
|
<div
|
||||||
|
className={classNames('d-flex', {
|
||||||
|
'course-subcontainer': type === course,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{finalProductList
|
||||||
|
.filter((courseObj) => courseTypeToProductTypeMap[courseObj.courseType] === type)
|
||||||
|
.map((item) => (
|
||||||
|
<ProductCard
|
||||||
|
key={item.title}
|
||||||
|
url={item.marketingUrl}
|
||||||
|
title={item.title}
|
||||||
|
subtitle={item.owners[0].name}
|
||||||
|
headerImage={item.image.src}
|
||||||
|
courseRunKey={item.courseRunKey}
|
||||||
|
schoolLogo={item.owners[0].logoImageUrl}
|
||||||
|
courseType={type}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
ProductCardContainer.propTypes = {
|
||||||
|
finalProductList: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape(courseShape),
|
||||||
|
).isRequired,
|
||||||
|
courseTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductCardContainer;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import { mockCrossProductCourses, mockOpenCourses } from '../testData';
|
||||||
|
import ProductCardContainer from './ProductCardContainer';
|
||||||
|
import { executiveEducation, bootCamp, course } from '../constants';
|
||||||
|
|
||||||
|
describe('ProductRecommendations ProductCardContainer', () => {
|
||||||
|
const props = {
|
||||||
|
finalProductList: [...mockCrossProductCourses, ...mockOpenCourses],
|
||||||
|
courseTypes: [executiveEducation, bootCamp, course],
|
||||||
|
};
|
||||||
|
|
||||||
|
it('matches snapshot', () => {
|
||||||
|
expect(shallow(<ProductCardContainer {...props} />)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with finalCourseList containing cross product and open courses', () => {
|
||||||
|
it('renders 3 ProductCardHeaders with the 3 different course types', () => {
|
||||||
|
const wrapper = shallow(<ProductCardContainer {...props} />);
|
||||||
|
const productCardHeaders = wrapper.find('ProductCardHeader');
|
||||||
|
|
||||||
|
expect(productCardHeaders.length).toEqual(3);
|
||||||
|
productCardHeaders.forEach((header, index) => {
|
||||||
|
expect(header.props().courseType).toEqual(props.courseTypes[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with finalCourseList containing only open courses', () => {
|
||||||
|
it('renders 1 ProductHeader with the one course type', () => {
|
||||||
|
const openCoursesProps = {
|
||||||
|
finalProductList: [...mockOpenCourses, ...mockOpenCourses],
|
||||||
|
courseTypes: ['Course'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = shallow(<ProductCardContainer {...openCoursesProps} />);
|
||||||
|
const productCardHeaders = wrapper.find('ProductCardHeader');
|
||||||
|
|
||||||
|
expect(productCardHeaders.length).toEqual(1);
|
||||||
|
expect(productCardHeaders.at(0).props().courseType).toEqual(openCoursesProps.courseTypes[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
import { Icon, Hyperlink } from '@edx/paragon';
|
||||||
|
import { ChevronRight } from '@edx/paragon/icons';
|
||||||
|
import { trackProductHeaderClicked } from '../optimizelyExperiment';
|
||||||
|
import { recommendationsHeaderClicked } from '../track';
|
||||||
|
import { executiveEducation, bootCamp } from '../constants';
|
||||||
|
import messages from '../messages';
|
||||||
|
|
||||||
|
const ProductCardHeader = ({ courseType }) => {
|
||||||
|
const getProductTypeDetail = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case executiveEducation:
|
||||||
|
return {
|
||||||
|
heading: messages.executiveEducationHeading,
|
||||||
|
description: messages.executiveEducationDescription,
|
||||||
|
url: '/executive-education?linked_from=recommender',
|
||||||
|
};
|
||||||
|
case bootCamp:
|
||||||
|
return {
|
||||||
|
heading: messages.bootcampHeading,
|
||||||
|
description: messages.bootcampDescription,
|
||||||
|
url: '/boot-camps?linked_from=recommender',
|
||||||
|
};
|
||||||
|
default: {
|
||||||
|
return {
|
||||||
|
heading: messages.courseHeading,
|
||||||
|
description: messages.courseDescription,
|
||||||
|
url: '/search?tab=course',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (type, url) => {
|
||||||
|
const userId = getAuthenticatedUser().userId.toString();
|
||||||
|
|
||||||
|
trackProductHeaderClicked(userId);
|
||||||
|
recommendationsHeaderClicked(type, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const productTypeDetail = getProductTypeDetail(courseType);
|
||||||
|
const headerUrl = `${process.env.MARKETING_SITE_BASE_URL}${productTypeDetail.url}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Hyperlink
|
||||||
|
destination={headerUrl}
|
||||||
|
className="base-card-link text-decoration-none"
|
||||||
|
onClick={() => {
|
||||||
|
handleClick(courseType, headerUrl);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="d-flex align-items-center border-bottom">
|
||||||
|
<h3 className="h3 mb-2 text-left">
|
||||||
|
{formatMessage(productTypeDetail.heading)}
|
||||||
|
</h3>
|
||||||
|
<Icon src={ChevronRight} className="text-primary-500 ml-2.5" />
|
||||||
|
</div>
|
||||||
|
</Hyperlink>
|
||||||
|
<p className="text-gray-500 x-small mt-2 mb-2">
|
||||||
|
{formatMessage(productTypeDetail.description)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProductCardHeader.propTypes = {
|
||||||
|
courseType: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductCardHeader;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import ProductCardHeader from './ProductCardHeader';
|
||||||
|
import { executiveEducation, bootCamp } from '../constants';
|
||||||
|
import { trackProductHeaderClicked } from '../optimizelyExperiment';
|
||||||
|
import { recommendationsHeaderClicked } from '../track';
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
|
getAuthenticatedUser: jest.fn(() => ({ userId: '1' })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../optimizelyExperiment', () => ({
|
||||||
|
trackProductHeaderClicked: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../track', () => ({
|
||||||
|
recommendationsHeaderClicked: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ProductRecommendations ProductCardHeader', () => {
|
||||||
|
const coursesType = 'Courses';
|
||||||
|
|
||||||
|
it('matches snapshot', () => {
|
||||||
|
expect(shallow(<ProductCardHeader courseType={executiveEducation} />)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with bootcamp courseType prop', () => {
|
||||||
|
it('renders a bootcamp header', () => {
|
||||||
|
const wrapper = shallow(<ProductCardHeader courseType={bootCamp} />);
|
||||||
|
|
||||||
|
expect(wrapper.find('h3').text()).toEqual(bootCamp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with courses courseType prop', () => {
|
||||||
|
it('renders a courses header', () => {
|
||||||
|
const wrapper = shallow(<ProductCardHeader courseType={coursesType} />);
|
||||||
|
|
||||||
|
expect(wrapper.find('h3').text()).toEqual(coursesType);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('send outs experiment events when clicked', () => {
|
||||||
|
const wrapper = shallow(<ProductCardHeader courseType={executiveEducation} />);
|
||||||
|
const hyperLink = wrapper.find('Hyperlink');
|
||||||
|
const execEdLink = 'http://localhost:18000/executive-education?linked_from=recommender';
|
||||||
|
|
||||||
|
hyperLink.simulate('click');
|
||||||
|
|
||||||
|
expect(trackProductHeaderClicked).toHaveBeenCalledWith('1');
|
||||||
|
expect(recommendationsHeaderClicked).toHaveBeenCalledWith(executiveEducation, execEdLink);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ProductRecommendations LoadedView matches snapshot 1`] = `
|
||||||
|
<Container
|
||||||
|
className="recommendations-container pt-sm-5 pt-4.5 pb-2 pb-sm-4.5"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
You might also like
|
||||||
|
</h2>
|
||||||
|
<ProductCardContainer
|
||||||
|
courseTypes={
|
||||||
|
Array [
|
||||||
|
"Executive Education",
|
||||||
|
"Boot Camp",
|
||||||
|
"Course",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
finalProductList={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"courseRunKey": "course-v1:Test+Course+2022T2",
|
||||||
|
"courseType": "executive-education-2u",
|
||||||
|
"image": Object {
|
||||||
|
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
|
||||||
|
},
|
||||||
|
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
|
||||||
|
"owners": Array [
|
||||||
|
Object {
|
||||||
|
"key": "HarvardX",
|
||||||
|
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
|
||||||
|
"name": "Harvard University",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Introduction to Computer Science",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"courseRunKey": "course-v1:Test+Course+2022T2",
|
||||||
|
"courseType": "bootcamp-2u",
|
||||||
|
"image": Object {
|
||||||
|
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
|
||||||
|
},
|
||||||
|
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
|
||||||
|
"owners": Array [
|
||||||
|
Object {
|
||||||
|
"key": "HarvardX",
|
||||||
|
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
|
||||||
|
"name": "Harvard University",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Introduction to Computer Science",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"courseRunKey": "course-v1:Test+Course+2022T2",
|
||||||
|
"courseType": "verified-audit",
|
||||||
|
"image": Object {
|
||||||
|
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
|
||||||
|
},
|
||||||
|
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
|
||||||
|
"owners": Array [
|
||||||
|
Object {
|
||||||
|
"key": "HarvardX",
|
||||||
|
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
|
||||||
|
"name": "Harvard University",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Introduction to Computer Science",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"courseRunKey": "course-v1:Test+Course+2022T2",
|
||||||
|
"courseType": "audit",
|
||||||
|
"image": Object {
|
||||||
|
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
|
||||||
|
},
|
||||||
|
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
|
||||||
|
"owners": Array [
|
||||||
|
Object {
|
||||||
|
"key": "HarvardX",
|
||||||
|
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
|
||||||
|
"name": "Harvard University",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"title": "Introduction to Computer Science",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ProductRecommendations LoadingView matches snapshot 1`] = `
|
||||||
|
<Skeleton
|
||||||
|
height={100}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user