Compare commits

...

43 Commits

Author SHA1 Message Date
Ghassan Maslamani
f92bd9c8f9 fix: force LMS url to reload when changed (#136) 2023-06-13 12:24:23 -03:00
leangseu-edx
5db95b0029 Revert "fix: stop user from unenroll after earned the certificate"
This reverts commit a479b7ead6.
2023-06-12 11:40:39 -04:00
Leangseu Kim
a479b7ead6 fix: stop user from unenroll after earned the certificate 2023-06-12 10:07:28 -04:00
Kris Hatcher
e43a49b431 feat: add career link to user dropdown (#152)
Co-authored-by: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com>
2023-06-05 11:50:40 -04:00
Mashal Malik
4643e0b130 refactor: update lock version file (#154) 2023-05-31 17:49:23 +05:00
Bilal Qamar
8c29abd0c8 feat: upgraded to node v18, added .nvmrc and updated workflows (#151) 2023-05-24 12:56:20 +05:00
Jason Wesson
d44b123815 fix: parse creditRequest data correctly and add ecommerce URL to envs (#150) 2023-05-19 11:46:02 -07:00
Jason Wesson
8829f756d8 fix: change ecommerce url reference 2023-05-19 15:29:40 +00:00
Jason Wesson
176a803f94 Merge branch 'master' into jwesson/fix-purchase-credit 2023-05-18 11:22:33 -07:00
Jason Wesson
309a07ffa9 fix: add ecommerce url env to env.prod 2023-05-18 18:11:34 +00:00
Jason Wesson
e3784d36f1 refactor: extract data closer to origin of API request 2023-05-18 18:09:16 +00:00
Jason Wesson
5048fffd04 fix: parse creditRequest data correctly and add ecommerce URL to dev and test envs 2023-05-17 20:19:09 +00:00
Jenkins
5ca3036849 chore(i18n): update translations 2023-05-14 12:46:46 -04:00
Leangseu Kim
e57f44068b fix: missing image 2023-05-10 11:01:58 -04:00
leangseu-edx
a4d10b6c72 chore: refactor disable course action into a single hook (#145) 2023-05-09 14:30:44 -04:00
Omar Al-Ithawi
5769629250 feat: use atlas in make pull_translations (#137)
- Bump frontend-platform to bring intl-imports.js script
 - Move all i18n imports into `src/i18n/index.js` so intl-imports.js can override it with latest translations
 - Add `atlas` into `make pull_translations` when `OPENEDX_ATLAS_PULL` environment variable is set.

This pull request is part of the [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) which is sparked by the [Translation Infrastructure update OEP-58](https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0058-arch-translations-management.html#specification).
2023-05-09 10:05:48 -04:00
Jenkins
a59ff5e7e8 chore(i18n): update translations 2023-05-07 12:46:45 -04:00
Leangseu Kim
9a9c0583ca chore: update zendesk chat department 2023-05-03 12:47:06 -04:00
Jenkins
2f409e5168 chore(i18n): update translations 2023-04-30 12:46:45 -04:00
Hamzah Ullah
cf35c7d611 fix: account for isLearnerPortalEnabled when determining hasAvailableDashboards (#141) 2023-04-28 11:29:44 -04:00
Hamzah Ullah
4a2eee2a1d chore: update test and snapshot 2023-04-28 11:09:07 -04:00
Adam Stankiewicz
0ed2b10b13 fix: change button case for enterpriseDialogConfirmButton 2023-04-26 16:57:41 -04:00
Adam Stankiewicz
01f67265f6 chore: remove console.log 2023-04-26 16:40:20 -04:00
Adam Stankiewicz
8a73043368 fix: account for isLearnerPortalEnabled when determining hasAvailableDashboards 2023-04-26 16:00:38 -04:00
Jansen Kantor
b09c36e13e fix: noticeswrapper api response error (#139) 2023-04-21 10:09:22 -04:00
Jansen Kantor
14f7389900 Jkantor/notices (#134)
Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
2023-04-19 14:35:46 -04:00
Leangseu Kim
895e867b91 fix: make credit actions disable on masquerade 2023-04-19 11:55:05 -04:00
Leangseu Kim
6bc60bad33 chore: remove incorrect information in unenroll popup 2023-04-19 09:25:58 -04:00
leangseu-edx
5e716ece2d feat: Remove Learner Home from web crawling (#133) 2023-04-13 11:35:32 -04:00
Ben Warzeski
320f6acc21 fix: show cert for not-passing courses and hide link when missing URL (#131) 2023-04-10 10:07:02 -04:00
Jenkins
af51373e2c chore(i18n): update translations 2023-04-09 12:46:42 -04:00
Mubbshar Anwar
5dd00e9f24 refactor: update API endpoint (#129) 2023-04-06 08:39:27 -04:00
Ben Warzeski
63eaa00ee1 Bw/fix network args (#130) 2023-03-30 10:51:30 -04:00
Leangseu Kim
e25610c66e fix: disable title link on homeUrl undefined 2023-03-23 10:13:52 -04:00
Leangseu Kim
5724d051b2 fix: begin button disable when audit access expire 2023-03-21 16:06:18 -04:00
Leangseu Kim
9a5ac5ddf7 chore: add es, fr_ca, pt_BR translation
chore: remove intl as it is no longer needed

chore: update test snaphot
2023-03-17 13:56:40 -04:00
Leangseu Kim
145c18d9ed fix: credit cors error 2023-03-16 11:32:24 -04:00
Jenkins
b4bb924659 chore(i18n): update translations 2023-03-14 10:42:13 -04:00
Leangseu Kim
45e8113553 chore: empty out translation file 2023-03-13 11:09:26 -04:00
Mashal Malik
cfb9bfdb6b Update transifex api from v2 to v3 (#118) 2023-03-13 10:58:34 -04:00
Nathan Sprenkle
6a73054a9c fix: update links to new MFE experiences in header (#121) 2023-03-08 01:23:32 -05:00
Nathan Sprenkle
5d88e8d1ec fix: update menu item link for variant header (#120) 2023-03-07 18:20:23 -05:00
Nathan Sprenkle
19d7aa3e33 fix: update menu item link for account and profile (#119) 2023-03-07 17:48:34 -05:00
141 changed files with 7511 additions and 31668 deletions

5
.env
View File

@@ -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=''

View File

@@ -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=''

View File

@@ -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=''

View File

@@ -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': ['*'] } }],
},
});

View File

@@ -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

View File

@@ -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

View File

@@ -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

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18.15

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -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
? (

View File

@@ -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);

View File

@@ -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"

View File

@@ -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>
`;

View 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 };

View 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);
});
});
});
});

View 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;

View 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);
});
});
});
});

View 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;

View 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);
});
});
});

View File

@@ -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 {

View File

@@ -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: [

View File

@@ -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 = {};

View File

@@ -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}

View File

@@ -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);
});
});
});

View File

@@ -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}

View File

@@ -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);
});
});
});

View File

@@ -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)}

View File

@@ -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);
});
});

View File

@@ -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>

View File

@@ -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);

View File

@@ -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}

View File

@@ -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);
});
});

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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>
))}

View File

@@ -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,
{

View File

@@ -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,

View File

@@ -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} />),

View File

@@ -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>,
}),
);
});

View File

@@ -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 })}
/>

View File

@@ -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(

View File

@@ -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({

View File

@@ -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', () => {

View File

@@ -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"

View File

@@ -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 };
};

View File

@@ -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);
});
});
});

View File

@@ -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"

View File

@@ -41,7 +41,7 @@ describe('CourseCardDetails hooks', () => {
};
const entitlementData = {
isEntitlement: false,
canViewCourse: false,
disableViewCourse: false,
isFulfilled: false,
isExpired: false,
canChange: false,

View File

@@ -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

View File

@@ -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);
});
});
});

View File

@@ -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>
);
};

View File

@@ -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);
});
});
});

View File

@@ -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);

View File

@@ -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>
`;

View File

@@ -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>
`;

View 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;

View 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);
});
});
});

View File

@@ -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"
>

View File

@@ -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)}

View File

@@ -6,7 +6,7 @@ import EmptyCourse from '.';
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: 'course-search-url',
courseSearchUrl: '/course-search-url',
})),
},
}));

View File

@@ -34,7 +34,7 @@ exports[`EnterpriseDashboard snapshot 1`] = `
onClick={[MockFunction useEnterpriseDashboardHook.handleCTAClick]}
type="a"
>
Go To Dashboard
Go to dashboard
</Button>
</ActionRow>
</div>

View File

@@ -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',
},
});

View File

@@ -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;

View File

@@ -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();
});
});
});

View File

@@ -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/${

View File

@@ -17,7 +17,7 @@ jest.mock('hooks', () => ({
url: 'url',
}),
usePlatformSettingsData: () => ({
courseSearchUrl: 'courseSearchUrl',
courseSearchUrl: '/courseSearchUrl',
}),
},
}));

View File

@@ -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

View File

@@ -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 && (

View File

@@ -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'),
});

View File

@@ -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 && (

View File

@@ -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',

View File

@@ -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>

View File

@@ -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

View File

@@ -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}

View File

@@ -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',
}),
},
}));

View File

@@ -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;

View File

@@ -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();
});
});
});
});

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -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,
};

View File

@@ -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);
});
});
});

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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);
});
});

View File

@@ -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',
},
});

View File

@@ -1,10 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LearnerDashboardHeaderVariant render 1`] = `
<Fragment>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</Fragment>
`;

View File

@@ -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,
};

View File

@@ -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);
});
});
});

View File

@@ -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