Compare commits
43 Commits
aurora/use
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f92bd9c8f9 | ||
|
|
5db95b0029 | ||
|
|
a479b7ead6 | ||
|
|
e43a49b431 | ||
|
|
4643e0b130 | ||
|
|
8c29abd0c8 | ||
|
|
d44b123815 | ||
|
|
8829f756d8 | ||
|
|
176a803f94 | ||
|
|
309a07ffa9 | ||
|
|
e3784d36f1 | ||
|
|
5048fffd04 | ||
|
|
5ca3036849 | ||
|
|
e57f44068b | ||
|
|
a4d10b6c72 | ||
|
|
5769629250 | ||
|
|
a59ff5e7e8 | ||
|
|
9a9c0583ca | ||
|
|
2f409e5168 | ||
|
|
cf35c7d611 | ||
|
|
4a2eee2a1d | ||
|
|
0ed2b10b13 | ||
|
|
01f67265f6 | ||
|
|
8a73043368 | ||
|
|
b09c36e13e | ||
|
|
14f7389900 | ||
|
|
895e867b91 | ||
|
|
6bc60bad33 | ||
|
|
5e716ece2d | ||
|
|
320f6acc21 | ||
|
|
af51373e2c | ||
|
|
5dd00e9f24 | ||
|
|
63eaa00ee1 | ||
|
|
e25610c66e | ||
|
|
5724d051b2 | ||
|
|
9a5ac5ddf7 | ||
|
|
145c18d9ed | ||
|
|
b4bb924659 | ||
|
|
45e8113553 | ||
|
|
cfb9bfdb6b | ||
|
|
6a73054a9c | ||
|
|
5d88e8d1ec | ||
|
|
19d7aa3e33 |
5
.env
5
.env
@@ -2,6 +2,7 @@ NODE_ENV='production'
|
||||
NODE_PATH=./src
|
||||
BASE_URL=''
|
||||
LMS_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
LOGIN_URL=''
|
||||
LOGOUT_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
@@ -35,3 +36,7 @@ ZENDESK_KEY=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
ACCOUNT_PROFILE_URL=''
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
|
||||
@@ -2,6 +2,7 @@ NODE_ENV='development'
|
||||
PORT=1996
|
||||
BASE_URL='localhost:1996'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
@@ -42,3 +43,7 @@ ZENDESK_KEY=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
|
||||
@@ -2,6 +2,7 @@ NODE_ENV='test'
|
||||
PORT=1996
|
||||
BASE_URL='localhost:1996'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
@@ -41,3 +42,7 @@ ZENDESK_KEY='test-zendesk-key'
|
||||
HOTJAR_APP_ID='hot-jar-app-id'
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
|
||||
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
|
||||
@@ -5,6 +5,7 @@ const config = createConfig('eslint', {
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'import/no-import-module-exports': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
},
|
||||
});
|
||||
|
||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -11,17 +11,17 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
9
.github/workflows/npm-publish.yml
vendored
9
.github/workflows/npm-publish.yml
vendored
@@ -10,14 +10,17 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
27
Makefile
27
Makefile
@@ -1,15 +1,13 @@
|
||||
npm-install-%: ## install specified % npm package
|
||||
npm install $* --save-dev
|
||||
git add package.json
|
||||
export TRANSIFEX_RESOURCE = frontend-app-learner-dashboard
|
||||
transifex_langs = "ar,fr,fr_CA,es_419,pt_BR,zh_CN"
|
||||
|
||||
transifex_resource = frontend-app-learner-dashboard
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
@@ -49,15 +47,28 @@ push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
|
||||
|
||||
$(intl_imports) frontend-component-footer frontend-app-learner-dashboard
|
||||
endif
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
35403
package-lock.json
generated
35403
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -16,6 +16,7 @@
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"quality": "npm run lint-fix && npm run test",
|
||||
"watch-tests": "jest --watch",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"author": "edX",
|
||||
@@ -27,10 +28,10 @@
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
|
||||
"@edx/browserslist-config": "^1.1.0",
|
||||
"@edx/frontend-component-footer": "^11.4.1",
|
||||
"@edx/frontend-component-footer": "^12.0.0",
|
||||
"@edx/frontend-enterprise-hotjar": "^1.2.0",
|
||||
"@edx/frontend-platform": "^2.6.2",
|
||||
"@edx/paragon": "20.19.0",
|
||||
"@edx/frontend-platform": "^4.2.0",
|
||||
"@edx/paragon": "^20.32.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
@@ -76,10 +77,12 @@
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "11.0.1",
|
||||
"@edx/frontend-build": "12.8.27",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"enzyme-adapter-react-16": "^1.15.6",
|
||||
"fetch-mock": "^9.11.0",
|
||||
"husky": "^7.0.0",
|
||||
@@ -87,8 +90,7 @@
|
||||
"jest-expect-message": "^1.0.2",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
"react-test-renderer": "^16.14.0",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"semantic-release": "^17.4.5"
|
||||
"semantic-release": "^20.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -23,7 +23,7 @@ import ZendeskFab from 'components/ZendeskFab';
|
||||
import track from 'tracking';
|
||||
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
import LearnerDashboardHeaderVariant from './containers/LearnerDashboardHeaderVariant';
|
||||
import LearnerDashboardHeader from './containers/LearnerDashboardHeader';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -77,7 +77,7 @@ export const App = () => {
|
||||
<title>{formatMessage(messages.pageTitle)}</title>
|
||||
</Helmet>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
|
||||
@@ -12,14 +12,14 @@ import { Alert } from '@edx/paragon';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import LearnerDashboardHeaderVariant from 'containers/LearnerDashboardHeaderVariant';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeaderVariant', () => 'LearnerDashboardHeaderVariant');
|
||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: 'redux.selectors',
|
||||
@@ -54,7 +54,7 @@ describe('App router component', () => {
|
||||
expect(el.find(Helmet).find('title').text()).toEqual(useIntl().formatMessage(messages.pageTitle));
|
||||
});
|
||||
it('displays learner dashboard header', () => {
|
||||
expect(el.find(LearnerDashboardHeaderVariant).length).toEqual(1);
|
||||
expect(el.find(LearnerDashboardHeader).length).toEqual(1);
|
||||
});
|
||||
it('wraps the page in a browser router', () => {
|
||||
expect(el.find(Router)).toMatchObject(el);
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
@@ -40,7 +40,7 @@ exports[`App router component component no network failure snapshot 1`] = `
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Dashboard />
|
||||
</main>
|
||||
@@ -63,7 +63,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
>
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
</IntlProvider>
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
>
|
||||
<AppProvider
|
||||
store={
|
||||
Object {
|
||||
"redux": "store",
|
||||
}
|
||||
<AppProvider
|
||||
store={
|
||||
Object {
|
||||
"redux": "store",
|
||||
}
|
||||
>
|
||||
}
|
||||
>
|
||||
<NoticesWrapper>
|
||||
<Switch>
|
||||
<PageRoute
|
||||
path="/"
|
||||
@@ -31,6 +25,6 @@ exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
to="/"
|
||||
/>
|
||||
</Switch>
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
</NoticesWrapper>
|
||||
</AppProvider>
|
||||
`;
|
||||
|
||||
26
src/components/NoticesWrapper/api.js
Normal file
26
src/components/NoticesWrapper/api.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
|
||||
export const error404Message = 'This probably happened because the notices plugin is not installed on platform.';
|
||||
|
||||
export const getNotices = ({ onLoad }) => {
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
|
||||
const handleError = async (e) => {
|
||||
// Error probably means that notices is not installed, which is fine.
|
||||
const { customAttributes: { httpErrorStatus } } = e;
|
||||
if (httpErrorStatus === 404) {
|
||||
logInfo(`${e}. ${error404Message}`);
|
||||
} else {
|
||||
logError(e);
|
||||
}
|
||||
};
|
||||
if (authenticatedUser) {
|
||||
return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default { getNotices };
|
||||
65
src/components/NoticesWrapper/api.test.js
Normal file
65
src/components/NoticesWrapper/api.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({
|
||||
LMS_BASE_URL: 'test-lms-url',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
getAuthenticatedUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
logError: jest.fn(),
|
||||
logInfo: jest.fn(),
|
||||
}));
|
||||
|
||||
const testData = 'test-data';
|
||||
const successfulGet = () => Promise.resolve(testData);
|
||||
const error404 = { customAttributes: { httpErrorStatus: 404 }, test: 'error' };
|
||||
const error404Get = () => Promise.reject(error404);
|
||||
const error500 = { customAttributes: { httpErrorStatus: 500 }, test: 'error' };
|
||||
const error500Get = () => Promise.reject(error500);
|
||||
|
||||
const get = jest.fn().mockImplementation(successfulGet);
|
||||
getAuthenticatedHttpClient.mockReturnValue({ get });
|
||||
const authenticatedUser = { fake: 'user' };
|
||||
getAuthenticatedUser.mockReturnValue(authenticatedUser);
|
||||
|
||||
const onLoad = jest.fn();
|
||||
describe('getNotices api method', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('not authenticated', () => {
|
||||
it('does not fetch anything', () => {
|
||||
getAuthenticatedUser.mockReturnValueOnce(null);
|
||||
api.getNotices({ onLoad });
|
||||
expect(get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('authenticated', () => {
|
||||
it('fetches noticesUrl with onLoad behavior', async () => {
|
||||
await api.getNotices({ onLoad });
|
||||
expect(get).toHaveBeenCalledWith(api.noticesUrl, {});
|
||||
expect(onLoad).toHaveBeenCalledWith(testData);
|
||||
});
|
||||
it('calls logInfo if fetch fails with 404', async () => {
|
||||
get.mockImplementation(error404Get);
|
||||
await api.getNotices({ onLoad });
|
||||
expect(logInfo).toHaveBeenCalledWith(`${error404}. ${api.error404Message}`);
|
||||
});
|
||||
it('calls logError if fetch fails with non-404 error', async () => {
|
||||
get.mockImplementation(error500Get);
|
||||
await api.getNotices({ onLoad });
|
||||
expect(logError).toHaveBeenCalledWith(error500);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
35
src/components/NoticesWrapper/hooks.js
Normal file
35
src/components/NoticesWrapper/hooks.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { getNotices } from './api';
|
||||
import * as module from './hooks';
|
||||
|
||||
/**
|
||||
* This component uses the platform-plugin-notices plugin to function.
|
||||
* If the user has an unacknowledged notice, they will be rerouted off
|
||||
* course home and onto a full-screen notice page. If the plugin is not
|
||||
* installed, or there are no notices, we just passthrough this component.
|
||||
*/
|
||||
export const state = StrictDict({
|
||||
isRedirected: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useNoticesWrapperData = () => {
|
||||
const [isRedirected, setIsRedirected] = module.state.isRedirected();
|
||||
React.useEffect(() => {
|
||||
if (getConfig().ENABLE_NOTICES) {
|
||||
getNotices({
|
||||
onLoad: (data) => {
|
||||
if (data?.data?.results?.length > 0) {
|
||||
setIsRedirected(true);
|
||||
window.location.replace(`${data.data.results[0]}?next=${window.location.href}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [setIsRedirected]);
|
||||
return { isRedirected };
|
||||
};
|
||||
|
||||
export default useNoticesWrapperData;
|
||||
83
src/components/NoticesWrapper/hooks.test.js
Normal file
83
src/components/NoticesWrapper/hooks.test.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getNotices } from './api';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
|
||||
jest.mock('./api', () => ({ getNotices: jest.fn() }));
|
||||
|
||||
getConfig.mockReturnValue({ ENABLE_NOTICES: true });
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
let hook;
|
||||
describe('NoticesWrapper hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.isRedirected);
|
||||
});
|
||||
describe('useNoticesWrapperData', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes state hooks', () => {
|
||||
hooks.useNoticesWrapperData();
|
||||
expect(hooks.state.isRedirected).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('effects', () => {
|
||||
it('does not call notices if not enabled', () => {
|
||||
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
|
||||
hooks.useNoticesWrapperData();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
cb();
|
||||
expect(getNotices).not.toHaveBeenCalled();
|
||||
});
|
||||
describe('getNotices call (if enabled) onLoad behavior', () => {
|
||||
it('does not redirect if there are no results', () => {
|
||||
hooks.useNoticesWrapperData();
|
||||
expect(React.useEffect).toHaveBeenCalled();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
cb();
|
||||
expect(getNotices).toHaveBeenCalled();
|
||||
const { onLoad } = getNotices.mock.calls[0][0];
|
||||
onLoad({});
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
onLoad({ data: {} });
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
onLoad({ data: { results: [] } });
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
});
|
||||
it('redirects and set isRedirected if results are returned', () => {
|
||||
delete window.location;
|
||||
window.location = { replace: jest.fn(), href: 'test-old-href' };
|
||||
hooks.useNoticesWrapperData();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected]);
|
||||
cb();
|
||||
expect(getNotices).toHaveBeenCalled();
|
||||
const { onLoad } = getNotices.mock.calls[0][0];
|
||||
const target = 'url-target';
|
||||
onLoad({ data: { results: [target] } });
|
||||
expect(state.setState.isRedirected).toHaveBeenCalledWith(true);
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`${target}?next=${window.location.href}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards isRedirected from state call', () => {
|
||||
hook = hooks.useNoticesWrapperData();
|
||||
expect(hook.isRedirected).toEqual(state.stateVals.isRedirected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
25
src/components/NoticesWrapper/index.jsx
Normal file
25
src/components/NoticesWrapper/index.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
|
||||
/**
|
||||
* This component uses the platform-plugin-notices plugin to function.
|
||||
* If the user has an unacknowledged notice, they will be rerouted off
|
||||
* course home and onto a full-screen notice page. If the plugin is not
|
||||
* installed, or there are no notices, we just passthrough this component.
|
||||
*/
|
||||
const NoticesWrapper = ({ children }) => {
|
||||
const { isRedirected } = useNoticesWrapperData();
|
||||
return (
|
||||
<div>
|
||||
{isRedirected === true ? null : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoticesWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default NoticesWrapper;
|
||||
34
src/components/NoticesWrapper/index.test.jsx
Normal file
34
src/components/NoticesWrapper/index.test.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
import NoticesWrapper from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = { isRedirected: false };
|
||||
useNoticesWrapperData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
const children = [<b key={1}>some</b>, <i key={2}>children</i>];
|
||||
describe('NoticesWrapper component', () => {
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(useNoticesWrapperData).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('does not show children if redirected', () => {
|
||||
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
|
||||
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(el.children().length).toEqual(0);
|
||||
});
|
||||
it('shows children if not redirected', () => {
|
||||
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(el.children().length).toEqual(2);
|
||||
expect(el.children().at(0).matchesElement(children[0])).toEqual(true);
|
||||
expect(el.children().at(1).matchesElement(children[1])).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,17 @@ exports[`ZendeskFab snapshot 1`] = `
|
||||
},
|
||||
},
|
||||
"chat": Object {
|
||||
"departments": Object {
|
||||
"enabled": Array [
|
||||
"account settings",
|
||||
"billing and payments",
|
||||
"certificates",
|
||||
"deadlines",
|
||||
"errors and technical issues",
|
||||
"other",
|
||||
"proctoring",
|
||||
],
|
||||
},
|
||||
"suppress": false,
|
||||
},
|
||||
"contactForm": Object {
|
||||
|
||||
@@ -16,6 +16,9 @@ const ZendeskFab = () => {
|
||||
},
|
||||
chat: {
|
||||
suppress: false,
|
||||
departments: {
|
||||
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
|
||||
},
|
||||
},
|
||||
contactForm: {
|
||||
ticketForms: [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const configuration = {
|
||||
// BASE_URL: process.env.BASE_URL,
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
|
||||
// LOGIN_URL: process.env.LOGIN_URL,
|
||||
// LOGOUT_URL: process.env.LOGOUT_URL,
|
||||
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
|
||||
@@ -13,6 +14,8 @@ const configuration = {
|
||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
|
||||
ZENDESK_KEY: process.env.ZENDESK_KEY,
|
||||
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
||||
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
||||
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
@@ -5,14 +5,14 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const BeginCourseButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { disableBeginCourse } = useActionDisabledState(cardId);
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
@@ -20,7 +20,7 @@ export const BeginCourseButton = ({ cardId }) => {
|
||||
);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={isMasquerading || !hasAccess}
|
||||
disabled={disableBeginCourse}
|
||||
as="a"
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
@@ -14,14 +15,12 @@ jest.mock('tracking', () => ({
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'home-url' })),
|
||||
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
let wrapper;
|
||||
@@ -51,21 +50,10 @@ describe('BeginCourseButton', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('initializes enrollment data with cardId', () => {
|
||||
test('disabled states', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('learner does not have access', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,14 +5,14 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const ResumeButton = ({ cardId }) => {
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess, isAudit, isAuditAccessExpired } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { disableResumeCourse } = useActionDisabledState(cardId);
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
@@ -20,7 +20,7 @@ export const ResumeButton = ({ cardId }) => {
|
||||
);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired)}
|
||||
disabled={disableResumeCourse}
|
||||
as="a"
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -3,22 +3,18 @@ import { shallow } from 'enzyme';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ResumeButton from './ResumeButton';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ resumeUrl: 'resumeUrl' })),
|
||||
useCardEnrollmentData: jest.fn(() => ({
|
||||
hasAccess: true,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: false,
|
||||
})),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useTrackCourseEvent: (eventName, cardId, url) => jest
|
||||
.fn()
|
||||
.mockName(`useTrackCourseEvent('${eventName}', '${cardId}', '${url}')`),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: 'enterCourseClicked',
|
||||
@@ -48,36 +44,14 @@ describe('ResumeButton', () => {
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data based on cardId', () => {
|
||||
shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
|
||||
props.cardId,
|
||||
);
|
||||
});
|
||||
it('initializes course enrollment data based on cardId', () => {
|
||||
shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner does not have access', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
hasAccess: false,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: false,
|
||||
});
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('audit access expired', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
hasAccess: true,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: true,
|
||||
});
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('disabled states', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,18 +4,17 @@ import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const SelectSessionButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { canChange, hasSessions } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const { disableSelectSession } = useActionDisabledState(cardId);
|
||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={isMasquerading || !hasAccess || (!canChange || !hasSessions)}
|
||||
disabled={disableSelectSession}
|
||||
onClick={openSessionModal}
|
||||
>
|
||||
{formatMessage(messages.selectSession)}
|
||||
|
||||
@@ -2,66 +2,34 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
|
||||
useCardEntitlementData: jest.fn(() => ({ canChange: true, hasSessions: true })),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useUpdateSelectSessionModalCallback: () => jest.fn().mockName('mockOpenSessionModal'),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
let wrapper;
|
||||
|
||||
describe('SelectSessionButton', () => {
|
||||
const props = { cardId: 'cardId' };
|
||||
describe('snapshot', () => {
|
||||
test('renders default button', () => {
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('renders disabled button when user does not have access to the course', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('renders disabled button if masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('default render', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick).getMockName()).toEqual(
|
||||
reduxHooks.useUpdateSelectSessionModalCallback().getMockName(),
|
||||
);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('default render', () => {
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick).getMockName())
|
||||
.toEqual(reduxHooks.useUpdateSelectSessionModalCallback().getMockName());
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('learner does not have access', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner cannot change sessions', () => {
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ canChange: false, hasSessions: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('entitlement does not have available sessions', () => {
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ canChange: true, hasSessions: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('user is masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
test('disabled states', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true });
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
@@ -14,15 +15,14 @@ export const UpgradeButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { canUpgrade } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { disableUpgradeCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.upgradeClicked,
|
||||
cardId,
|
||||
upgradeUrl,
|
||||
);
|
||||
|
||||
const isEnabled = (!isMasquerading && canUpgrade);
|
||||
const enabledProps = {
|
||||
as: 'a',
|
||||
href: upgradeUrl,
|
||||
@@ -32,8 +32,8 @@ export const UpgradeButton = ({ cardId }) => {
|
||||
<ActionButton
|
||||
iconBefore={Locked}
|
||||
variant="outline-primary"
|
||||
disabled={!isEnabled}
|
||||
{...isEnabled && enabledProps}
|
||||
disabled={disableUpgradeCourse}
|
||||
{...!disableUpgradeCourse && enabledProps}
|
||||
>
|
||||
{formatMessage(messages.upgrade)}
|
||||
</ActionButton>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
@@ -13,15 +14,13 @@ jest.mock('tracking', () => ({
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableUpgradeCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
describe('UpgradeButton', () => {
|
||||
@@ -42,13 +41,7 @@ describe('UpgradeButton', () => {
|
||||
));
|
||||
});
|
||||
test('cannot upgrade', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
|
||||
@@ -5,23 +5,23 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const ViewCourseButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess, isAudit, isAuditAccessExpired } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { disableViewCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
);
|
||||
// disabled on no access or (is audit track but audit access was expired)
|
||||
const disabledViewCourseButton = !hasAccess || (isAudit && isAuditAccessExpired);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={disabledViewCourseButton}
|
||||
disabled={disableViewCourse}
|
||||
as="a"
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
import track from 'tracking';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
@@ -13,73 +14,33 @@ jest.mock('tracking', () => ({
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
let wrapper;
|
||||
const defaultProps = { cardId: 'cardId' };
|
||||
const homeUrl = 'homeUrl';
|
||||
|
||||
const createWrapper = ({
|
||||
hasAccess = false,
|
||||
isAudit = false,
|
||||
isAuditAccessExpired = false,
|
||||
isEntitlement = false,
|
||||
isExpired = false,
|
||||
propsOveride = {},
|
||||
}) => {
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess, isAudit, isAuditAccessExpired });
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
|
||||
return shallow(<ViewCourseButton {...defaultProps} {...propsOveride} />);
|
||||
};
|
||||
|
||||
describe('ViewCourseButton', () => {
|
||||
describe('learner has access to course', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ hasAccess: true });
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('links to home URL', () => {
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
defaultProps.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
});
|
||||
test('link is enabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
test('link is disabled when audit access is expired', () => {
|
||||
wrapper = createWrapper({ hasAccess: true, isAudit: true, isAuditAccessExpired: true });
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner can view course', () => {
|
||||
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
defaultProps.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
describe('learner does not have access to course', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ hasAccess: false });
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('links to home URL', () => {
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
defaultProps.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
});
|
||||
test('link is disabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner cannot view course', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
|
||||
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectSessionButton snapshot renders default button 1`] = `
|
||||
exports[`SelectSessionButton default render 1`] = `undefined`;
|
||||
|
||||
exports[`SelectSessionButton disabled states 1`] = `
|
||||
<ActionButton
|
||||
disabled={false}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
@@ -8,21 +10,3 @@ exports[`SelectSessionButton snapshot renders default button 1`] = `
|
||||
Select Session
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionButton snapshot renders disabled button if masquerading 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Select Session
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionButton snapshot renders disabled button when user does not have access to the course 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Select Session
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
@@ -30,13 +30,3 @@ exports[`UpgradeButton snapshot cannot upgrade 1`] = `
|
||||
Upgrade
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`UpgradeButton snapshot masquerading 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ViewCourseButton learner does not have access to course snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
View Course
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`ViewCourseButton learner has access to course snapshot 1`] = `
|
||||
exports[`ViewCourseButton learner can view course 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
@@ -37,3 +18,22 @@ exports[`ViewCourseButton learner has access to course snapshot 1`] = `
|
||||
View Course
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`ViewCourseButton learner cannot view course 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
View Course
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
@@ -40,6 +40,21 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isDownloadable) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
{formatMessage(messages.certReady)}
|
||||
{certificate.certPreviewUrl && (
|
||||
<>
|
||||
{' '}
|
||||
<Hyperlink isInline destination={certificate.certPreviewUrl}>
|
||||
{formatMessage(messages.viewCertificate)}
|
||||
</Hyperlink>
|
||||
</>
|
||||
)}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (!isPassing) {
|
||||
if (isAudit) {
|
||||
return (
|
||||
@@ -63,17 +78,6 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isDownloadable) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
{formatMessage(messages.certReady)}
|
||||
{' '}
|
||||
<Hyperlink isInline destination={certificate.certPreviewUrl}>
|
||||
{formatMessage(messages.viewCertificate)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isEarnedButUnavailable) {
|
||||
return (
|
||||
<Banner>
|
||||
|
||||
@@ -54,6 +54,7 @@ describe('CertificateBanner', () => {
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
|
||||
return shallow(<CertificateBanner {...props} />);
|
||||
};
|
||||
/** TODO: Update tests to validate snapshots **/
|
||||
describe('snapshot', () => {
|
||||
test('is restricted', () => {
|
||||
const wrapper = createWrapper({
|
||||
@@ -74,6 +75,20 @@ describe('CertificateBanner', () => {
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is passing and is downloadable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: { isPassing: true },
|
||||
certificate: { isDownloadable: true },
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('not passing and is downloadable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: { isPassing: false },
|
||||
certificate: { isDownloadable: true },
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('not passing and audit', () => {
|
||||
const wrapper = createWrapper({
|
||||
enrollment: {
|
||||
@@ -92,17 +107,6 @@ describe('CertificateBanner', () => {
|
||||
const wrapper = createWrapper({});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is passing and is downloadable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: {
|
||||
isPassing: true,
|
||||
},
|
||||
certificate: {
|
||||
isDownloadable: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is passing and is earned but unavailable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: {
|
||||
|
||||
@@ -36,11 +36,9 @@ export const CourseBanner = ({ cardId }) => {
|
||||
<Banner>
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
{
|
||||
<Hyperlink isInline destination="">
|
||||
{formatMessage(messages.findAnotherCourse)}
|
||||
</Hyperlink>
|
||||
}
|
||||
<Hyperlink isInline destination="">
|
||||
{formatMessage(messages.findAnotherCourse)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
))}
|
||||
|
||||
|
||||
@@ -11,10 +11,11 @@ import messages from './messages';
|
||||
|
||||
export const ApprovedContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{ href, message: formatMessage(messages.viewCredit) }}
|
||||
action={{ href, message: formatMessage(messages.viewCredit), disabled: isMasquerading }}
|
||||
message={formatMessage(
|
||||
messages.approved,
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ import ApprovedContent from './ApprovedContent';
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
@@ -22,6 +23,7 @@ const credit = {
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
describe('ApprovedContent component', () => {
|
||||
beforeEach(() => {
|
||||
@@ -44,6 +46,9 @@ describe('ApprovedContent component', () => {
|
||||
test('action.message is formatted viewCredit message', () => {
|
||||
expect(component.props().action.message).toEqual(formatMessage(messages.viewCredit));
|
||||
});
|
||||
test('action.disabled is false', () => {
|
||||
expect(component.props().action.disabled).toEqual(false);
|
||||
});
|
||||
test('message is formatted approved message', () => {
|
||||
expect(component.props().message).toEqual(formatMessage(
|
||||
messages.approved,
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import hooks from './hooks';
|
||||
@@ -12,11 +13,13 @@ import messages from './messages';
|
||||
export const MustRequestContent = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
message: formatMessage(messages.requestCredit),
|
||||
onClick: createCreditRequest,
|
||||
disabled: isMasquerading,
|
||||
}}
|
||||
message={formatMessage(messages.mustRequest, {
|
||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import messages from './messages';
|
||||
import hooks from './hooks';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
@@ -11,6 +12,9 @@ import MustRequestContent from './MustRequestContent';
|
||||
jest.mock('./hooks', () => ({
|
||||
useCreditRequestData: jest.fn(),
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: { useMasqueradeData: jest.fn() },
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
@@ -20,7 +24,11 @@ let component;
|
||||
const cardId = 'test-card-id';
|
||||
const requestData = { test: 'requestData' };
|
||||
const createCreditRequest = jest.fn().mockName('createCreditRequest');
|
||||
hooks.useCreditRequestData.mockReturnValue({ requestData, createCreditRequest });
|
||||
hooks.useCreditRequestData.mockReturnValue({
|
||||
requestData,
|
||||
createCreditRequest,
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<MustRequestContent cardId={cardId} />);
|
||||
@@ -43,13 +51,18 @@ describe('MustRequestContent component', () => {
|
||||
expect(component.props().action.onClick).toEqual(createCreditRequest);
|
||||
});
|
||||
test('action.message is formatted requestCredit message', () => {
|
||||
expect(component.props().action.message).toEqual(formatMessage(messages.requestCredit));
|
||||
expect(component.props().action.message).toEqual(
|
||||
formatMessage(messages.requestCredit),
|
||||
);
|
||||
});
|
||||
test('action.disabled is false', () => {
|
||||
expect(component.props().action.disabled).toEqual(false);
|
||||
});
|
||||
test('message is formatted mustRequest message', () => {
|
||||
expect(component.props().message).toEqual(
|
||||
formatMessage(messages.mustRequest, {
|
||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||
requestCredit: (<b>{formatMessage(messages.requestCredit)}</b>),
|
||||
linkToProviderSite: <ProviderLink cardId={cardId} />,
|
||||
requestCredit: <b>{formatMessage(messages.requestCredit)}</b>,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,12 +9,14 @@ import messages from './messages';
|
||||
|
||||
export const PendingContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
href,
|
||||
message: formatMessage(messages.viewDetails),
|
||||
disabled: isMasquerading,
|
||||
}}
|
||||
message={formatMessage(messages.received, { providerName })}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,9 @@ import { reduxHooks } from 'hooks';
|
||||
import messages from './messages';
|
||||
import PendingContent from './PendingContent';
|
||||
|
||||
jest.mock('hooks', () => ({ reduxHooks: { useCardCreditData: jest.fn() } }));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
@@ -17,7 +19,11 @@ let component;
|
||||
const cardId = 'test-card-id';
|
||||
const providerName = 'test-credit-provider-name';
|
||||
const providerStatusUrl = 'test-credit-provider-status-url';
|
||||
reduxHooks.useCardCreditData.mockReturnValue({ providerName, providerStatusUrl });
|
||||
reduxHooks.useCardCreditData.mockReturnValue({
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<PendingContent cardId={cardId} />);
|
||||
@@ -40,7 +46,12 @@ describe('PendingContent component', () => {
|
||||
expect(component.props().action.href).toEqual(providerStatusUrl);
|
||||
});
|
||||
test('action.message is formatted requestCredit message', () => {
|
||||
expect(component.props().action.message).toEqual(formatMessage(messages.viewDetails));
|
||||
expect(component.props().action.message).toEqual(
|
||||
formatMessage(messages.viewDetails),
|
||||
);
|
||||
});
|
||||
test('action.disabled is false', () => {
|
||||
expect(component.props().action.disabled).toEqual(false);
|
||||
});
|
||||
test('message is formatted pending message', () => {
|
||||
expect(component.props().message).toEqual(
|
||||
|
||||
@@ -13,7 +13,9 @@ export const CreditContent = ({ action, message, requestData }) => (
|
||||
<ActionRow className="mt-4">
|
||||
<Button
|
||||
as="a"
|
||||
href={action.href}
|
||||
disabled={!!action.disabled}
|
||||
// make sure href is not undefined. Paragon won't disable the button if href is undefined.
|
||||
href={action.href || '#'}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
variant="outline-primary"
|
||||
@@ -36,6 +38,7 @@ CreditContent.propTypes = {
|
||||
href: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
message: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
}),
|
||||
message: PropTypes.node.isRequired,
|
||||
requestData: PropTypes.shape({
|
||||
|
||||
@@ -8,6 +8,7 @@ const action = {
|
||||
href: 'test-action-href',
|
||||
onClick: jest.fn().mockName('test-action-onClick'),
|
||||
message: 'test-action-message',
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
const message = 'test-message';
|
||||
@@ -27,6 +28,7 @@ describe('CreditContent component', () => {
|
||||
const buttonEl = el.find('ActionRow Button');
|
||||
expect(buttonEl.props().href).toEqual(action.href);
|
||||
expect(buttonEl.props().onClick).toEqual(action.onClick);
|
||||
expect(buttonEl.props().disabled).toEqual(action.disabled);
|
||||
expect(buttonEl.text()).toEqual(action.message);
|
||||
});
|
||||
it('loads message into credit-msg div', () => {
|
||||
@@ -35,6 +37,10 @@ describe('CreditContent component', () => {
|
||||
it('loads CreditRequestForm with passed requestData', () => {
|
||||
expect(el.find('CreditRequestForm').props().requestData).toEqual(requestData);
|
||||
});
|
||||
test('disables action button when action.disabled is true', () => {
|
||||
el.setProps({ action: { ...action, disabled: true } });
|
||||
expect(el.find('ActionRow Button').props().disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('without action', () => {
|
||||
test('snapshot', () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ exports[`CreditContent component render with action snapshot 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
className="border-gray-400"
|
||||
disabled={false}
|
||||
href="test-action-href"
|
||||
onClick={[MockFunction test-action-onClick]}
|
||||
rel="noopener"
|
||||
|
||||
@@ -14,7 +14,10 @@ export const useCreditRequestData = (cardId) => {
|
||||
const createCreditApiRequest = apiHooks.useCreateCreditRequest(cardId);
|
||||
const createCreditRequest = (e) => {
|
||||
e.preventDefault();
|
||||
createCreditApiRequest().then(setRequestData);
|
||||
createCreditApiRequest()
|
||||
.then((request) => {
|
||||
setRequestData(request.data);
|
||||
});
|
||||
};
|
||||
return { requestData, createCreditRequest };
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ jest.mock('hooks', () => ({
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const requestData = { test: 'request data' };
|
||||
const requestData = { data: 'request data' };
|
||||
const creditRequest = jest.fn().mockReturnValue(Promise.resolve(requestData));
|
||||
apiHooks.useCreateCreditRequest.mockReturnValue(creditRequest);
|
||||
const event = { preventDefault: jest.fn() };
|
||||
@@ -48,7 +48,7 @@ describe('Credit Banner view hooks', () => {
|
||||
it('calls api.createCreditRequest and sets requestData with the response', async () => {
|
||||
await out.createCreditRequest(event);
|
||||
expect(creditRequest).toHaveBeenCalledWith();
|
||||
expect(state.setState.creditRequestData).toHaveBeenCalledWith(requestData);
|
||||
expect(state.setState.creditRequestData).toHaveBeenCalledWith(requestData.data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,12 +6,6 @@ exports[`CertificateBanner snapshot is passing and is downloadable 1`] = `
|
||||
variant="success"
|
||||
>
|
||||
Congratulations. Your certificate is ready.
|
||||
|
||||
<Hyperlink
|
||||
isInline={true}
|
||||
>
|
||||
View Certificate.
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
@@ -113,6 +107,15 @@ exports[`CertificateBanner snapshot not passing and has finished 1`] = `
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot not passing and is downloadable 1`] = `
|
||||
<Banner
|
||||
icon={[MockFunction icons.CheckCircle]}
|
||||
variant="success"
|
||||
>
|
||||
Congratulations. Your certificate is ready.
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CertificateBanner snapshot not passing and not audit and not finished 1`] = `
|
||||
<Banner
|
||||
variant="warning"
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('CourseCardDetails hooks', () => {
|
||||
};
|
||||
const entitlementData = {
|
||||
isEntitlement: false,
|
||||
canViewCourse: false,
|
||||
disableViewCourse: false,
|
||||
isFulfilled: false,
|
||||
isExpired: false,
|
||||
canChange: false,
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Badge } from '@edx/paragon';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import verifiedRibbon from 'assets/verified-ribbon.png';
|
||||
import useActionDisabledState from './hooks';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
@@ -18,13 +18,13 @@ export const CourseCardImage = ({ cardId, orientation }) => {
|
||||
const { bannerImgSrc } = reduxHooks.useCardCourseData(cardId);
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isEntitlement } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const { disableCourseTitle } = useActionDisabledState(cardId);
|
||||
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
|
||||
const wrapperClassName = `pgn__card-wrapper-image-cap overflow-visible ${orientation}`;
|
||||
const image = (
|
||||
<>
|
||||
<img
|
||||
className="pgn__card-image-cap"
|
||||
className="pgn__card-image-cap show"
|
||||
src={bannerImgSrc}
|
||||
alt={formatMessage(messages.bannerAlt)}
|
||||
/>
|
||||
@@ -43,7 +43,7 @@ export const CourseCardImage = ({ cardId, orientation }) => {
|
||||
}
|
||||
</>
|
||||
);
|
||||
return isEntitlement
|
||||
return disableCourseTitle
|
||||
? (<div className={wrapperClassName}>{image}</div>)
|
||||
: (
|
||||
<a
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from './hooks';
|
||||
import CourseCardImage from './CourseCardImage';
|
||||
|
||||
const homeUrl = 'home-url';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
courseImageClicked: jest.fn().mockName('segment.courseImageClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(() => ({ bannerImgSrc: 'banner-img-src' })),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||
useCardEnrollmentData: jest.fn(() => ({ isVerified: true })),
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
|
||||
trackCourseEvent: { eventName, cardId, upgradeUrl },
|
||||
})),
|
||||
},
|
||||
}));
|
||||
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
|
||||
|
||||
describe('CourseCardImage', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
orientation: 'orientation',
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('renders clickable link course Image', () => {
|
||||
const wrapper = shallow(<CourseCardImage {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.type()).toBe('a');
|
||||
expect(wrapper.prop('onClick')).toEqual(
|
||||
reduxHooks.useTrackCourseEvent(
|
||||
track.course.courseImageClicked,
|
||||
props.cardId,
|
||||
homeUrl,
|
||||
),
|
||||
);
|
||||
});
|
||||
test('renders disabled link', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
|
||||
const wrapper = shallow(<CourseCardImage {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.type()).toBe('div');
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes', () => {
|
||||
shallow(<CourseCardImage {...props} />);
|
||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
|
||||
props.cardId,
|
||||
);
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,25 +3,33 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from './hooks';
|
||||
|
||||
const { courseTitleClicked } = track.course;
|
||||
|
||||
export const CourseCardTitle = ({ cardId }) => {
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const handleTitleClicked = reduxHooks.useTrackCourseEvent(courseTitleClicked, cardId, homeUrl);
|
||||
const handleTitleClicked = reduxHooks.useTrackCourseEvent(
|
||||
courseTitleClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
);
|
||||
const { disableCourseTitle } = useActionDisabledState(cardId);
|
||||
return (
|
||||
<h3>
|
||||
<a
|
||||
href={homeUrl}
|
||||
className="course-card-title"
|
||||
data-testid="CourseCardTitle"
|
||||
onClick={handleTitleClicked}
|
||||
disabled={isEntitlement && !isFulfilled}
|
||||
>
|
||||
{courseName}
|
||||
</a>
|
||||
{disableCourseTitle ? (
|
||||
<span className="course-card-title" data-testid="CourseCardTitle">{courseName}</span>
|
||||
) : (
|
||||
<a
|
||||
href={homeUrl}
|
||||
className="course-card-title"
|
||||
data-testid="CourseCardTitle"
|
||||
onClick={handleTitleClicked}
|
||||
>
|
||||
{courseName}
|
||||
</a>
|
||||
)}
|
||||
</h3>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from './hooks';
|
||||
import CourseCardTitle from './CourseCardTitle';
|
||||
|
||||
const homeUrl = 'home-url';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
courseTitleClicked: jest.fn().mockName('segment.courseTitleClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(() => ({ courseName: 'course-name' })),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
|
||||
trackCourseEvent: { eventName, cardId, upgradeUrl },
|
||||
})),
|
||||
},
|
||||
}));
|
||||
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
|
||||
|
||||
describe('CourseCardTitle', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('renders clickable link course title', () => {
|
||||
const wrapper = shallow(<CourseCardTitle {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const title = wrapper.find('.course-card-title');
|
||||
expect(title.type()).toBe('a');
|
||||
expect(title.prop('onClick')).toEqual(
|
||||
reduxHooks.useTrackCourseEvent(
|
||||
track.course.courseTitleClicked,
|
||||
props.cardId,
|
||||
homeUrl,
|
||||
),
|
||||
);
|
||||
});
|
||||
test('renders disabled link', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
|
||||
const wrapper = shallow(<CourseCardTitle {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const title = wrapper.find('.course-card-title');
|
||||
expect(title.type()).toBe('span');
|
||||
expect(title.prop('onClick')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes', () => {
|
||||
shallow(<CourseCardTitle {...props} />);
|
||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
|
||||
props.cardId,
|
||||
);
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,9 +17,8 @@ const cardId = 'test-card-id';
|
||||
const state = new MockUseState(hooks);
|
||||
const numPrograms = 27;
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
describe('RelatedProgramsBadge hooks', () => {
|
||||
const { formatMessage } = useIntl();
|
||||
let out;
|
||||
describe('state values', () => {
|
||||
state.testGetter(state.keys.isOpen);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
|
||||
<a
|
||||
className="pgn__card-wrapper-image-cap overflow-visible orientation"
|
||||
href="home-url"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.courseImageClicked],
|
||||
"upgradeUrl": "home-url",
|
||||
},
|
||||
}
|
||||
}
|
||||
tabIndex="-1"
|
||||
>
|
||||
<img
|
||||
alt="Course thumbnail"
|
||||
className="pgn__card-image-cap show"
|
||||
src="banner-img-src"
|
||||
/>
|
||||
<span
|
||||
className="course-card-verify-ribbon-container"
|
||||
title="You're enrolled as a verified student"
|
||||
>
|
||||
<Badge
|
||||
as="div"
|
||||
className="w-100"
|
||||
variant="success"
|
||||
>
|
||||
Verified
|
||||
</Badge>
|
||||
<img
|
||||
alt="ID Verified Ribbon/Badge"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
`;
|
||||
|
||||
exports[`CourseCardImage snapshot renders disabled link 1`] = `
|
||||
<div
|
||||
className="pgn__card-wrapper-image-cap overflow-visible orientation"
|
||||
>
|
||||
<img
|
||||
alt="Course thumbnail"
|
||||
className="pgn__card-image-cap show"
|
||||
src="banner-img-src"
|
||||
/>
|
||||
<span
|
||||
className="course-card-verify-ribbon-container"
|
||||
title="You're enrolled as a verified student"
|
||||
>
|
||||
<Badge
|
||||
as="div"
|
||||
className="w-100"
|
||||
variant="success"
|
||||
>
|
||||
Verified
|
||||
</Badge>
|
||||
<img
|
||||
alt="ID Verified Ribbon/Badge"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
|
||||
<h3>
|
||||
<a
|
||||
className="course-card-title"
|
||||
data-testid="CourseCardTitle"
|
||||
href="home-url"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.courseTitleClicked],
|
||||
"upgradeUrl": "home-url",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
course-name
|
||||
</a>
|
||||
</h3>
|
||||
`;
|
||||
|
||||
exports[`CourseCardTitle snapshot renders disabled link 1`] = `
|
||||
<h3>
|
||||
<span
|
||||
className="course-card-title"
|
||||
data-testid="CourseCardTitle"
|
||||
>
|
||||
course-name
|
||||
</span>
|
||||
</h3>
|
||||
`;
|
||||
32
src/containers/CourseCard/components/hooks.js
Normal file
32
src/containers/CourseCard/components/hooks.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
export const useActionDisabledState = (cardId) => {
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const {
|
||||
canUpgrade, hasAccess, isAudit, isAuditAccessExpired,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const {
|
||||
isEntitlement, isFulfilled, canChange, hasSessions,
|
||||
} = reduxHooks.useCardEntitlementData(cardId);
|
||||
|
||||
const { resumeUrl, homeUrl, upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
|
||||
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
|
||||
const disableSelectSession = !isEntitlement || isMasquerading || !hasAccess || (!canChange || !hasSessions);
|
||||
const disableUpgradeCourse = !upgradeUrl || (isMasquerading && !canUpgrade);
|
||||
|
||||
const disableCourseTitle = (isEntitlement && !isFulfilled) || disableViewCourse;
|
||||
|
||||
return {
|
||||
disableBeginCourse,
|
||||
disableResumeCourse,
|
||||
disableViewCourse,
|
||||
disableUpgradeCourse,
|
||||
disableSelectSession,
|
||||
disableCourseTitle,
|
||||
};
|
||||
};
|
||||
|
||||
export default useActionDisabledState;
|
||||
205
src/containers/CourseCard/components/hooks.test.js
Normal file
205
src/containers/CourseCard/components/hooks.test.js
Normal file
@@ -0,0 +1,205 @@
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const cardId = 'my-test-course-number';
|
||||
|
||||
describe('useActionDisabledState', () => {
|
||||
const defaultData = {
|
||||
isMasquerading: false,
|
||||
canUpgrade: false,
|
||||
isEntitlement: false,
|
||||
isFulfilled: false,
|
||||
canChange: false,
|
||||
hasSessions: false,
|
||||
hasAccess: false,
|
||||
isAudit: false,
|
||||
isAuditAccessExpired: false,
|
||||
resumeUrl: 'resume.url',
|
||||
homeUrl: 'home.url',
|
||||
upgradeUrl: 'upgrade.url',
|
||||
};
|
||||
const mockHooksData = (args) => {
|
||||
const {
|
||||
isMasquerading,
|
||||
canUpgrade,
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
hasSessions,
|
||||
hasAccess,
|
||||
isAudit,
|
||||
isAuditAccessExpired,
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
upgradeUrl,
|
||||
} = { ...defaultData, ...args };
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
canUpgrade,
|
||||
hasAccess,
|
||||
isAudit,
|
||||
isAuditAccessExpired,
|
||||
});
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
hasSessions,
|
||||
});
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
upgradeUrl,
|
||||
});
|
||||
};
|
||||
|
||||
describe('disableBeginCourse', () => {
|
||||
it('disable when homeUrl is invalid', () => {
|
||||
mockHooksData({ homeUrl: null });
|
||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableBeginCourse).toBe(true);
|
||||
});
|
||||
it('disable when isMasquerading is true', () => {
|
||||
mockHooksData({ isMasquerading: true });
|
||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableBeginCourse).toBe(true);
|
||||
});
|
||||
it('disable when hasAccess is false', () => {
|
||||
mockHooksData({ hasAccess: false });
|
||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableBeginCourse).toBe(true);
|
||||
});
|
||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||
mockHooksData({ isAudit: true, isAuditAccessExpired: true });
|
||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableBeginCourse).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
mockHooksData({ hasAccess: true });
|
||||
const { disableBeginCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableBeginCourse).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('disableResumeCourse', () => {
|
||||
it('disable when resumeUrl is invalid', () => {
|
||||
mockHooksData({ resumeUrl: null });
|
||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableResumeCourse).toBe(true);
|
||||
});
|
||||
it('disable when isMasquerading is true', () => {
|
||||
mockHooksData({ isMasquerading: true });
|
||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableResumeCourse).toBe(true);
|
||||
});
|
||||
it('disable when hasAccess is false', () => {
|
||||
mockHooksData({ hasAccess: false });
|
||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableResumeCourse).toBe(true);
|
||||
});
|
||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||
mockHooksData({ isAudit: true, isAuditAccessExpired: true });
|
||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableResumeCourse).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
mockHooksData({ hasAccess: true });
|
||||
const { disableResumeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableResumeCourse).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('disableViewCourse', () => {
|
||||
it('disable when hasAccess is false', () => {
|
||||
mockHooksData({ hasAccess: false });
|
||||
const { disableViewCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableViewCourse).toBe(true);
|
||||
});
|
||||
it('disable when isAudit is true and isAuditAccessExpired is true', () => {
|
||||
mockHooksData({ isAudit: true, isAuditAccessExpired: true });
|
||||
const { disableViewCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableViewCourse).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
mockHooksData({ hasAccess: true });
|
||||
const { disableViewCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableViewCourse).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('disableUpgradeCourse', () => {
|
||||
it('disable when upgradeUrl is invalid', () => {
|
||||
mockHooksData({ upgradeUrl: null });
|
||||
const { disableUpgradeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableUpgradeCourse).toBe(true);
|
||||
});
|
||||
it('disable when isMasquerading is true and canUpgrade is false', () => {
|
||||
mockHooksData({ isMasquerading: true, canUpgrade: false });
|
||||
const { disableUpgradeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableUpgradeCourse).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
mockHooksData({ canUpgrade: true });
|
||||
const { disableUpgradeCourse } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableUpgradeCourse).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('disableSelectSession', () => {
|
||||
it('disable when isEntitlement is false', () => {
|
||||
mockHooksData({ isEntitlement: false });
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(true);
|
||||
});
|
||||
it('disable when isMasquerading is true', () => {
|
||||
mockHooksData({ isMasquerading: true });
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(true);
|
||||
});
|
||||
it('disable when hasAccess is false', () => {
|
||||
mockHooksData({ hasAccess: false });
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(true);
|
||||
});
|
||||
it('disable when canChange is false', () => {
|
||||
mockHooksData({ canChange: false });
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(true);
|
||||
});
|
||||
it('disable when hasSessions is false', () => {
|
||||
mockHooksData({ hasSessions: false });
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
mockHooksData({
|
||||
isEntitlement: true, hasAccess: true, canChange: true, hasSessions: true,
|
||||
});
|
||||
const { disableSelectSession } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableSelectSession).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('disableCourseTitle', () => {
|
||||
it('disable when isEntitlement is true and isFulfilled is false', () => {
|
||||
mockHooksData({ isEntitlement: true, isFulfilled: false });
|
||||
const { disableCourseTitle } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableCourseTitle).toBe(true);
|
||||
});
|
||||
it('disable when disableViewCourse is true', () => {
|
||||
mockHooksData({ hasAccess: false });
|
||||
const { disableCourseTitle } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableCourseTitle).toBe(true);
|
||||
});
|
||||
it('enable when all conditions are met', () => {
|
||||
mockHooksData({ isEntitlement: true, isFulfilled: true, hasAccess: true });
|
||||
const { disableCourseTitle } = hooks.useActionDisabledState(cardId);
|
||||
expect(disableCourseTitle).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ exports[`NoCoursesView snapshot 1`] = `
|
||||
</p>
|
||||
<Button
|
||||
as="a"
|
||||
href="course-search-url"
|
||||
href="http://localhost:18000/course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
variant="brand"
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Image } from '@edx/paragon';
|
||||
import { Search } from '@edx/paragon/icons';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import emptyCourseSVG from 'assets/empty-course.svg';
|
||||
import { reduxHooks } from 'hooks';
|
||||
@@ -27,7 +28,7 @@ export const NoCoursesView = () => {
|
||||
<Button
|
||||
variant="brand"
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
href={baseAppUrl(courseSearchUrl)}
|
||||
iconBefore={Search}
|
||||
>
|
||||
{formatMessage(messages.exploreCoursesButton)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import EmptyCourse from '.';
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: 'course-search-url',
|
||||
courseSearchUrl: '/course-search-url',
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -34,7 +34,7 @@ exports[`EnterpriseDashboard snapshot 1`] = `
|
||||
onClick={[MockFunction useEnterpriseDashboardHook.handleCTAClick]}
|
||||
type="a"
|
||||
>
|
||||
Go To Dashboard
|
||||
Go to dashboard
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ const messages = defineMessages({
|
||||
},
|
||||
enterpriseDialogConfirmButton: {
|
||||
id: 'leanerDashboard.enterpriseDialogConfirmButton',
|
||||
defaultMessage: 'Go To Dashboard',
|
||||
defaultMessage: 'Go to dashboard',
|
||||
description: 'Confirm button to go to the dashboard url',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { AvatarButton, Dropdown } from '@edx/paragon';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { useIsCollapsed, findCoursesNavDropdownClicked } from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const AuthenticatedUserDropdown = ({ username }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
const { profileImage } = authenticatedUser;
|
||||
|
||||
return (
|
||||
<Dropdown variant={isCollapsed ? 'light' : 'dark'} className="user-dropdown ml-1">
|
||||
<Dropdown.Toggle
|
||||
as={AvatarButton}
|
||||
src={profileImage}
|
||||
id="user"
|
||||
variant="primary"
|
||||
>
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{username}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
<Dropdown.Header>SWITCH DASHBOARD</Dropdown.Header>
|
||||
<Dropdown.Item as="a" href="/edx-dashboard" className="active">Personal</Dropdown.Item>
|
||||
{!!dashboard && (
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
href={dashboard.url}
|
||||
key={dashboard.label}
|
||||
>
|
||||
{dashboard.label} {formatMessage(messages.dashboard)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${username}`}>
|
||||
{formatMessage(messages.profile)}
|
||||
</Dropdown.Item>
|
||||
{isCollapsed && (
|
||||
<>
|
||||
<Dropdown.Item href={urls.programsUrl}>
|
||||
{formatMessage(messages.viewPrograms)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href={courseSearchUrl} onClick={findCoursesNavDropdownClicked(courseSearchUrl)}>
|
||||
{formatMessage(messages.exploreCourses)}
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
)}
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
|
||||
{formatMessage(messages.account)}
|
||||
</Dropdown.Item>
|
||||
{getConfig().ORDER_HISTORY_URL && (
|
||||
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
||||
{formatMessage(messages.orderHistory)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item href={getConfig().SUPPORT_URL}>
|
||||
{formatMessage(messages.help)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
||||
{formatMessage(messages.signOut)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
AuthenticatedUserDropdown.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AuthenticatedUserDropdown;
|
||||
@@ -1,50 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { AuthenticatedUserDropdown } from './AuthenticatedUserDropdown';
|
||||
import { useIsCollapsed } from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppContext: {
|
||||
authenticatedUser: {
|
||||
profileImage: 'profileImage',
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: 'test-course-search-url',
|
||||
})),
|
||||
},
|
||||
}));
|
||||
jest.mock('containers/LearnerDashboardHeader/hooks', () => ({
|
||||
useIsCollapsed: jest.fn(),
|
||||
findCoursesNavDropdownClicked: (href) => jest.fn().mockName(`findCoursesNavDropdownClicked('${href}')`),
|
||||
}));
|
||||
|
||||
describe('AuthenticatedUserDropdown', () => {
|
||||
const props = {
|
||||
username: 'username',
|
||||
};
|
||||
const defaultDashboardData = {
|
||||
label: 'label',
|
||||
url: 'url',
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
test('with enterprise dashboard', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(defaultDashboardData);
|
||||
useIsCollapsed.mockReturnValueOnce(true);
|
||||
const wrapper = shallow(<AuthenticatedUserDropdown {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('without enterprise dashboard and expanded', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
const wrapper = shallow(<AuthenticatedUserDropdown {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Button, Badge } from '@edx/paragon';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
import { reduxHooks } from 'hooks';
|
||||
@@ -21,7 +21,7 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
|
||||
const exploreCoursesClick = findCoursesNavDropdownClicked(courseSearchUrl);
|
||||
const exploreCoursesClick = findCoursesNavDropdownClicked(urls.baseAppUrl(courseSearchUrl));
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
@@ -34,7 +34,7 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
href={urls.baseAppUrl(courseSearchUrl)}
|
||||
variant="inverse-primary"
|
||||
onClick={exploreCoursesClick}
|
||||
>
|
||||
@@ -45,11 +45,19 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
||||
</Button>
|
||||
{authenticatedUser && (
|
||||
<>
|
||||
{dashboard && (
|
||||
{!!dashboard && (
|
||||
<Button as="a" href={dashboard.url} variant="inverse-primary">
|
||||
{formatMessage(messages.dashboard)}
|
||||
</Button>
|
||||
)}
|
||||
{!dashboard && getConfig().CAREER_LINK_URL && (
|
||||
<Button href={`${getConfig().CAREER_LINK_URL}`}>
|
||||
{formatMessage(messages.career)}
|
||||
<Badge className="px-2 mx-2" variant="warning">
|
||||
{formatMessage(messages.newAlert)}
|
||||
</Badge>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
as="a"
|
||||
href={`${getConfig().LMS_BASE_URL}/u/${
|
||||
@@ -17,7 +17,7 @@ jest.mock('hooks', () => ({
|
||||
url: 'url',
|
||||
}),
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: 'courseSearchUrl',
|
||||
courseSearchUrl: '/courseSearchUrl',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
@@ -20,8 +20,8 @@ exports[`CollapseMenuBody render 1`] = `
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("courseSearchUrl")]}
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
@@ -86,8 +86,8 @@ exports[`CollapseMenuBody render unauthenticated 1`] = `
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("courseSearchUrl")]}
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
@@ -4,7 +4,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Menu, Close } from '@edx/paragon/icons';
|
||||
import { IconButton, Icon } from '@edx/paragon';
|
||||
|
||||
import { useLearnerDashboardHeaderVariantData, useIsCollapsed } from '../hooks';
|
||||
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
|
||||
|
||||
import CollapseMenuBody from './CollapseMenuBody';
|
||||
import BrandLogo from '../BrandLogo';
|
||||
@@ -14,7 +14,7 @@ import messages from '../messages';
|
||||
export const CollapsedHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderVariantData();
|
||||
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderData();
|
||||
|
||||
return (
|
||||
isCollapsed && (
|
||||
@@ -2,14 +2,14 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import CollapsedHeader from '.';
|
||||
|
||||
import { useLearnerDashboardHeaderVariantData, useIsCollapsed } from '../hooks';
|
||||
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
|
||||
|
||||
jest.mock('../BrandLogo', () => jest.fn(() => 'BrandLogo'));
|
||||
jest.mock('./CollapseMenuBody', () => jest.fn(() => 'CollapseMenuBody'));
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useIsCollapsed: jest.fn(() => true),
|
||||
useLearnerDashboardHeaderVariantData: jest.fn(() => ({
|
||||
useLearnerDashboardHeaderData: jest.fn(() => ({
|
||||
isOpen: false,
|
||||
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
|
||||
})),
|
||||
@@ -28,7 +28,7 @@ describe('CollapsedHeader', () => {
|
||||
});
|
||||
|
||||
it('renders with isOpen true', () => {
|
||||
useLearnerDashboardHeaderVariantData.mockReturnValueOnce({
|
||||
useLearnerDashboardHeaderData.mockReturnValueOnce({
|
||||
isOpen: true,
|
||||
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { AvatarButton, Dropdown } from '@edx/paragon';
|
||||
import { AvatarButton, Dropdown, Badge } from '@edx/paragon';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
@@ -39,10 +39,18 @@ export const AuthenticatedUserDropdown = () => {
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${authenticatedUser.username}`}>
|
||||
{!dashboard && getConfig().CAREER_LINK_URL && (
|
||||
<Dropdown.Item href={`${getConfig().CAREER_LINK_URL}`}>
|
||||
{formatMessage(messages.career)}
|
||||
<Badge className="px-2 mx-2" variant="warning">
|
||||
{formatMessage(messages.newAlert)}
|
||||
</Badge>
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`}>
|
||||
{formatMessage(messages.profile)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
|
||||
<Dropdown.Item href={getConfig().ACCOUNT_SETTINGS_URL}>
|
||||
{formatMessage(messages.account)}
|
||||
</Dropdown.Item>
|
||||
{getConfig().ORDER_HISTORY_URL && (
|
||||
@@ -1,10 +1,15 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { AuthenticatedUserDropdown } from './AuthenticatedUserDropdown';
|
||||
import { useIsCollapsed } from '../hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppContext: {
|
||||
authenticatedUser: {
|
||||
@@ -13,11 +18,13 @@ jest.mock('@edx/frontend-platform/react', () => ({
|
||||
},
|
||||
},
|
||||
}));
|
||||
const COURSE_SEARCH_URL = 'test-course-search-url';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: 'test-course-search-url',
|
||||
courseSearchUrl: COURSE_SEARCH_URL,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
@@ -26,6 +33,22 @@ jest.mock('../hooks', () => ({
|
||||
findCoursesNavDropdownClicked: (href) => jest.fn().mockName(`findCoursesNavDropdownClicked('${href}')`),
|
||||
}));
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
baseAppUrl: (url) => (url),
|
||||
programsUrl: 'http://localhost:18000/dashboard/programs',
|
||||
}));
|
||||
|
||||
const config = {
|
||||
ACCOUNT_PROFILE_URL: 'http://account-profile-url.test',
|
||||
ACCOUNT_SETTINGS_URL: 'http://account-settings-url.test',
|
||||
LOGOUT_URL: 'http://logout-url.test',
|
||||
ORDER_HISTORY_URL: 'http://order-history-url.test',
|
||||
SUPPORT_URL: 'http://localhost:18000/support',
|
||||
CAREER_LINK_URL: 'http://localhost:18000/career',
|
||||
LMS_BASE_URL: 'http:/localhost:18000',
|
||||
};
|
||||
getConfig.mockReturnValue(config);
|
||||
|
||||
describe('AuthenticatedUserDropdown', () => {
|
||||
const defaultDashboardData = {
|
||||
label: 'label',
|
||||
@@ -43,18 +43,23 @@ exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/u/username"
|
||||
href="http://account-profile-url.test/u/username"
|
||||
>
|
||||
Profile
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/account/settings"
|
||||
href="http://account-settings-url.test"
|
||||
>
|
||||
Account
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://order-history-url.test"
|
||||
>
|
||||
Order History
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/logout"
|
||||
href="http://logout-url.test"
|
||||
>
|
||||
Sign Out
|
||||
</Dropdown.Item>
|
||||
@@ -94,18 +99,34 @@ exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and ex
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/u/username"
|
||||
href="http://localhost:18000/career"
|
||||
>
|
||||
Career
|
||||
<Badge
|
||||
className="px-2 mx-2"
|
||||
variant="warning"
|
||||
>
|
||||
New
|
||||
</Badge>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://account-profile-url.test/u/username"
|
||||
>
|
||||
Profile
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/account/settings"
|
||||
href="http://account-settings-url.test"
|
||||
>
|
||||
Account
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://order-history-url.test"
|
||||
>
|
||||
Order History
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/logout"
|
||||
href="http://logout-url.test"
|
||||
>
|
||||
Sign Out
|
||||
</Dropdown.Item>
|
||||
@@ -27,8 +27,8 @@ exports[`ExpandedHeader render 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4"
|
||||
href="courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavClicked("courseSearchUrl")]}
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
@@ -18,7 +18,7 @@ export const ExpandedHeader = () => {
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
|
||||
const exploreCoursesClick = findCoursesNavClicked(courseSearchUrl);
|
||||
const exploreCoursesClick = findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
|
||||
|
||||
return (
|
||||
!isCollapsed && (
|
||||
@@ -44,7 +44,7 @@ export const ExpandedHeader = () => {
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
href={urls.baseAppUrl(courseSearchUrl)}
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
onClick={exploreCoursesClick}
|
||||
@@ -6,12 +6,13 @@ import { useIsCollapsed } from '../hooks';
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
programsUrl: 'programsUrl',
|
||||
baseAppUrl: url => (`http://localhost:18000${url}`),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: 'courseSearchUrl',
|
||||
courseSearchUrl: '/courseSearchUrl',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
@@ -1,63 +0,0 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Image } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const GreetingBanner = ({ size }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
let greetMessage;
|
||||
const hour = new Date().getHours();
|
||||
|
||||
if (hour > 16) {
|
||||
greetMessage = messages.goodEvening;
|
||||
} else if (hour > 11) {
|
||||
greetMessage = messages.goodAfternoon;
|
||||
} else {
|
||||
greetMessage = messages.goodMorning;
|
||||
}
|
||||
|
||||
const isSmall = size === 'small';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'd-flex align-items-center justify-content-center',
|
||||
{ 'pb-5': !isSmall, 'p-3.5': isSmall },
|
||||
)}
|
||||
>
|
||||
<a href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
<Image
|
||||
style={{ width: isSmall ? '46px' : '148px' }}
|
||||
className="d-block"
|
||||
src={getConfig().LOGO_WHITE_URL}
|
||||
alt={getConfig().SITE_NAME}
|
||||
/>
|
||||
</a>
|
||||
<div className={`greetings-slash-container-${size} bg-brand-500`} />
|
||||
{isSmall
|
||||
? (
|
||||
<h5 role="presentation" className="text-center text-accent-b">
|
||||
{formatMessage(greetMessage)}
|
||||
</h5>
|
||||
) : (
|
||||
<h1
|
||||
role="presentation"
|
||||
className="text-center text-accent-b display-1"
|
||||
>
|
||||
{formatMessage(greetMessage)}
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
GreetingBanner.propTypes = {
|
||||
size: PropTypes.oneOf(['small', 'large']).isRequired,
|
||||
};
|
||||
|
||||
export default GreetingBanner;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { GreetingBanner } from './GreetingBanner';
|
||||
|
||||
describe('GreetingBanner', () => {
|
||||
const morning = new Date('2021-01-01T11:59:59.999');
|
||||
const afternoon = new Date('2021-01-01T16:59:59.999');
|
||||
const evening = new Date('2021-01-01T18:00:00');
|
||||
afterAll(() => jest.useRealTimers());
|
||||
describe('snapshots', () => {
|
||||
['small', 'large'].forEach((size) => {
|
||||
test(`with size ${size} and morning`, () => {
|
||||
jest.useFakeTimers('modern').setSystemTime(morning);
|
||||
const wrapper = shallow(<GreetingBanner size={size} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test(`with size ${size} and afternoon`, () => {
|
||||
jest.useFakeTimers('modern').setSystemTime(afternoon);
|
||||
const wrapper = shallow(<GreetingBanner size={size} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test(`with size ${size} and evening`, () => {
|
||||
jest.useFakeTimers('modern').setSystemTime(evening);
|
||||
const wrapper = shallow(<GreetingBanner size={size} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
|
||||
<Dropdown
|
||||
className="user-dropdown ml-1"
|
||||
variant="light"
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
id="user"
|
||||
src="profileImage"
|
||||
variant="primary"
|
||||
>
|
||||
<span
|
||||
className="d-none d-md-inline"
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
username
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu
|
||||
className="dropdown-menu-right"
|
||||
>
|
||||
<Dropdown.Header>
|
||||
SWITCH DASHBOARD
|
||||
</Dropdown.Header>
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
className="active"
|
||||
href="/edx-dashboard"
|
||||
>
|
||||
Personal
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
href="url"
|
||||
key="label"
|
||||
>
|
||||
label
|
||||
|
||||
Dashboard
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/u/username"
|
||||
>
|
||||
Profile
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/dashboard/programs"
|
||||
>
|
||||
View Programs
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="test-course-search-url"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked('test-course-search-url')]}
|
||||
>
|
||||
Explore courses
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/account/settings"
|
||||
>
|
||||
Account
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/support"
|
||||
>
|
||||
Help
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/logout"
|
||||
>
|
||||
Sign Out
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
`;
|
||||
|
||||
exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and expanded 1`] = `
|
||||
<Dropdown
|
||||
className="user-dropdown ml-1"
|
||||
variant="dark"
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
id="user"
|
||||
src="profileImage"
|
||||
variant="primary"
|
||||
>
|
||||
<span
|
||||
className="d-none d-md-inline"
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
username
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu
|
||||
className="dropdown-menu-right"
|
||||
>
|
||||
<Dropdown.Header>
|
||||
SWITCH DASHBOARD
|
||||
</Dropdown.Header>
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
className="active"
|
||||
href="/edx-dashboard"
|
||||
>
|
||||
Personal
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/u/username"
|
||||
>
|
||||
Profile
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/account/settings"
|
||||
>
|
||||
Account
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/support"
|
||||
>
|
||||
Help
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/logout"
|
||||
>
|
||||
Sign Out
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
`;
|
||||
@@ -1,181 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GreetingBanner snapshots with size large and afternoon 1`] = `
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center pb-5"
|
||||
>
|
||||
<a
|
||||
href="http://localhost:18000/dashboard"
|
||||
>
|
||||
<Image
|
||||
alt="localhost"
|
||||
className="d-block"
|
||||
src="https://edx-cdn.org/v3/default/logo-white.svg"
|
||||
style={
|
||||
Object {
|
||||
"width": "148px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
className="greetings-slash-container-large bg-brand-500"
|
||||
/>
|
||||
<h1
|
||||
className="text-center text-accent-b display-1"
|
||||
role="presentation"
|
||||
>
|
||||
Good Afternoon!
|
||||
</h1>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GreetingBanner snapshots with size large and evening 1`] = `
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center pb-5"
|
||||
>
|
||||
<a
|
||||
href="http://localhost:18000/dashboard"
|
||||
>
|
||||
<Image
|
||||
alt="localhost"
|
||||
className="d-block"
|
||||
src="https://edx-cdn.org/v3/default/logo-white.svg"
|
||||
style={
|
||||
Object {
|
||||
"width": "148px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
className="greetings-slash-container-large bg-brand-500"
|
||||
/>
|
||||
<h1
|
||||
className="text-center text-accent-b display-1"
|
||||
role="presentation"
|
||||
>
|
||||
Good Evening!
|
||||
</h1>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GreetingBanner snapshots with size large and morning 1`] = `
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center pb-5"
|
||||
>
|
||||
<a
|
||||
href="http://localhost:18000/dashboard"
|
||||
>
|
||||
<Image
|
||||
alt="localhost"
|
||||
className="d-block"
|
||||
src="https://edx-cdn.org/v3/default/logo-white.svg"
|
||||
style={
|
||||
Object {
|
||||
"width": "148px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
className="greetings-slash-container-large bg-brand-500"
|
||||
/>
|
||||
<h1
|
||||
className="text-center text-accent-b display-1"
|
||||
role="presentation"
|
||||
>
|
||||
Good Morning!
|
||||
</h1>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GreetingBanner snapshots with size small and afternoon 1`] = `
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center p-3.5"
|
||||
>
|
||||
<a
|
||||
href="http://localhost:18000/dashboard"
|
||||
>
|
||||
<Image
|
||||
alt="localhost"
|
||||
className="d-block"
|
||||
src="https://edx-cdn.org/v3/default/logo-white.svg"
|
||||
style={
|
||||
Object {
|
||||
"width": "46px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
className="greetings-slash-container-small bg-brand-500"
|
||||
/>
|
||||
<h5
|
||||
className="text-center text-accent-b"
|
||||
role="presentation"
|
||||
>
|
||||
Good Afternoon!
|
||||
</h5>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GreetingBanner snapshots with size small and evening 1`] = `
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center p-3.5"
|
||||
>
|
||||
<a
|
||||
href="http://localhost:18000/dashboard"
|
||||
>
|
||||
<Image
|
||||
alt="localhost"
|
||||
className="d-block"
|
||||
src="https://edx-cdn.org/v3/default/logo-white.svg"
|
||||
style={
|
||||
Object {
|
||||
"width": "46px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
className="greetings-slash-container-small bg-brand-500"
|
||||
/>
|
||||
<h5
|
||||
className="text-center text-accent-b"
|
||||
role="presentation"
|
||||
>
|
||||
Good Evening!
|
||||
</h5>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`GreetingBanner snapshots with size small and morning 1`] = `
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center p-3.5"
|
||||
>
|
||||
<a
|
||||
href="http://localhost:18000/dashboard"
|
||||
>
|
||||
<Image
|
||||
alt="localhost"
|
||||
className="d-block"
|
||||
src="https://edx-cdn.org/v3/default/logo-white.svg"
|
||||
style={
|
||||
Object {
|
||||
"width": "46px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
<div
|
||||
className="greetings-slash-container-small bg-brand-500"
|
||||
/>
|
||||
<h5
|
||||
className="text-center text-accent-b"
|
||||
role="presentation"
|
||||
>
|
||||
Good Morning!
|
||||
</h5>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,97 +1,10 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LearnerDashboardHeader UserMenu snapshots with authenticated user 1`] = `
|
||||
<AuthenticatedUserDropdown
|
||||
username="test-username"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`LearnerDashboardHeader UserMenu snapshots without authenticated user 1`] = `""`;
|
||||
|
||||
exports[`LearnerDashboardHeader snapshots with collapsed 1`] = `
|
||||
exports[`LearnerDashboardHeader render 1`] = `
|
||||
<Fragment>
|
||||
<ConfirmEmailBanner />
|
||||
<div
|
||||
className="flex-column bg-primary"
|
||||
>
|
||||
<header
|
||||
className="learner-dashboard-header"
|
||||
>
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<div
|
||||
className="flex-grow-1"
|
||||
>
|
||||
<GreetingBanner
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="my-auto ml-1 d-flex"
|
||||
>
|
||||
<IconButton
|
||||
alt="Course search"
|
||||
as="a"
|
||||
href="test-course-search-url"
|
||||
iconAs="Icon"
|
||||
invertColors={true}
|
||||
onClick={[MockFunction findCoursesNavClicked('test-course-search-url')]}
|
||||
src={[MockFunction icons.Search]}
|
||||
variant="primary"
|
||||
/>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<MasqueradeBar />
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`LearnerDashboardHeader snapshots without collapsed 1`] = `
|
||||
<Fragment>
|
||||
<ConfirmEmailBanner />
|
||||
<div
|
||||
className="flex-column bg-primary"
|
||||
>
|
||||
<Image
|
||||
className="d-block w-100 mb-4"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
<header
|
||||
className="learner-dashboard-header"
|
||||
>
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/dashboard/programs"
|
||||
iconBefore={[MockFunction icons.Program]}
|
||||
variant="inverse-tertiary"
|
||||
>
|
||||
Switch to Programs
|
||||
</Button>
|
||||
<div
|
||||
className="flex-grow-1"
|
||||
/>
|
||||
<Button
|
||||
as="a"
|
||||
href="test-course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
onClick={[MockFunction findCoursesNavClicked('test-course-search-url')]}
|
||||
variant="inverse-tertiary"
|
||||
>
|
||||
Explore courses
|
||||
</Button>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</header>
|
||||
<GreetingBanner
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<CollapsedHeader />
|
||||
<ExpandedHeader />
|
||||
<MasqueradeBar />
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import track from 'tracking';
|
||||
import { StrictDict } from 'utils';
|
||||
import { linkNames } from 'tracking/constants';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
isOpen: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useIsCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
const isCollapsed = React.useMemo(() => (width <= breakpoints.large.maxWidth), [width]);
|
||||
const isCollapsed = React.useMemo(() => (width <= breakpoints.large.minWidth), [width]);
|
||||
return isCollapsed;
|
||||
};
|
||||
|
||||
@@ -17,8 +24,19 @@ export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCou
|
||||
linkName: linkNames.learnerHomeNavDropdownExplore,
|
||||
});
|
||||
|
||||
export const useLearnerDashboardHeaderData = () => {
|
||||
const [isOpen, setIsOpen] = module.state.isOpen(false);
|
||||
const toggleIsOpen = () => setIsOpen(!isOpen);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
toggleIsOpen,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
useIsCollapsed,
|
||||
findCoursesNavClicked,
|
||||
findCoursesNavDropdownClicked,
|
||||
useLearnerDashboardHeaderData,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import track from 'tracking';
|
||||
import { linkNames } from 'tracking/constants';
|
||||
import { useIsCollapsed, findCoursesNavClicked, findCoursesNavDropdownClicked } from './hooks';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const {
|
||||
useIsCollapsed,
|
||||
findCoursesNavClicked,
|
||||
findCoursesNavDropdownClicked,
|
||||
useLearnerDashboardHeaderData,
|
||||
} = hooks;
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
findCourses: {
|
||||
@@ -12,13 +24,17 @@ jest.mock('tracking', () => ({
|
||||
const url = 'http://example.com';
|
||||
|
||||
describe('LearnerDashboardHeader hooks', () => {
|
||||
describe('state values', () => {
|
||||
state.testGetter(state.keys.isOpen);
|
||||
});
|
||||
|
||||
describe('useIsCollapsed', () => {
|
||||
test('large screen is not collapsed', () => {
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.maxWidth + 1 });
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth + 1 });
|
||||
expect(useIsCollapsed()).toEqual(false);
|
||||
});
|
||||
test('small screen is collapsed', () => {
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.maxWidth - 1 });
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth - 1 });
|
||||
expect(useIsCollapsed()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -40,4 +56,14 @@ describe('LearnerDashboardHeader hooks', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLearnerDashboardHeaderData', () => {
|
||||
test('default state', () => {
|
||||
state.mock();
|
||||
const out = useLearnerDashboardHeaderData();
|
||||
state.expectInitializedWith(state.keys.isOpen, false);
|
||||
out.toggleIsOpen();
|
||||
expect(state.values.isOpen).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,92 +1,22 @@
|
||||
import React, { useContext } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { Program, Search } from '@edx/paragon/icons';
|
||||
import {
|
||||
Button, Image, IconButton, Icon,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import topBanner from 'assets/top_stripe.svg';
|
||||
import MasqueradeBar from 'containers/MasqueradeBar';
|
||||
import urls from 'data/services/lms/urls';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||
import GreetingBanner from './GreetingBanner';
|
||||
import ConfirmEmailBanner from './ConfirmEmailBanner';
|
||||
|
||||
import { useIsCollapsed, findCoursesNavClicked } from './hooks';
|
||||
import messages from './messages';
|
||||
import CollapsedHeader from './CollapsedHeader';
|
||||
import ExpandedHeader from './ExpandedHeader';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const UserMenu = () => {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
return authenticatedUser ? (<AuthenticatedUserDropdown username={authenticatedUser.username} />) : null;
|
||||
};
|
||||
export const LearnerDashboardHeader = () => (
|
||||
<>
|
||||
<ConfirmEmailBanner />
|
||||
<CollapsedHeader />
|
||||
<ExpandedHeader />
|
||||
<MasqueradeBar />
|
||||
</>
|
||||
);
|
||||
|
||||
export const LearnerDashboardHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
|
||||
const exploreCoursesClick = findCoursesNavClicked(courseSearchUrl);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmEmailBanner />
|
||||
<div className="flex-column bg-primary">
|
||||
{!(isCollapsed) && (
|
||||
<Image className="d-block w-100 mb-4" src={topBanner} />
|
||||
)}
|
||||
<header className="learner-dashboard-header">
|
||||
<div className="d-flex">
|
||||
{(!isCollapsed) && (
|
||||
<Button as="a" href={urls.programsUrl} variant="inverse-tertiary" iconBefore={Program}>
|
||||
{formatMessage(messages.switchToProgram)}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex-grow-1">
|
||||
{isCollapsed && <GreetingBanner size="small" />}
|
||||
</div>
|
||||
{isCollapsed ? (
|
||||
<div className="my-auto ml-1 d-flex">
|
||||
<IconButton
|
||||
alt={formatMessage(messages.courseSearchAlt)}
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
variant="primary"
|
||||
invertColors
|
||||
src={Search}
|
||||
iconAs={Icon}
|
||||
onClick={exploreCoursesClick}
|
||||
/>
|
||||
<UserMenu />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
variant="inverse-tertiary"
|
||||
iconBefore={Search}
|
||||
onClick={exploreCoursesClick}
|
||||
>
|
||||
{formatMessage(messages.exploreCourses)}
|
||||
</Button>
|
||||
<UserMenu />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
{!isCollapsed && <GreetingBanner size="large" />}
|
||||
</div>
|
||||
<MasqueradeBar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
LearnerDashboardHeader.propTypes = {
|
||||
};
|
||||
LearnerDashboardHeader.propTypes = {};
|
||||
|
||||
export default LearnerDashboardHeader;
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
.greetings-slash-container-small {
|
||||
height: 4px;
|
||||
width: 40px;
|
||||
transform-origin: center;
|
||||
transform: rotate(-70deg);
|
||||
.dropdown-menu-collapse {
|
||||
width: 100vw;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
.greetings-slash-container-large {
|
||||
height: 8px;
|
||||
width: 120px;
|
||||
transform-origin: center;
|
||||
transform: rotate(-70deg);
|
||||
|
||||
.learner-variant-header {
|
||||
a {
|
||||
// needed to make the link not resize the header
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.course-link {
|
||||
border-bottom: 2px solid !important;
|
||||
}
|
||||
|
||||
.course-link:hover {
|
||||
border-bottom: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-small-menu {
|
||||
> * {
|
||||
justify-content: flex-start !important;
|
||||
|
||||
border-radius: 0 !important;
|
||||
border-top: 1px solid #ddd !important;
|
||||
|
||||
&::after {
|
||||
content: '\00BB';
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
// copy from legacy dashboard
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
@@ -1,59 +1,18 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import LearnerDashboardHeader from '.';
|
||||
|
||||
import LearnerDashboardHeader, { UserMenu } from '.';
|
||||
|
||||
import { useIsCollapsed } from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppContext: {
|
||||
authenticatedUser: {
|
||||
username: 'test-username',
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('./hooks', () => ({
|
||||
useIsCollapsed: jest.fn(),
|
||||
findCoursesNavClicked: (href) => jest.fn().mockName(`findCoursesNavClicked('${href}')`),
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: 'test-course-search-url',
|
||||
})),
|
||||
},
|
||||
}));
|
||||
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');
|
||||
|
||||
jest.mock('./CollapsedHeader', () => 'CollapsedHeader');
|
||||
jest.mock('./ConfirmEmailBanner', () => 'ConfirmEmailBanner');
|
||||
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
|
||||
jest.mock('./GreetingBanner', () => 'GreetingBanner');
|
||||
jest.mock('./ExpandedHeader', () => 'ExpandedHeader');
|
||||
|
||||
describe('LearnerDashboardHeader', () => {
|
||||
describe('snapshots', () => {
|
||||
test('with collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(true);
|
||||
const wrapper = shallow(<LearnerDashboardHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('without collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
const wrapper = shallow(<LearnerDashboardHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('UserMenu', () => {
|
||||
describe('snapshots', () => {
|
||||
test('with authenticated user', () => {
|
||||
const wrapper = shallow(<UserMenu />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('without authenticated user', () => {
|
||||
AppContext.authenticatedUser = null;
|
||||
const wrapper = shallow(<UserMenu />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
test('render', () => {
|
||||
const wrapper = shallow(<LearnerDashboardHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('ConfirmEmailBanner')).toHaveLength(1);
|
||||
expect(wrapper.find('MasqueradeBar')).toHaveLength(1);
|
||||
expect(wrapper.find('CollapsedHeader')).toHaveLength(1);
|
||||
expect(wrapper.find('ExpandedHeader')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,70 +2,79 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
dashboard: {
|
||||
id: 'leanerDashboard.menu.dashboard.label',
|
||||
id: 'learnerVariantDashboard.menu.dashboard.label',
|
||||
defaultMessage: 'Dashboard',
|
||||
description: 'The text for the user menu Dashboard navigation link.',
|
||||
},
|
||||
help: {
|
||||
id: 'leanerDashboard.help.label',
|
||||
id: 'learnerVariantDashboard.help.label',
|
||||
defaultMessage: 'Help',
|
||||
description: 'The text for the link to the Help Center',
|
||||
},
|
||||
profile: {
|
||||
id: 'leanerDashboard.menu.profile.label',
|
||||
id: 'learnerVariantDashboard.menu.profile.label',
|
||||
defaultMessage: 'Profile',
|
||||
description: 'The text for the user menu Profile navigation link.',
|
||||
},
|
||||
viewPrograms: {
|
||||
id: 'leanerDashboard.menu.viewPrograms.label',
|
||||
id: 'learnerVariantDashboard.menu.viewPrograms.label',
|
||||
defaultMessage: 'View Programs',
|
||||
description: 'The text for the user menu View Programs navigation link.',
|
||||
},
|
||||
account: {
|
||||
id: 'leanerDashboard.menu.account.label',
|
||||
id: 'learnerVariantDashboard.menu.account.label',
|
||||
defaultMessage: 'Account',
|
||||
description: 'The text for the user menu Account navigation link.',
|
||||
},
|
||||
orderHistory: {
|
||||
id: 'leanerDashboard.menu.orderHistory.label',
|
||||
id: 'learnerVariantDashboard.menu.orderHistory.label',
|
||||
defaultMessage: 'Order History',
|
||||
description: 'The text for the user menu Order History navigation link.',
|
||||
},
|
||||
signOut: {
|
||||
id: 'leanerDashboard.menu.signOut.label',
|
||||
id: 'learnerVariantDashboard.menu.signOut.label',
|
||||
defaultMessage: 'Sign Out',
|
||||
description: 'The label for the user menu Sign Out action.',
|
||||
},
|
||||
|
||||
goodMorning: {
|
||||
id: 'greeting.morning',
|
||||
defaultMessage: 'Good Morning!',
|
||||
description: 'Good Morning',
|
||||
course: {
|
||||
id: 'learnerVariantDashboard.course',
|
||||
defaultMessage: 'Courses',
|
||||
description: 'Header link for switching to dashboard page.',
|
||||
},
|
||||
goodAfternoon: {
|
||||
id: 'greeting.afternoon',
|
||||
defaultMessage: 'Good Afternoon!',
|
||||
description: 'Good Afternoon',
|
||||
},
|
||||
goodEvening: {
|
||||
id: 'greeting.evening',
|
||||
defaultMessage: 'Good Evening!',
|
||||
description: 'Good Evening',
|
||||
},
|
||||
switchToProgram: {
|
||||
id: 'leanerDashboard.switchToProgram',
|
||||
defaultMessage: 'Switch to Programs',
|
||||
program: {
|
||||
id: 'learnerVariantDashboard.program',
|
||||
defaultMessage: 'Programs',
|
||||
description: 'Header link for switching to program page.',
|
||||
},
|
||||
exploreCourses: {
|
||||
id: 'leanerDashboard.exploreCourses',
|
||||
defaultMessage: 'Explore courses',
|
||||
description: 'Header link for switching to course page.',
|
||||
discoverNew: {
|
||||
id: 'learnerVariantDashboard.discoverNew',
|
||||
defaultMessage: 'Discover New',
|
||||
description: 'Header link for switching to discover page.',
|
||||
},
|
||||
courseSearchAlt: {
|
||||
id: 'leanerDashboard.courseSearchAlt',
|
||||
defaultMessage: 'Course search',
|
||||
description: 'Alt-text for course search icon button',
|
||||
logoAltText: {
|
||||
id: 'learnerVariantDashboard.logoAltText',
|
||||
defaultMessage: 'edX, Inc. Dashboard',
|
||||
description: 'Alt text for the edX logo.',
|
||||
},
|
||||
collapseMenuOpenAltText: {
|
||||
id: 'learnerVariantDashboard.collapseMenuOpenAltText',
|
||||
defaultMessage: 'Menu',
|
||||
description: 'Alt text for the collapse menu icon when the menu is open.',
|
||||
},
|
||||
collapseMenuClosedAltText: {
|
||||
id: 'learnerVariantDashboard.collapseMenuClosedAltText',
|
||||
defaultMessage: 'Close',
|
||||
description: 'Alt text for the collapse menu icon when the menu is closed.',
|
||||
},
|
||||
career: {
|
||||
id: 'leanerDashboard.menu.career.label',
|
||||
defaultMessage: 'Career',
|
||||
description: 'The text for the user menu Career navigation link.',
|
||||
},
|
||||
newAlert: {
|
||||
id: 'header.menu.new.label',
|
||||
defaultMessage: 'New',
|
||||
description: 'The text announcing that an item in the user menu is New',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LearnerDashboardHeaderVariant render 1`] = `
|
||||
<Fragment>
|
||||
<ConfirmEmailBanner />
|
||||
<CollapsedHeader />
|
||||
<ExpandedHeader />
|
||||
<MasqueradeBar />
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import track from 'tracking';
|
||||
import { StrictDict } from 'utils';
|
||||
import { linkNames } from 'tracking/constants';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
isOpen: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useIsCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
const isCollapsed = React.useMemo(() => (width <= breakpoints.large.minWidth), [width]);
|
||||
return isCollapsed;
|
||||
};
|
||||
|
||||
export const findCoursesNavClicked = (href) => track.findCourses.findCoursesClicked(href, {
|
||||
linkName: linkNames.learnerHomeNavExplore,
|
||||
});
|
||||
|
||||
export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCoursesClicked(href, {
|
||||
linkName: linkNames.learnerHomeNavDropdownExplore,
|
||||
});
|
||||
|
||||
export const useLearnerDashboardHeaderVariantData = () => {
|
||||
const [isOpen, setIsOpen] = module.state.isOpen(false);
|
||||
const toggleIsOpen = () => setIsOpen(!isOpen);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
toggleIsOpen,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
useIsCollapsed,
|
||||
findCoursesNavClicked,
|
||||
findCoursesNavDropdownClicked,
|
||||
useLearnerDashboardHeaderVariantData,
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import track from 'tracking';
|
||||
import { linkNames } from 'tracking/constants';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const {
|
||||
useIsCollapsed,
|
||||
findCoursesNavClicked,
|
||||
findCoursesNavDropdownClicked,
|
||||
useLearnerDashboardHeaderVariantData,
|
||||
} = hooks;
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
findCourses: {
|
||||
findCoursesClicked: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const url = 'http://example.com';
|
||||
|
||||
describe('LearnerDashboardHeaderVariant hooks', () => {
|
||||
describe('state values', () => {
|
||||
state.testGetter(state.keys.isOpen);
|
||||
});
|
||||
|
||||
describe('useIsCollapsed', () => {
|
||||
test('large screen is not collapsed', () => {
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth + 1 });
|
||||
expect(useIsCollapsed()).toEqual(false);
|
||||
});
|
||||
test('small screen is collapsed', () => {
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth - 1 });
|
||||
expect(useIsCollapsed()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findCoursesNavClicked', () => {
|
||||
test('calls tracking with nav link name', () => {
|
||||
findCoursesNavClicked(url);
|
||||
expect(track.findCourses.findCoursesClicked).toHaveBeenCalledWith(url, {
|
||||
linkName: linkNames.learnerHomeNavExplore,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findCoursesNavDropdownClicked', () => {
|
||||
test('calls tracking with dropdown link name', () => {
|
||||
findCoursesNavDropdownClicked(url);
|
||||
expect(track.findCourses.findCoursesClicked).toHaveBeenCalledWith(url, {
|
||||
linkName: linkNames.learnerHomeNavDropdownExplore,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLearnerDashboardHeaderVariantData', () => {
|
||||
test('default state', () => {
|
||||
state.mock();
|
||||
const out = useLearnerDashboardHeaderVariantData();
|
||||
state.expectInitializedWith(state.keys.isOpen, false);
|
||||
out.toggleIsOpen();
|
||||
expect(state.values.isOpen).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import MasqueradeBar from 'containers/MasqueradeBar';
|
||||
import ConfirmEmailBanner from 'containers/LearnerDashboardHeader/ConfirmEmailBanner';
|
||||
|
||||
import CollapsedHeader from './CollapsedHeader';
|
||||
import ExpandedHeader from './ExpandedHeader';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const LearnerDashboardHeaderVariant = () => (
|
||||
<>
|
||||
<ConfirmEmailBanner />
|
||||
<CollapsedHeader />
|
||||
<ExpandedHeader />
|
||||
<MasqueradeBar />
|
||||
</>
|
||||
);
|
||||
|
||||
LearnerDashboardHeaderVariant.propTypes = {};
|
||||
|
||||
export default LearnerDashboardHeaderVariant;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user