Compare commits
80 Commits
mfrank/tes
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6212c8eecf | ||
|
|
a580d245c0 | ||
|
|
ca954e139d | ||
|
|
0d2eb96c86 | ||
|
|
f1180bffde | ||
|
|
f93598a9e4 | ||
|
|
35680b8b05 | ||
|
|
5d1000530d | ||
|
|
eef5d3f671 | ||
|
|
8fc839dc3d | ||
|
|
b21e6e553d | ||
|
|
cfc82975d8 | ||
|
|
50ea19d6af | ||
|
|
9e61ae677e | ||
|
|
247794b21d | ||
|
|
0a50937b4c | ||
|
|
22a1c658f1 | ||
|
|
75396f1dab | ||
|
|
62099a50eb | ||
|
|
19ccb8ab87 | ||
|
|
a3e2c80537 | ||
|
|
324cb525c6 | ||
|
|
f14ab8851d | ||
|
|
c277150716 | ||
|
|
2a0ed5714f | ||
|
|
1f0b758705 | ||
|
|
85a5a6e94e | ||
|
|
f59b5013c8 | ||
|
|
86b6574c60 | ||
|
|
c38e80505c | ||
|
|
b926e13c01 | ||
|
|
0a4285aad3 | ||
|
|
e70fa29261 | ||
|
|
b6ba8fb366 | ||
|
|
2221655950 | ||
|
|
0db621b134 | ||
|
|
aaf2e36fe9 | ||
|
|
b527fbcfba | ||
|
|
fa3f7b27cf | ||
|
|
3b0c58f376 | ||
|
|
f903392ca1 | ||
|
|
89032f09f4 | ||
|
|
03f146cce9 | ||
|
|
7dd2bda86e | ||
|
|
410f2f730f | ||
|
|
de13749443 | ||
|
|
0e66b2031d | ||
|
|
697c9ff2c8 | ||
|
|
fdb5d2f68c | ||
|
|
7cf79f8931 | ||
|
|
73b7b7f5d0 | ||
|
|
516544c7cc | ||
|
|
2134df5478 | ||
|
|
5345d2eef2 | ||
|
|
d94ac8dd62 | ||
|
|
ff925b06f1 | ||
|
|
2696486e5b | ||
|
|
5854b00d08 | ||
|
|
13e9b1a85f | ||
|
|
8116956a4b | ||
|
|
8d23e7585b | ||
|
|
b63a40006e | ||
|
|
92243063b9 | ||
|
|
a2673399aa | ||
|
|
cedcb4172d | ||
|
|
9e4faf1569 | ||
|
|
3a9acc981b | ||
|
|
e3c18698d8 | ||
|
|
eb38beedc3 | ||
|
|
01faf5a30a | ||
|
|
f4807614e2 | ||
|
|
ab448e52f2 | ||
|
|
3383016176 | ||
|
|
054cd57d4b | ||
|
|
0fc3cc4d53 | ||
|
|
e5c1244e59 | ||
|
|
3a389e14e1 | ||
|
|
6de409d7cc | ||
|
|
517b8424b3 | ||
|
|
49e527d810 |
3
.env
3
.env
@@ -1,4 +1,5 @@
|
||||
NODE_ENV='production'
|
||||
APP_ID='learner-dashboard'
|
||||
NODE_PATH=./src
|
||||
BASE_URL=''
|
||||
LMS_BASE_URL=''
|
||||
@@ -37,10 +38,10 @@ HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
ACCOUNT_PROFILE_URL=''
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
SHOW_UNENROLL_SURVEY=true
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
NODE_ENV='development'
|
||||
APP_ID='learner-dashboard'
|
||||
PORT=1996
|
||||
BASE_URL='localhost:1996'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
@@ -43,10 +44,10 @@ HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
SHOW_UNENROLL_SURVEY=true
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
NODE_ENV='test'
|
||||
APP_ID='learner-dashboard'
|
||||
PORT=1996
|
||||
BASE_URL='localhost:1996'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
@@ -42,9 +43,9 @@ 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=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
SHOW_UNENROLL_SURVEY=true
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
* @openedx/2U-aperture
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -29,6 +29,9 @@ jobs:
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Type check
|
||||
run: npm run types
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
11
Makefile
11
Makefile
@@ -12,6 +12,11 @@ transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
# Variables for additional translation sources and imports (define in edx-internal if needed)
|
||||
ATLAS_EXTRA_SOURCES ?=
|
||||
ATLAS_EXTRA_INTL_IMPORTS ?=
|
||||
ATLAS_OPTIONS ?=
|
||||
|
||||
.PHONY: test
|
||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||
|
||||
@@ -47,10 +52,12 @@ pull_translations:
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
|
||||
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard \
|
||||
$(ATLAS_EXTRA_SOURCES)
|
||||
|
||||
$(intl_imports) frontend-platform paragon frontend-component-footer frontend-app-learner-dashboard
|
||||
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-learner-dashboard $(ATLAS_EXTRA_INTL_IMPORTS)
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
18
README.rst
18
README.rst
@@ -39,22 +39,12 @@ The parts of this MFE that can be customized in that manner are documented `here
|
||||
Contributing
|
||||
------------
|
||||
|
||||
A core goal of this app is to provide a clean experimentation interface. To promote this end, we have provided a
|
||||
silo'ed code directory at ``src/widgets`` in which contributors should add their custom widget components. In order to
|
||||
ensure our ability to maintain the code stability of the app, the code for these widgets should be strictly contained
|
||||
within the bounds of that directory.
|
||||
Contributions are very welcome. Please read `So you want to contribute to Open edX <https://docs.openedx.org/en/latest/developers/quickstarts/so_you_want_to_contribute.html>`_ for details on how to get started as an Open edX contributor.
|
||||
|
||||
Once written, the widgets can be configured into one of our widget containers at ``src/containers/WidgetContainers``.
|
||||
This can include conditional logic, as well as Optimizely triggers. It is important to note that our integration tests
|
||||
will isolate and ignore these containers, and thus testing your widget is the response of the creator/maintainer of the
|
||||
widget itself.
|
||||
This project is currently accepting all types of contributions — bug fixes, security fixes, maintenance work, or new features.
|
||||
However, if you intend to add a new feature, make sure it has gone through the `Product Review process <https://openedx.atlassian.net/wiki/spaces/COMM/pages/3875962884/How+to+submit+an+open+source+contribution+for+Product+Review>`_.
|
||||
|
||||
Some guidelines for writing widgets:
|
||||
|
||||
* Code for the widget should be strictly confined to the ``src/widgets`` directory.
|
||||
* You can load data from the redux store, but should not add or modify fields in that structure.
|
||||
* Network events should be managed in component hooks, though can use our ``data/constants/requests:requestStates`` for
|
||||
ease of tracking the request states.
|
||||
When proposing a change, create an issue in this repo to get the discussion started.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
@@ -19,6 +19,7 @@ frontend-platform's getConfig loads configuration in the following sequence:
|
||||
|
||||
module.exports = {
|
||||
NODE_ENV: 'development',
|
||||
APP_ID: 'learner-dashboard',
|
||||
NODE_PATH: './src',
|
||||
PORT: 1996,
|
||||
BASE_URL: 'localhost:1996',
|
||||
@@ -67,7 +68,7 @@ module.exports = {
|
||||
NEW_RELIC_LICENSE_KEY: '',
|
||||
ACCOUNT_SETTINGS_URL: 'http://localhost:1997',
|
||||
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
|
||||
ENABLE_NOTICES: '',
|
||||
CAREER_LINK_URL: '',
|
||||
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
|
||||
SHOW_UNENROLL_SURVEY: true
|
||||
};
|
||||
|
||||
6108
package-lock.json
generated
6108
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -19,7 +19,8 @@
|
||||
"dev": "PUBLIC_PATH=/learner-dashboard/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"quality": "npm run lint-fix && npm run test",
|
||||
"watch-tests": "jest --watch"
|
||||
"watch-tests": "jest --watch",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -30,22 +31,19 @@
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-component-header": "^8.0.0",
|
||||
"@edx/frontend-enterprise-hotjar": "7.2.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.0.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||
"@fortawesome/react-fontawesome": "^3.0.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^23.4.5",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.42.0",
|
||||
"filesize": "^10.0.0",
|
||||
"core-js": "3.48.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -55,32 +53,25 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "6.8.9",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "6.29.0",
|
||||
"react-share": "^4.4.0",
|
||||
"redux": "4.2.1",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.4.2",
|
||||
"react-router-dom": "6.30.3",
|
||||
"react-share": "^5.2.2",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"reselect": "^4.0.0",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"util": "^0.12.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.3.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@edx/typescript-config": "^1.1.0",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"copy-webpack-plugin": "^12.0.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jest-when": "^3.6.0",
|
||||
"react-dev-utils": "^12.0.0",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"redux-mock-store": "^1.5.4"
|
||||
"react-test-renderer": "^18.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
50
src/App.jsx
50
src/App.jsx
@@ -5,60 +5,30 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
||||
|
||||
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import store from 'data/store';
|
||||
import {
|
||||
selectors,
|
||||
actions,
|
||||
} from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
|
||||
import track from 'tracking';
|
||||
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
|
||||
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
|
||||
import AppWrapper from 'containers/AppWrapper';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import { useMasquerade } from 'data/context';
|
||||
import messages from './messages';
|
||||
import './App.scss';
|
||||
|
||||
export const App = () => {
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
const { formatMessage } = useIntl();
|
||||
const isFailed = {
|
||||
initialize: reduxHooks.useRequestIsFailed(RequestKeys.initialize),
|
||||
refreshList: reduxHooks.useRequestIsFailed(RequestKeys.refreshList),
|
||||
};
|
||||
const hasNetworkFailure = isFailed.initialize || isFailed.refreshList;
|
||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
||||
const loadData = reduxHooks.useLoadData();
|
||||
const { masqueradeUser } = useMasquerade();
|
||||
const { data, isError } = useInitializeLearnerHome();
|
||||
const hasNetworkFailure = !masqueradeUser && isError;
|
||||
const supportEmail = data?.platformSettings?.supportEmail || undefined;
|
||||
|
||||
/* istanbul ignore next */
|
||||
React.useEffect(() => {
|
||||
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
|
||||
window.loadEmptyData = () => {
|
||||
loadData({ ...fakeData.globalData, courses: [] });
|
||||
};
|
||||
window.loadMockData = () => {
|
||||
loadData({
|
||||
...fakeData.globalData,
|
||||
courses: [
|
||||
...fakeData.courseRunData,
|
||||
...fakeData.entitlementData,
|
||||
],
|
||||
});
|
||||
};
|
||||
window.store = store;
|
||||
window.selectors = selectors;
|
||||
window.actions = actions;
|
||||
window.track = track;
|
||||
}
|
||||
if (getConfig().HOTJAR_APP_ID) {
|
||||
try {
|
||||
initializeHotjar({
|
||||
@@ -70,7 +40,7 @@ export const App = () => {
|
||||
logError(error);
|
||||
}
|
||||
}
|
||||
}, [authenticatedUser, loadData]);
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
|
||||
@@ -3,44 +3,43 @@ import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('data/context', () => ({
|
||||
useMasquerade: jest.fn(() => ({ masqueradeUser: null })),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => ({
|
||||
FooterSlot: jest.fn(() => <div>FooterSlot</div>),
|
||||
}));
|
||||
jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
|
||||
jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => <div>LearnerDashboardHeader</div>));
|
||||
jest.mock('containers/WidgetContainers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: 'redux.selectors',
|
||||
actions: 'redux.actions',
|
||||
thunkActions: 'redux.thunkActions',
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useRequestIsFailed: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
useLoadData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('data/store', () => 'data/store');
|
||||
jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('react');
|
||||
|
||||
const loadData = jest.fn();
|
||||
reduxHooks.useLoadData.mockReturnValue(loadData);
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/react'),
|
||||
ErrorPage: () => 'ErrorPage',
|
||||
}));
|
||||
|
||||
const supportEmail = 'test@support.com';
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
||||
useInitializeLearnerHome.mockReturnValue({
|
||||
data: {
|
||||
platformSettings: {
|
||||
supportEmail,
|
||||
},
|
||||
},
|
||||
isError: false,
|
||||
});
|
||||
|
||||
describe('App router component', () => {
|
||||
describe('component', () => {
|
||||
@@ -65,7 +64,6 @@ describe('App router component', () => {
|
||||
describe('no network failure', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({});
|
||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||
});
|
||||
@@ -78,7 +76,6 @@ describe('App router component', () => {
|
||||
describe('no network failure with optimizely url', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
|
||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||
});
|
||||
@@ -91,7 +88,6 @@ describe('App router component', () => {
|
||||
describe('no network failure with optimizely project id', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
|
||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||
});
|
||||
@@ -104,7 +100,10 @@ describe('App router component', () => {
|
||||
describe('initialize failure', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
|
||||
useInitializeLearnerHome.mockReturnValue({
|
||||
data: null,
|
||||
isError: true,
|
||||
});
|
||||
getConfig.mockReturnValue({});
|
||||
render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
|
||||
});
|
||||
@@ -118,7 +117,6 @@ describe('App router component', () => {
|
||||
});
|
||||
describe('refresh failure', () => {
|
||||
beforeEach(() => {
|
||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
|
||||
getConfig.mockReturnValue({});
|
||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import Banner from './Banner';
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
describe('Banner component', () => {
|
||||
it('renders children content', () => {
|
||||
render(<Banner>Test content</Banner>);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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 getNotices = ({ onLoad, notFoundMessage }) => {
|
||||
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}. ${notFoundMessage}`);
|
||||
} else {
|
||||
logError(e);
|
||||
}
|
||||
};
|
||||
if (authenticatedUser) {
|
||||
return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default { getNotices };
|
||||
@@ -1,65 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { getNotices } from './api';
|
||||
import * as module from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* 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();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
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}`);
|
||||
}
|
||||
},
|
||||
notFoundMessage: formatMessage(messages.error404Message),
|
||||
});
|
||||
}
|
||||
}, [setIsRedirected, formatMessage]);
|
||||
return { isRedirected };
|
||||
};
|
||||
|
||||
export default useNoticesWrapperData;
|
||||
@@ -1,89 +0,0 @@
|
||||
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() }));
|
||||
const mockFormatMessage = jest.fn(message => message.defaultMessage || 'translated-string');
|
||||
jest.mock('react-intl', () => ({
|
||||
useIntl: () => ({
|
||||
formatMessage: mockFormatMessage,
|
||||
}),
|
||||
}));
|
||||
|
||||
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, mockFormatMessage]);
|
||||
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, mockFormatMessage]);
|
||||
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, mockFormatMessage]);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
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;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
import NoticesWrapper from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = { isRedirected: false };
|
||||
|
||||
const children = [<b key={1}>some</b>, <i key={2}>children</i>];
|
||||
describe('NoticesWrapper component', () => {
|
||||
beforeEach(() => {
|
||||
useNoticesWrapperData.mockClear();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
useNoticesWrapperData.mockReturnValue(hookProps);
|
||||
render(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(useNoticesWrapperData).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('does not show children if redirected', () => {
|
||||
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
|
||||
render(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(screen.queryByText('some')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('children')).not.toBeInTheDocument();
|
||||
});
|
||||
it('shows children if not redirected', () => {
|
||||
useNoticesWrapperData.mockReturnValue(hookProps);
|
||||
render(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(screen.getByText('some')).toBeInTheDocument();
|
||||
expect(screen.getByText('children')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
error404Message: {
|
||||
id: 'learner-dash.notices.error404Message',
|
||||
defaultMessage: 'This probably happened because the notices plugin is not installed on platform.',
|
||||
description: 'Error message when notices API returns 404',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,5 +1,6 @@
|
||||
const configuration = {
|
||||
// BASE_URL: process.env.BASE_URL,
|
||||
APP_ID: process.env.APP_ID,
|
||||
LMS_BASE_URL: process.env.LMS_BASE_URL,
|
||||
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
|
||||
CREDIT_PURCHASE_URL: process.env.CREDIT_PURCHASE_URL,
|
||||
@@ -14,13 +15,13 @@ const configuration = {
|
||||
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
|
||||
SUPPORT_URL: process.env.SUPPORT_URL || null,
|
||||
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
||||
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
|
||||
LOGO_URL: process.env.LOGO_URL,
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
|
||||
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
|
||||
ENABLE_PROGRAMS: process.env.ENABLE_PROGRAMS === 'true',
|
||||
NON_BROWSABLE_COURSES: process.env.NON_BROWSABLE_COURSES === 'true',
|
||||
SHOW_UNENROLL_SURVEY: process.env.SHOW_UNENROLL_SURVEY === 'true',
|
||||
};
|
||||
|
||||
const features = {};
|
||||
|
||||
15
src/containers/AppWrapper/index.test.tsx
Normal file
15
src/containers/AppWrapper/index.test.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import AppWrapper from './index';
|
||||
|
||||
describe('AppWrapper', () => {
|
||||
it('should render children without modification', () => {
|
||||
render(
|
||||
<AppWrapper>
|
||||
<div>Test Child</div>
|
||||
</AppWrapper>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Child')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,33 @@
|
||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||
import useIsCollapsed from './hooks';
|
||||
|
||||
jest.mock('@openedx/paragon', () => ({
|
||||
...jest.requireActual('@openedx/paragon'),
|
||||
useWindowSize: jest.fn(),
|
||||
breakpoints: {
|
||||
extraSmall: {
|
||||
minWidth: 0,
|
||||
maxWidth: 575,
|
||||
},
|
||||
small: {
|
||||
minWidth: 576,
|
||||
maxWidth: 767,
|
||||
},
|
||||
medium: {
|
||||
minWidth: 768,
|
||||
maxWidth: 991,
|
||||
},
|
||||
large: {
|
||||
minWidth: 992,
|
||||
maxWidth: 1199,
|
||||
},
|
||||
extraLarge: {
|
||||
minWidth: 1200,
|
||||
maxWidth: 100000,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useIsCollapsed', () => {
|
||||
it('returns true only when it is between medium and small', () => {
|
||||
// make sure all three breakpoints gap is large enough for test
|
||||
|
||||
@@ -5,8 +5,6 @@ import useIsCollapsed from './hooks';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
|
||||
describe('ActionButton', () => {
|
||||
const props = {
|
||||
className: 'custom-class',
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/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 execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||
const { data: learnerData } = useInitializeLearnerHome();
|
||||
const courseData = useCourseData(cardId);
|
||||
const homeUrl = courseData?.courseRun?.homeUrl;
|
||||
const execEdTrackingParam = useMemo(() => {
|
||||
const isExecEd2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(courseData.enrollment.mode);
|
||||
const { authOrgId } = learnerData.enterpriseDashboard || {};
|
||||
return isExecEd2UCourse ? `?org_id=${authOrgId}` : '';
|
||||
}, [courseData.enrollment.mode, learnerData.enterpriseDashboard]);
|
||||
const { disableBeginCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
const handleClick = useCourseTrackingEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl + execEdTrackingParam,
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import track from 'tracking';
|
||||
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn().mockReturnValue({
|
||||
enrollment: { mode: 'executive-education' },
|
||||
courseRun: { homeUrl: 'home-url' },
|
||||
}),
|
||||
useCourseTrackingEvent: jest.fn().mockReturnValue({
|
||||
trackCourseEvent: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||
data: {
|
||||
enterpriseDashboard: {
|
||||
authOrgId: 'test-org-id',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
@@ -13,44 +32,29 @@ jest.mock('tracking', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
||||
|
||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||
|
||||
const homeUrl = 'home-url';
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||
);
|
||||
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
|
||||
const renderComponent = () => render(<IntlProvider locale="en"><BeginCourseButton {...props} /></IntlProvider>);
|
||||
|
||||
describe('BeginCourseButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('initiliaze hooks', () => {
|
||||
it('initializes course run data with cardId', () => {
|
||||
render(<BeginCourseButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads exec education path param', () => {
|
||||
render(<BeginCourseButton {...props} />);
|
||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||
renderComponent();
|
||||
expect(useCourseData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads disabled states for begin action from action hooks', () => {
|
||||
render(<BeginCourseButton {...props} />);
|
||||
renderComponent();
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
@@ -58,7 +62,7 @@ describe('BeginCourseButton', () => {
|
||||
describe('disabled', () => {
|
||||
it('should be disabled', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
|
||||
render(<BeginCourseButton {...props} />);
|
||||
renderComponent();
|
||||
const button = screen.getByRole('button', { name: 'Begin Course' });
|
||||
expect(button).toHaveClass('disabled');
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true');
|
||||
@@ -66,20 +70,20 @@ describe('BeginCourseButton', () => {
|
||||
});
|
||||
describe('enabled', () => {
|
||||
it('should be enabled', () => {
|
||||
render(<BeginCourseButton {...props} />);
|
||||
renderComponent();
|
||||
const button = screen.getByRole('button', { name: 'Begin Course' });
|
||||
expect(button).not.toHaveClass('disabled');
|
||||
expect(button).not.toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
it('should track enter course clicked event on click, with exec ed param', async () => {
|
||||
render(<BeginCourseButton {...props} />);
|
||||
it('should track enter course clicked event on click, with exec ed param', () => {
|
||||
renderComponent();
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button', { name: 'Begin Course' });
|
||||
user.click(button);
|
||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
||||
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
homeUrl + execEdPath(props.cardId),
|
||||
`${homeUrl}?org_id=test-org-id`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseTrackingEvent, useCourseData } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const ResumeButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||
const { data: learnerData } = useInitializeLearnerHome();
|
||||
const courseData = useCourseData(cardId);
|
||||
const resumeUrl = courseData?.courseRun?.resumeUrl;
|
||||
const execEdTrackingParam = useMemo(() => {
|
||||
const isExecEd2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(courseData.enrollment.mode);
|
||||
const { authOrgId } = learnerData.enterpriseDashboard || {};
|
||||
return isExecEd2UCourse ? `?org_id=${authOrgId}` : '';
|
||||
}, [courseData.enrollment.mode, learnerData.enterpriseDashboard]);
|
||||
const { disableResumeCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
const handleClick = useCourseTrackingEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
resumeUrl + execEdTrackingParam,
|
||||
|
||||
@@ -1,47 +1,56 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useCourseTrackingEvent, useCourseData } from 'hooks';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ResumeButton from './ResumeButton';
|
||||
|
||||
const authOrgId = 'auth-org-id';
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||
data: {
|
||||
enterpriseDashboard: {
|
||||
authOrgId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn().mockReturnValue({
|
||||
enrollment: { mode: 'executive-education' },
|
||||
courseRun: { homeUrl: 'home-url' },
|
||||
}),
|
||||
useCourseTrackingEvent: jest.fn().mockReturnValue({
|
||||
trackCourseEvent: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
||||
jest.unmock('@openedx/paragon');
|
||||
|
||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||
|
||||
const resumeUrl = 'resume-url';
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
|
||||
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
|
||||
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
|
||||
reduxHooks.useTrackCourseEvent.mockImplementation(
|
||||
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||
);
|
||||
useCourseData.mockReturnValue({
|
||||
enrollment: { mode: 'executive-education' },
|
||||
courseRun: { resumeUrl: 'home-url' },
|
||||
});
|
||||
|
||||
describe('ResumeButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
describe('initialize hooks', () => {
|
||||
beforeEach(() => render(<ResumeButton {...props} />));
|
||||
beforeEach(() => render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>));
|
||||
it('initializes course run data with cardId', () => {
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads exec education path param', () => {
|
||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||
expect(useCourseData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads disabled states for resume action from action hooks', () => {
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
@@ -53,7 +62,7 @@ describe('ResumeButton', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
|
||||
});
|
||||
it('should be disabled', () => {
|
||||
render(<ResumeButton {...props} />);
|
||||
render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: 'Resume' });
|
||||
expect(button).toHaveClass('disabled');
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true');
|
||||
@@ -61,21 +70,21 @@ describe('ResumeButton', () => {
|
||||
});
|
||||
describe('enabled', () => {
|
||||
it('should be enabled', () => {
|
||||
render(<ResumeButton {...props} />);
|
||||
render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: 'Resume' });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).not.toHaveClass('disabled');
|
||||
expect(button).not.toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
it('should track enter course clicked event on click, with exec ed param', async () => {
|
||||
render(<ResumeButton {...props} />);
|
||||
render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button', { name: 'Resume' });
|
||||
user.click(button);
|
||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
||||
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
resumeUrl + execEdPath(props.cardId),
|
||||
`home-url?org_id=${authOrgId}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useSelectSessionModal } from 'data/context';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
@@ -11,11 +11,11 @@ import messages from './messages';
|
||||
export const SelectSessionButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { disableSelectSession } = useActionDisabledState(cardId);
|
||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
const { updateSelectSessionModal } = useSelectSessionModal();
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={disableSelectSession}
|
||||
onClick={openSessionModal}
|
||||
onClick={() => updateSelectSessionModal(cardId)}
|
||||
>
|
||||
{formatMessage(messages.selectSession)}
|
||||
</ActionButton>
|
||||
|
||||
@@ -1,43 +1,47 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useSelectSessionModal } from 'data/context';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useUpdateSelectSessionModalCallback: jest.fn(),
|
||||
},
|
||||
jest.mock('data/context', () => ({
|
||||
useSelectSessionModal: jest.fn().mockReturnValue({
|
||||
updateSelectSessionModal: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false })));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||
|
||||
describe('SelectSessionButton', () => {
|
||||
const props = { cardId: 'cardId' };
|
||||
it('default render', () => {
|
||||
render(<SelectSessionButton {...props} />);
|
||||
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: 'Select Session' });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
describe('if useActionDisabledState is false', () => {
|
||||
it('should disabled Select Session', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true });
|
||||
render(<SelectSessionButton {...props} />);
|
||||
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: 'Select Session' });
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
describe('on click', () => {
|
||||
it('should call openSessionModal', async () => {
|
||||
render(<SelectSessionButton {...props} />);
|
||||
const mockedUpdateSelectSessionModal = jest.fn();
|
||||
useSelectSessionModal.mockReturnValue({
|
||||
updateSelectSessionModal: mockedUpdateSelectSessionModal,
|
||||
});
|
||||
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button', { name: 'Select Session' });
|
||||
await user.click(button);
|
||||
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(props.cardId);
|
||||
expect(mockedUpdateSelectSessionModal).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,17 +4,18 @@ import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseTrackingEvent, useCourseData } 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 courseData = useCourseData(cardId);
|
||||
const homeUrl = courseData?.courseRun?.homeUrl;
|
||||
const { disableViewCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
const handleClick = useCourseTrackingEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useCourseTrackingEvent } from 'hooks';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn().mockReturnValue({
|
||||
courseRun: { homeUrl: 'homeUrl' },
|
||||
}),
|
||||
useCourseTrackingEvent: jest.fn().mockReturnValue({
|
||||
trackCourseEvent: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||
|
||||
const defaultProps = { cardId: 'cardId' };
|
||||
@@ -28,26 +31,29 @@ const homeUrl = 'homeUrl';
|
||||
|
||||
describe('ViewCourseButton', () => {
|
||||
it('learner can view course', async () => {
|
||||
render(<ViewCourseButton {...defaultProps} />);
|
||||
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: 'View Course' });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).not.toHaveClass('disabled');
|
||||
expect(button).not.toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
it('calls trackCourseEvent on click', async () => {
|
||||
render(<ViewCourseButton {...defaultProps} />);
|
||||
const mockedTrackCourseEvent = jest.fn();
|
||||
useCourseTrackingEvent.mockReturnValue(mockedTrackCourseEvent);
|
||||
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button', { name: 'View Course' });
|
||||
await user.click(button);
|
||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
||||
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||
track.course.enterCourseClicked,
|
||||
defaultProps.cardId,
|
||||
homeUrl,
|
||||
);
|
||||
expect(mockedTrackCourseEvent).toHaveBeenCalled();
|
||||
});
|
||||
it('learner cannot view course', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
|
||||
render(<ViewCourseButton {...defaultProps} />);
|
||||
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: 'View Course' });
|
||||
expect(button).toHaveClass('disabled');
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { ActionRow } from '@openedx/paragon';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useEntitlementInfo } from 'hooks';
|
||||
|
||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
@@ -12,11 +12,10 @@ import ResumeButton from './ResumeButton';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
export const CourseCardActions = ({ cardId }) => {
|
||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const {
|
||||
hasStarted,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const cardData = useCourseData(cardId);
|
||||
const hasStarted = cardData.enrollment.hasStarted || false;
|
||||
const { isEntitlement, isFulfilled } = useEntitlementInfo(cardData);
|
||||
const isArchived = cardData.courseRun.isArchived || false;
|
||||
|
||||
return (
|
||||
<ActionRow data-test-id="CourseCardActions">
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { useCourseData } from 'hooks';
|
||||
import CourseCardActions from '.';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
...jest.requireActual('hooks'),
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('plugin-slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
|
||||
@@ -18,34 +13,28 @@ jest.mock('./ViewCourseButton', () => jest.fn(() => <div>ViewCourseButton</div>)
|
||||
jest.mock('./BeginCourseButton', () => jest.fn(() => <div>BeginCourseButton</div>));
|
||||
jest.mock('./ResumeButton', () => jest.fn(() => <div>ResumeButton</div>));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const props = { cardId };
|
||||
|
||||
describe('CourseCardActions', () => {
|
||||
const mockHooks = ({
|
||||
isEntitlement = false,
|
||||
isExecEd2UCourse = false,
|
||||
isFulfilled = false,
|
||||
isArchived = false,
|
||||
isVerified = false,
|
||||
hasStarted = false,
|
||||
isMasquerading = false,
|
||||
} = {}) => {
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted });
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
useCourseData.mockReturnValueOnce({
|
||||
enrollment: { hasStarted },
|
||||
courseRun: { isArchived },
|
||||
entitlement: isEntitlement !== null ? { isEntitlement, isFulfilled } : null,
|
||||
});
|
||||
};
|
||||
const renderComponent = () => render(<CourseCardActions {...props} />);
|
||||
describe('hooks', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
it('initializes hooks', () => {
|
||||
mockHooks();
|
||||
renderComponent();
|
||||
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
@@ -65,7 +54,7 @@ describe('CourseCardActions', () => {
|
||||
});
|
||||
describe('not entitlement, verified, or exec ed', () => {
|
||||
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
|
||||
mockHooks({ isArchived: true });
|
||||
mockHooks({ isArchived: true, isEntitlement: null });
|
||||
renderComponent();
|
||||
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
||||
expect(CourseCardActionSlot).toBeInTheDocument();
|
||||
@@ -74,7 +63,7 @@ describe('CourseCardActions', () => {
|
||||
});
|
||||
describe('unstarted courses', () => {
|
||||
it('renders CourseCardActionSlot and BeginCourseButton', () => {
|
||||
mockHooks();
|
||||
mockHooks({ isEntitlement: null });
|
||||
renderComponent();
|
||||
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
||||
expect(CourseCardActionSlot).toBeInTheDocument();
|
||||
@@ -84,7 +73,7 @@ describe('CourseCardActions', () => {
|
||||
});
|
||||
describe('active courses (started, and not archived)', () => {
|
||||
it('renders CourseCardActionSlot and ResumeButton', () => {
|
||||
mockHooks({ hasStarted: true });
|
||||
mockHooks({ hasStarted: true, isEntitlement: null });
|
||||
renderComponent();
|
||||
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
||||
expect(CourseCardActionSlot).toBeInTheDocument();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MailtoLink, Hyperlink } from '@openedx/paragon';
|
||||
import { CheckCircle } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import { utilHooks, useCourseData } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
import messages from './messages';
|
||||
@@ -14,15 +16,32 @@ import messages from './messages';
|
||||
const { useFormatDate } = utilHooks;
|
||||
|
||||
export const CertificateBanner = ({ cardId }) => {
|
||||
const certificate = reduxHooks.useCardCertificateData(cardId);
|
||||
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||
const courseData = useCourseData(cardId);
|
||||
const {
|
||||
isAudit,
|
||||
isVerified,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isPassing } = reduxHooks.useCardGradeData(cardId);
|
||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { minPassingGrade, progressUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { supportEmail, billingEmail } = reduxHooks.usePlatformSettingsData();
|
||||
certificate = {},
|
||||
isVerified = false,
|
||||
isAudit = false,
|
||||
isPassing = false,
|
||||
isArchived = false,
|
||||
minPassingGrade = 0,
|
||||
progressUrl = '',
|
||||
} = useMemo(() => ({
|
||||
isVerified: courseData?.enrollment?.isVerified,
|
||||
isAudit: courseData?.enrollment?.isAudit,
|
||||
certificate: courseData?.certificate || {},
|
||||
isPassing: courseData?.gradeData?.isPassing,
|
||||
isArchived: courseData?.courseRun?.isArchived,
|
||||
minPassingGrade: Math.floor((courseData?.courseRun?.minPassingGrade ?? 0) * 100),
|
||||
progressUrl: baseAppUrl(courseData?.courseRun?.progressUrl || ''),
|
||||
}), [courseData]);
|
||||
const { supportEmail, billingEmail } = useMemo(
|
||||
() => ({
|
||||
supportEmail: learnerHomeData?.platformSettings?.supportEmail,
|
||||
billingEmail: learnerHomeData?.platformSettings?.billingEmail,
|
||||
}),
|
||||
[learnerHomeData],
|
||||
);
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
@@ -75,7 +94,7 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isEarnedButUnavailable) {
|
||||
if (certificate.isEarned && new Date(certificate.availableDate) > new Date()) {
|
||||
return (
|
||||
<Banner>
|
||||
{formatMessage(
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import CertificateBanner from './CertificateBanner';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
utilHooks: {
|
||||
useFormatDate: jest.fn(() => date => date),
|
||||
},
|
||||
reduxHooks: {
|
||||
useCardCertificateData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardGradeData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('@openedx/paragon/icons');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('react');
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn(),
|
||||
}));
|
||||
|
||||
const defaultCertificate = {
|
||||
availableDate: '10/20/3030',
|
||||
@@ -40,9 +35,14 @@ const supportEmail = 'suport@email.com';
|
||||
const billingEmail = 'billing@email.com';
|
||||
|
||||
describe('CertificateBanner', () => {
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({
|
||||
minPassingGrade: 0.8,
|
||||
progressUrl: 'progressUrl',
|
||||
useCourseData.mockReturnValue({
|
||||
enrollment: {},
|
||||
certificate: {},
|
||||
gradeData: {},
|
||||
courseRun: {
|
||||
minPassingGrade: 0.8,
|
||||
progressUrl: 'progressUrl',
|
||||
},
|
||||
});
|
||||
const createWrapper = ({
|
||||
certificate = {},
|
||||
@@ -51,11 +51,17 @@ describe('CertificateBanner', () => {
|
||||
courseRun = {},
|
||||
platformSettings = {},
|
||||
}) => {
|
||||
reduxHooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
|
||||
reduxHooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings });
|
||||
useCourseData.mockReturnValue({
|
||||
enrollment: { ...defaultEnrollment, ...enrollment },
|
||||
certificate: { ...defaultCertificate, ...certificate },
|
||||
gradeData: { ...defaultGrade, ...grade },
|
||||
courseRun: {
|
||||
...defaultCourseRun,
|
||||
...courseRun,
|
||||
},
|
||||
});
|
||||
const lernearData = { data: { platformSettings: { ...defaultPlatformSettings, ...platformSettings } } };
|
||||
useInitializeLearnerHome.mockReturnValue(lernearData);
|
||||
return render(<IntlProvider locale="en"><CertificateBanner {...props} /></IntlProvider>);
|
||||
};
|
||||
beforeEach(() => {
|
||||
@@ -227,7 +233,8 @@ describe('CertificateBanner', () => {
|
||||
isPassing: true,
|
||||
},
|
||||
certificate: {
|
||||
isEarnedButUnavailable: true,
|
||||
isEarned: true,
|
||||
availableDate: '10/20/3030',
|
||||
},
|
||||
});
|
||||
const banner = screen.getByRole('alert');
|
||||
@@ -244,4 +251,27 @@ describe('CertificateBanner', () => {
|
||||
const banner = screen.queryByRole('alert');
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
it('should use default values when courseData is empty or undefined', () => {
|
||||
useCourseData.mockReturnValue({});
|
||||
const lernearData = { data: { platformSettings: { supportEmail } } };
|
||||
useInitializeLearnerHome.mockReturnValue(lernearData);
|
||||
render(<IntlProvider locale="en"><CertificateBanner cardId="test-card" /></IntlProvider>);
|
||||
|
||||
const mockedUseMemo = jest.spyOn(React, 'useMemo');
|
||||
const useMemoCall = mockedUseMemo.mock.calls.find(call => call[1].some(dep => dep === undefined || dep === null));
|
||||
|
||||
if (useMemoCall) {
|
||||
const result = useMemoCall[0]();
|
||||
|
||||
expect(result.certificate).toEqual({});
|
||||
expect(result.isVerified).toBe(false);
|
||||
expect(result.isAudit).toBe(false);
|
||||
expect(result.isPassing).toBe(false);
|
||||
expect(result.isArchived).toBe(false);
|
||||
expect(result.minPassingGrade).toBe(0);
|
||||
expect(result.progressUrl).toBeDefined();
|
||||
}
|
||||
|
||||
mockedUseMemo.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import { utilHooks, useCourseData } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
import messages from './messages';
|
||||
|
||||
export const CourseBanner = ({ cardId }) => {
|
||||
const {
|
||||
isVerified,
|
||||
isAuditAccessExpired,
|
||||
coursewareAccess = {},
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { formatMessage } = useIntl();
|
||||
const courseData = useCourseData(cardId);
|
||||
const {
|
||||
isVerified = false,
|
||||
isAuditAccessExpired = false,
|
||||
coursewareAccess = {},
|
||||
} = useMemo(() => ({
|
||||
isVerified: courseData.enrollment?.isVerified,
|
||||
isAuditAccessExpired: courseData.enrollment?.isAuditAccessExpired,
|
||||
coursewareAccess: courseData.enrollment?.coursewareAccess || {},
|
||||
}), [courseData]);
|
||||
const courseRun = courseData?.courseRun || {};
|
||||
const formatDate = utilHooks.useFormatDate();
|
||||
|
||||
const { hasUnmetPrerequisites, isStaff, isTooEarly } = coursewareAccess;
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { CourseBanner } from './CourseBanner';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
utilHooks: {
|
||||
useFormatDate: () => date => date,
|
||||
},
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('react');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
const enrollmentData = {
|
||||
@@ -43,13 +36,15 @@ const renderCourseBanner = (overrides = {}) => {
|
||||
courseRun = {},
|
||||
enrollment = {},
|
||||
} = overrides;
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
...courseRunData,
|
||||
...courseRun,
|
||||
});
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
...enrollmentData,
|
||||
...enrollment,
|
||||
useCourseData.mockReturnValue({
|
||||
courseRun: {
|
||||
...courseRunData,
|
||||
...courseRun,
|
||||
},
|
||||
enrollment: {
|
||||
...enrollmentData,
|
||||
...enrollment,
|
||||
},
|
||||
});
|
||||
return render(<IntlProvider locale="en"><CourseBanner cardId={cardId} /></IntlProvider>);
|
||||
};
|
||||
@@ -57,13 +52,20 @@ const renderCourseBanner = (overrides = {}) => {
|
||||
describe('CourseBanner', () => {
|
||||
it('initializes data with course number from enrollment, course and course run data', () => {
|
||||
renderCourseBanner();
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
it('no display if learner is verified', () => {
|
||||
renderCourseBanner({ enrollment: { isVerified: true } });
|
||||
expect(screen.queryByRole('alert')).toBeNull();
|
||||
});
|
||||
it('should use default values when enrollment data is undefined', () => {
|
||||
renderCourseBanner({
|
||||
enrollment: undefined,
|
||||
courseRun: {},
|
||||
});
|
||||
|
||||
expect(useCourseData).toHaveBeenCalledWith('test-card-id');
|
||||
});
|
||||
describe('audit access expired', () => {
|
||||
it('should display correct message and link', () => {
|
||||
renderCourseBanner({ enrollment: { isAuditAccessExpired: true } });
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
|
||||
import ApprovedContent from './views/ApprovedContent';
|
||||
import EligibleContent from './views/EligibleContent';
|
||||
@@ -15,9 +17,29 @@ export const statusComponents = StrictDict({
|
||||
});
|
||||
|
||||
export const useCreditBannerData = (cardId) => {
|
||||
const credit = reduxHooks.useCardCreditData(cardId);
|
||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
||||
if (!credit.isEligible) { return null; }
|
||||
const courseData = useCourseData(cardId);
|
||||
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||
const supportEmail = useMemo(
|
||||
() => (learnerHomeData?.platformSettings?.supportEmail),
|
||||
[learnerHomeData],
|
||||
);
|
||||
|
||||
const credit = useMemo(() => {
|
||||
const creditData = courseData?.credit;
|
||||
if (!creditData || Object.keys(creditData).length === 0) {
|
||||
return { isEligible: false };
|
||||
}
|
||||
return {
|
||||
isEligible: true,
|
||||
providerStatusUrl: creditData.providerStatusUrl,
|
||||
providerName: creditData.providerName,
|
||||
providerId: creditData.providerId,
|
||||
error: creditData.error,
|
||||
purchased: creditData.purchased,
|
||||
requestStatus: creditData.requestStatus,
|
||||
};
|
||||
}, [courseData]);
|
||||
if (!credit.isEligible || !courseData?.credit?.isEligible) { return null; }
|
||||
|
||||
const { error, purchased, requestStatus } = credit;
|
||||
let ContentComponent = EligibleContent;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { keyStore } from 'utils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
|
||||
import ApprovedContent from './views/ApprovedContent';
|
||||
import EligibleContent from './views/EligibleContent';
|
||||
@@ -9,12 +10,19 @@ import RejectedContent from './views/RejectedContent';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
},
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useMemo: (fn) => fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./views/ApprovedContent', () => 'ApprovedContent');
|
||||
jest.mock('./views/EligibleContent', () => 'EligibleContent');
|
||||
jest.mock('./views/MustRequestContent', () => 'MustRequestContent');
|
||||
@@ -34,18 +42,18 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
const loadHook = (creditData = {}) => {
|
||||
reduxHooks.useCardCreditData.mockReturnValue({ ...defaultProps, ...creditData });
|
||||
useCourseData.mockReturnValue({ credit: { ...defaultProps, ...creditData } });
|
||||
out = hooks.useCreditBannerData(cardId);
|
||||
};
|
||||
|
||||
describe('useCreditBannerData hook', () => {
|
||||
beforeEach(() => {
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
||||
useInitializeLearnerHome.mockReturnValue({ data: { platformSettings: { supportEmail } } });
|
||||
});
|
||||
it('loads card credit data with cardID and loads platform settings data', () => {
|
||||
loadHook({ isEligible: false });
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.usePlatformSettingsData).toHaveBeenCalledWith();
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
expect(useInitializeLearnerHome).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('non-credit-eligible learner', () => {
|
||||
it('returns null if the learner is not credit eligible', () => {
|
||||
|
||||
@@ -7,10 +7,6 @@ jest.mock('./hooks', () => ({
|
||||
useCreditBannerData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('react');
|
||||
|
||||
describe('CreditBanner', () => {
|
||||
const mockCardId = 'test-card-id';
|
||||
const mockContentComponent = () => <div data-testid="mock-content">Test Content</div>;
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const ApprovedContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const courseData = useCourseData(cardId);
|
||||
const { providerStatusUrl: href, providerName } = useMemo(() => {
|
||||
const creditData = courseData?.credit;
|
||||
return {
|
||||
providerStatusUrl: creditData.providerStatusUrl,
|
||||
providerName: creditData.providerName,
|
||||
};
|
||||
}, [courseData]);
|
||||
const isMasquerading = useIsMasquerading();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import messages from './messages';
|
||||
import ApprovedContent from './ApprovedContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
useIsMasquerading: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
providerStatusUrl: 'test-credit-provider-status-url',
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
useCourseData.mockReturnValue({ credit });
|
||||
useIsMasquerading.mockReturnValue(false);
|
||||
|
||||
describe('ApprovedContent component', () => {
|
||||
describe('hooks', () => {
|
||||
it('initializes credit data with cardId', () => {
|
||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
@@ -60,7 +54,7 @@ describe('ApprovedContent component', () => {
|
||||
});
|
||||
describe('when masquerading', () => {
|
||||
beforeEach(() => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
||||
useIsMasquerading.mockReturnValue(true);
|
||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import track from 'tracking';
|
||||
|
||||
import CreditContent from './components/CreditContent';
|
||||
@@ -11,8 +11,9 @@ import messages from './messages';
|
||||
|
||||
export const EligibleContent = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { courseId } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const courseData = useCourseData(cardId);
|
||||
const providerName = courseData?.credit?.providerName;
|
||||
const courseId = courseData?.courseRun?.courseId;
|
||||
|
||||
const onClick = track.credit.purchase(courseId);
|
||||
const getCredit = formatMessage(messages.getCredit);
|
||||
|
||||
@@ -2,17 +2,14 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import track from 'tracking';
|
||||
|
||||
import messages from './messages';
|
||||
import EligibleContent from './EligibleContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
@@ -21,17 +18,12 @@ jest.mock('tracking', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const courseId = 'test-course-id';
|
||||
const credit = {
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
|
||||
useCourseData.mockReturnValue({ credit, courseRun: { courseId } });
|
||||
|
||||
const renderEligibleContent = () => render(<IntlProvider locale="en" messages={{}}><EligibleContent cardId={cardId} /></IntlProvider>);
|
||||
|
||||
@@ -39,11 +31,7 @@ describe('EligibleContent component', () => {
|
||||
describe('hooks', () => {
|
||||
it('initializes credit data with cardId', () => {
|
||||
renderEligibleContent();
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
it('initializes course run data with cardId', () => {
|
||||
renderEligibleContent();
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
@@ -67,7 +55,7 @@ describe('EligibleContent component', () => {
|
||||
expect(eligibleMessage).toHaveTextContent(credit.providerName);
|
||||
});
|
||||
it('message is formatted eligible message if no provider', () => {
|
||||
reduxHooks.useCardCreditData.mockReturnValue({});
|
||||
useCourseData.mockReturnValue({ credit: {}, courseRun: { courseId } });
|
||||
renderEligibleContent();
|
||||
const eligibleMessage = screen.getByTestId('credit-msg');
|
||||
expect(eligibleMessage).toBeInTheDocument();
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useIsMasquerading } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import hooks from './hooks';
|
||||
@@ -13,7 +13,7 @@ import messages from './messages';
|
||||
export const MustRequestContent = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const isMasquerading = useIsMasquerading();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import messages from './messages';
|
||||
import hooks from './hooks';
|
||||
import MustRequestContent from './MustRequestContent';
|
||||
@@ -12,16 +11,10 @@ jest.mock('./hooks', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardCreditData: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
useIsMasquerading: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('react');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const requestData = {
|
||||
url: 'test-request-data-url',
|
||||
@@ -48,10 +41,12 @@ describe('MustRequestContent component', () => {
|
||||
requestData,
|
||||
createCreditRequest,
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
reduxHooks.useCardCreditData.mockReturnValue({
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
useIsMasquerading.mockReturnValue(false);
|
||||
useCourseData.mockReturnValue({
|
||||
credit: {
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,7 +89,7 @@ describe('MustRequestContent component', () => {
|
||||
|
||||
describe('when masquerading', () => {
|
||||
beforeEach(() => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
||||
useIsMasquerading.mockReturnValue(true);
|
||||
renderMustRequestContent();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import messages from './messages';
|
||||
|
||||
export const PendingContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const courseData = useCourseData(cardId);
|
||||
const { providerStatusUrl: href, providerName } = courseData?.credit || {};
|
||||
const isMasquerading = useIsMasquerading();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import PendingContent from './PendingContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
|
||||
useCourseData: jest.fn(),
|
||||
useIsMasquerading: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const providerName = 'test-credit-provider-name';
|
||||
const providerStatusUrl = 'test-credit-provider-status-url';
|
||||
reduxHooks.useCardCreditData.mockReturnValue({
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
useIsMasquerading.mockReturnValue(false);
|
||||
useCourseData.mockReturnValue({
|
||||
credit: {
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
},
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
const renderPendingContent = () => render(
|
||||
<IntlProvider messages={{}} locale="en">
|
||||
@@ -32,7 +30,7 @@ describe('PendingContent component', () => {
|
||||
describe('hooks', () => {
|
||||
it('initializes card credit data with cardId', () => {
|
||||
renderPendingContent();
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
@@ -60,7 +58,7 @@ describe('PendingContent component', () => {
|
||||
});
|
||||
describe('when masqueradeData is true', () => {
|
||||
it('disables the view details button', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
||||
useIsMasquerading.mockReturnValue(true);
|
||||
renderPendingContent();
|
||||
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
|
||||
expect(button).toHaveClass('disabled');
|
||||
|
||||
@@ -3,18 +3,19 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import messages from './messages';
|
||||
|
||||
export const RejectedContent = ({ cardId }) => {
|
||||
const credit = reduxHooks.useCardCreditData(cardId);
|
||||
const courseData = useCourseData(cardId);
|
||||
const credit = courseData?.credit;
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
message={formatMessage(messages.rejected, {
|
||||
providerName: credit.providerName,
|
||||
providerName: credit?.providerName,
|
||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import RejectedContent from './RejectedContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('react');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
providerStatusUrl: 'test-credit-provider-status-url',
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
useCourseData.mockReturnValue({
|
||||
credit,
|
||||
});
|
||||
|
||||
const renderRejectedContent = () => render(<IntlProvider><RejectedContent cardId={cardId} /></IntlProvider>);
|
||||
|
||||
@@ -27,7 +23,7 @@ describe('RejectedContent component', () => {
|
||||
describe('hooks', () => {
|
||||
it('initializes credit data with cardId', () => {
|
||||
renderRejectedContent();
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
|
||||
@@ -2,9 +2,6 @@ import { render, screen } from '@testing-library/react';
|
||||
|
||||
import CreditContent from './CreditContent';
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const action = {
|
||||
href: 'test-action-href',
|
||||
onClick: jest.fn().mockName('test-action-onClick'),
|
||||
|
||||
@@ -4,6 +4,12 @@ import useCreditRequestFormData from './hooks';
|
||||
|
||||
const requestData = 'test-request-data';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useRef: jest.fn((val) => ({ current: val, useRef: true })),
|
||||
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
|
||||
}));
|
||||
|
||||
let out;
|
||||
const ref = {
|
||||
current: { click: jest.fn() },
|
||||
|
||||
@@ -10,9 +10,6 @@ jest.mock('./hooks', () => ({
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const ref = { current: { click: jest.fn() }, useRef: jest.fn() };
|
||||
|
||||
const requestData = {
|
||||
|
||||
@@ -4,9 +4,6 @@ import { render } from '@testing-library/react';
|
||||
import useCreditRequestFormData from './hooks';
|
||||
import CreditRequestForm from '.';
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
|
||||
export const ProviderLink = ({ cardId }) => {
|
||||
const credit = reduxHooks.useCardCreditData(cardId);
|
||||
const courseData = useCourseData(cardId);
|
||||
const credit = courseData?.credit || {};
|
||||
return (
|
||||
<Hyperlink
|
||||
href={credit.providerStatusUrl}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useCourseData } from 'hooks';
|
||||
|
||||
import ProviderLink from './ProviderLink';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('react');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
providerStatusUrl: 'test-credit-provider-status-url',
|
||||
@@ -27,12 +21,12 @@ const renderProviderLink = () => render(
|
||||
describe('ProviderLink component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
useCourseData.mockReturnValue({ credit });
|
||||
renderProviderLink();
|
||||
});
|
||||
describe('hooks', () => {
|
||||
it('initializes credit hook with cardId', () => {
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { StrictDict } from 'utils';
|
||||
import { apiHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { useCreateCreditRequest } from 'data/hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
@@ -11,13 +12,19 @@ export const state = StrictDict({
|
||||
|
||||
export const useCreditRequestData = (cardId) => {
|
||||
const [requestData, setRequestData] = module.state.creditRequestData(null);
|
||||
const createCreditApiRequest = apiHooks.useCreateCreditRequest(cardId);
|
||||
const courseData = useCourseData(cardId);
|
||||
const providerId = courseData?.credit?.providerId;
|
||||
const { authenticatedUser: { username } } = React.useContext(AppContext);
|
||||
const courseId = courseData?.courseRun?.courseId;
|
||||
const { mutate: createCreditMutation } = useCreateCreditRequest();
|
||||
|
||||
const createCreditRequest = (e) => {
|
||||
e.preventDefault();
|
||||
createCreditApiRequest()
|
||||
.then((request) => {
|
||||
setRequestData(request.data);
|
||||
});
|
||||
createCreditMutation({ providerId, courseId, username }, {
|
||||
onSuccess: (response) => {
|
||||
setRequestData(response.data);
|
||||
},
|
||||
});
|
||||
};
|
||||
return { requestData, createCreditRequest };
|
||||
};
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { apiHooks } from 'hooks';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
apiHooks: {
|
||||
useCreateCreditRequest: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const requestData = { data: 'request data' };
|
||||
const creditRequest = jest.fn().mockReturnValue(Promise.resolve(requestData));
|
||||
apiHooks.useCreateCreditRequest.mockReturnValue(creditRequest);
|
||||
const event = { preventDefault: jest.fn() };
|
||||
|
||||
let out;
|
||||
describe('Credit Banner view hooks', () => {
|
||||
describe('state', () => {
|
||||
state.testGetter(state.keys.creditRequestData);
|
||||
});
|
||||
describe('useCreditRequestData', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
out = hooks.useCreditRequestData(cardId);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes creditRequestData state field with null value', () => {
|
||||
state.expectInitializedWith(state.keys.creditRequestData, null);
|
||||
});
|
||||
it('calls useCreateCreditRequest with passed cardID', () => {
|
||||
expect(apiHooks.useCreateCreditRequest).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('returns requestData state value', () => {
|
||||
state.mockVal(state.keys.creditRequestData, requestData);
|
||||
out = hooks.useCreditRequestData(cardId);
|
||||
expect(out.requestData).toEqual(requestData);
|
||||
});
|
||||
describe('createCreditRequest', () => {
|
||||
it('returns an event handler that prevents default click behavior', () => {
|
||||
out.createCreditRequest(event);
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
it('calls api.createCreditRequest and sets requestData with the response', async () => {
|
||||
await out.createCreditRequest(event);
|
||||
expect(creditRequest).toHaveBeenCalledWith();
|
||||
expect(state.setState.creditRequestData).toHaveBeenCalledWith(requestData.data);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import * as api from 'data/services/lms/api';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('data/services/lms/api', () => ({
|
||||
createCreditRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/logging'),
|
||||
logError: jest.fn(),
|
||||
}));
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContext.Provider value={{
|
||||
authenticatedUser: { username: 'test-user' },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
describe('useCreditRequestData', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper();
|
||||
(useCourseData as jest.Mock).mockReturnValue({
|
||||
credit: { providerId: 'provider-123' },
|
||||
courseRun: { courseId: 'course-456' },
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('initializes requestData as null', () => {
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
expect(result.current.requestData).toBeNull();
|
||||
});
|
||||
|
||||
it('returns createCreditRequest function', () => {
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
expect(typeof result.current.createCreditRequest).toBe('function');
|
||||
});
|
||||
|
||||
it('prevents default event behavior', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(api.createCreditRequest as jest.Mock).mockResolvedValue({ data: 'success' });
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls API with correct parameters', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(api.createCreditRequest as jest.Mock).mockResolvedValue({ data: 'success' });
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||
providerId: 'provider-123',
|
||||
courseId: 'course-456',
|
||||
username: 'test-user',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets requestData with response data on success', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
const responseData = { data: { id: 'credit-123', status: 'pending' } };
|
||||
(api.createCreditRequest as jest.Mock).mockResolvedValue(responseData);
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||
providerId: 'provider-123',
|
||||
courseId: 'course-456',
|
||||
username: 'test-user',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.requestData).toEqual(responseData.data);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing providerId gracefully', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(useCourseData as jest.Mock).mockReturnValue({
|
||||
credit: null,
|
||||
courseRun: { courseId: 'course-456' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||
providerId: undefined,
|
||||
courseId: 'course-456',
|
||||
username: 'test-user',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing courseId gracefully', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(useCourseData as jest.Mock).mockReturnValue({
|
||||
credit: { providerId: 'provider-123' },
|
||||
courseRun: null,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||
providerId: 'provider-123',
|
||||
courseId: undefined,
|
||||
username: 'test-user',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles API errors without crashing', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(api.createCreditRequest as jest.Mock).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(result.current.requestData).toBeNull();
|
||||
});
|
||||
|
||||
it('uses cardId to fetch course data', () => {
|
||||
renderHook(() => hooks.useCreditRequestData('different-card'), { wrapper });
|
||||
|
||||
expect(useCourseData).toHaveBeenCalledWith('different-card');
|
||||
});
|
||||
|
||||
it('handles undefined response data', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(api.createCreditRequest as jest.Mock).mockResolvedValue({ status: 200 });
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.requestData).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, MailtoLink } from '@openedx/paragon';
|
||||
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
|
||||
import { utilHooks, useCourseData, useEntitlementInfo } from 'hooks';
|
||||
import { useSelectSessionModal } from 'data/context';
|
||||
import Banner from 'components/Banner';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const EntitlementBanner = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||
const courseData = useCourseData(cardId);
|
||||
|
||||
const {
|
||||
isEntitlement,
|
||||
hasSessions,
|
||||
@@ -18,9 +23,12 @@ export const EntitlementBanner = ({ cardId }) => {
|
||||
changeDeadline,
|
||||
showExpirationWarning,
|
||||
isExpired,
|
||||
} = reduxHooks.useCardEntitlementData(cardId);
|
||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
} = useEntitlementInfo(courseData);
|
||||
const supportEmail = useMemo(
|
||||
() => learnerHomeData?.platformSettings?.supportEmail,
|
||||
[learnerHomeData],
|
||||
);
|
||||
const { updateSelectSessionModal } = useSelectSessionModal();
|
||||
const formatDate = utilHooks.useFormatDate();
|
||||
|
||||
if (!isEntitlement) {
|
||||
@@ -42,7 +50,7 @@ export const EntitlementBanner = ({ cardId }) => {
|
||||
{formatMessage(messages.entitlementExpiringSoon, {
|
||||
changeDeadline: formatDate(changeDeadline),
|
||||
selectSessionButton: (
|
||||
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
|
||||
<Button variant="link" size="inline" className="m-0 p-0" onClick={() => updateSelectSessionModal(cardId)}>
|
||||
{formatMessage(messages.selectSession)}
|
||||
</Button>
|
||||
),
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
utilHooks: {
|
||||
useFormatDate: () => date => date,
|
||||
},
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useUpdateSelectSessionModalCallback: jest.fn(
|
||||
(cardId) => jest.fn().mockName(`updateSelectSessionModalCallback(${cardId})`),
|
||||
),
|
||||
},
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useMemo: (fn) => fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||
data: {
|
||||
platformSettings: {
|
||||
supportEmail: 'test-support-email',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
const mockUpdateSelectSessionModal = jest.fn().mockName('updateSelectSessionModal');
|
||||
jest.mock('data/context/SelectSessionProvider', () => ({
|
||||
useSelectSessionModal: () => ({
|
||||
updateSelectSessionModal: mockUpdateSelectSessionModal,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
...jest.requireActual('hooks'),
|
||||
useCourseData: jest.fn(),
|
||||
utilHooks: {
|
||||
useFormatDate: () => date => date?.toDateString(),
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
@@ -36,16 +50,20 @@ const platformData = { supportEmail: 'test-support-email' };
|
||||
|
||||
const renderComponent = (overrides = {}) => {
|
||||
const { entitlement = {} } = overrides;
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValueOnce(platformData);
|
||||
useCourseData.mockReturnValue({
|
||||
entitlement: { ...entitlementData, ...entitlement },
|
||||
platformSettings: platformData,
|
||||
});
|
||||
return render(<IntlProvider locale="en"><EntitlementBanner cardId={cardId} /></IntlProvider>);
|
||||
};
|
||||
|
||||
describe('EntitlementBanner', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('initializes data with course number from entitlement', () => {
|
||||
renderComponent();
|
||||
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(cardId);
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
it('no display if not an entitlement', () => {
|
||||
renderComponent({ entitlement: { isEntitlement: false } });
|
||||
@@ -60,7 +78,10 @@ describe('EntitlementBanner', () => {
|
||||
expect(banner.innerHTML).toContain(platformData.supportEmail);
|
||||
});
|
||||
it('renders when expiration warning', () => {
|
||||
renderComponent({ entitlement: { showExpirationWarning: true } });
|
||||
const deadline = new Date();
|
||||
deadline.setDate(deadline.getDate() + 4);
|
||||
const deadlineStr = `${deadline.getMonth() + 1}/${deadline.getDate()}/${deadline.getFullYear()}`;
|
||||
renderComponent({ entitlement: { changeDeadline: deadlineStr, isFulfilled: false, availableSessions: [1, 2, 3] } });
|
||||
const banner = screen.getByRole('alert');
|
||||
expect(banner).toBeInTheDocument();
|
||||
expect(banner).toHaveClass('alert-info');
|
||||
@@ -68,9 +89,37 @@ describe('EntitlementBanner', () => {
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
it('renders expired banner', () => {
|
||||
renderComponent({ entitlement: { isExpired: true } });
|
||||
renderComponent({ entitlement: { isExpired: true, availableSessions: [1, 2, 3] } });
|
||||
const banner = screen.getByRole('alert');
|
||||
expect(banner).toBeInTheDocument();
|
||||
expect(banner.innerHTML).toContain(formatMessage(messages.entitlementExpired));
|
||||
});
|
||||
it('should call updateSelectSessionModal with cardId when select session button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const deadline = new Date();
|
||||
deadline.setDate(deadline.getDate() + 4);
|
||||
const deadlineStr = `${deadline.getMonth() + 1}/${deadline.getDate()}/${deadline.getFullYear()}`;
|
||||
renderComponent({ entitlement: { changeDeadline: deadlineStr, isFulfilled: false, availableSessions: [1, 2, 3] } });
|
||||
const banner = screen.getByRole('alert');
|
||||
expect(banner).toBeInTheDocument();
|
||||
expect(banner).toHaveClass('alert-info');
|
||||
const button = screen.getByRole('button', { name: formatMessage(messages.selectSession) });
|
||||
expect(button).toBeInTheDocument();
|
||||
await user.click(button);
|
||||
|
||||
expect(mockUpdateSelectSessionModal).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
it('should return null when isExpired is false and showExpirationWarning is false', () => {
|
||||
renderComponent({
|
||||
entitlement: {
|
||||
isEntitlement: true,
|
||||
hasSessions: true,
|
||||
isFulfilled: true,
|
||||
showExpirationWarning: false,
|
||||
isExpired: false,
|
||||
},
|
||||
});
|
||||
const banner = screen.queryByRole('alert');
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import { Program } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
import ProgramList from './ProgramsList';
|
||||
@@ -12,10 +12,10 @@ import messages from './messages';
|
||||
|
||||
export const RelatedProgramsBanner = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const courseData = useCourseData(cardId);
|
||||
const programData = courseData?.programs;
|
||||
|
||||
const programData = reduxHooks.useCardRelatedProgramsData(cardId);
|
||||
|
||||
if (!programData?.length) {
|
||||
if (!courseData || !programData?.relatedPrograms.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const RelatedProgramsBanner = ({ cardId }) => {
|
||||
<span className="font-weight-bolder">
|
||||
{formatMessage(messages.relatedPrograms)}
|
||||
</span>
|
||||
<ProgramList programs={programData.list} />
|
||||
<ProgramList programs={programData.relatedPrograms} />
|
||||
</Banner>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import RelatedProgramsBanner from '.';
|
||||
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('@openedx/paragon/icons');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('react');
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardRelatedProgramsData: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
@@ -32,21 +25,21 @@ const programData = {
|
||||
|
||||
describe('RelatedProgramsBanner', () => {
|
||||
it('render empty', () => {
|
||||
reduxHooks.useCardRelatedProgramsData.mockReturnValue({});
|
||||
useCourseData.mockReturnValue(null);
|
||||
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
|
||||
const banner = screen.queryByRole('alert');
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
|
||||
it('render with programs', () => {
|
||||
reduxHooks.useCardRelatedProgramsData.mockReturnValue(programData);
|
||||
useCourseData.mockReturnValue({ programs: { relatedPrograms: programData.list } });
|
||||
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
|
||||
const list = screen.getByRole('list');
|
||||
expect(list.childElementCount).toBe(programData.list.length);
|
||||
});
|
||||
|
||||
it('render related programs title', () => {
|
||||
reduxHooks.useCardRelatedProgramsData.mockReturnValue(programData);
|
||||
useCourseData.mockReturnValue({ programs: { relatedPrograms: programData.list } });
|
||||
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
|
||||
const title = screen.getByText('Related Programs:');
|
||||
expect(title).toBeInTheDocument();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
|
||||
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
|
||||
import CertificateBanner from './CertificateBanner';
|
||||
@@ -10,7 +10,11 @@ import EntitlementBanner from './EntitlementBanner';
|
||||
import RelatedProgramsBanner from './RelatedProgramsBanner';
|
||||
|
||||
export const CourseCardBanners = ({ cardId }) => {
|
||||
const { isEnrolled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const courseData = useCourseData(cardId);
|
||||
if (!courseData) {
|
||||
return null;
|
||||
}
|
||||
const { isEnrolled = false } = courseData.enrollment;
|
||||
return (
|
||||
<div className="course-card-banners" data-testid="CourseCardBanners">
|
||||
<RelatedProgramsBanner cardId={cardId} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
|
||||
import CourseCardBanners from '.';
|
||||
|
||||
@@ -20,9 +20,11 @@ const mockedComponents = [
|
||||
];
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardEnrollmentData: jest.fn(() => ({ isEnrolled: true })),
|
||||
},
|
||||
useCourseData: jest.fn(() => ({
|
||||
enrollment: {
|
||||
isEnrolled: true,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('CourseCardBanners', () => {
|
||||
@@ -36,8 +38,13 @@ describe('CourseCardBanners', () => {
|
||||
return expect(mockedComponent).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('render null with no courseData', () => {
|
||||
useCourseData.mockReturnValue(null);
|
||||
const { container } = render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
it('render with isEnrolled false', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
|
||||
useCourseData.mockReturnValue({ enrollment: { isEnrolled: false } });
|
||||
render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
|
||||
const mockedComponentsIfNotEnrolled = mockedComponents.slice(-2);
|
||||
mockedComponentsIfNotEnrolled.map((componentName) => {
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import { utilHooks, useCourseData, useEntitlementInfo } from 'hooks';
|
||||
import { useSelectSessionModal } from 'data/context';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const useAccessMessage = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const enrollment = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
||||
const courseData = useCourseData(cardId);
|
||||
const { courseRun, enrollment } = courseData || {};
|
||||
const formatDate = utilHooks.useFormatDate();
|
||||
if (!courseRun.isStarted) {
|
||||
if (!courseRun.startDate && !courseRun.advertisedStart) { return null; }
|
||||
const startDate = courseRun.advertisedStart ? courseRun.advertisedStart : formatDate(courseRun.startDate);
|
||||
return formatMessage(messages.courseStarts, { startDate });
|
||||
}
|
||||
if (enrollment.isEnrolled) {
|
||||
if (enrollment?.isEnrolled) {
|
||||
const { isArchived, endDate } = courseRun;
|
||||
const {
|
||||
accessExpirationDate,
|
||||
@@ -38,15 +39,15 @@ export const useAccessMessage = ({ cardId }) => {
|
||||
|
||||
export const useCardDetailsData = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const providerName = reduxHooks.useCardProviderData(cardId).name;
|
||||
const { courseNumber } = reduxHooks.useCardCourseData(cardId);
|
||||
const courseData = useCourseData(cardId);
|
||||
const providerName = courseData?.courseProvider?.name;
|
||||
const courseNumber = courseData?.course?.courseNumber;
|
||||
const {
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
} = reduxHooks.useCardEntitlementData(cardId);
|
||||
|
||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
} = useEntitlementInfo(courseData);
|
||||
const { updateSelectSessionModal } = useSelectSessionModal();
|
||||
|
||||
return {
|
||||
providerName: providerName || formatMessage(messages.unknownProviderName),
|
||||
@@ -54,7 +55,7 @@ export const useCardDetailsData = ({ cardId }) => {
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
openSessionModal,
|
||||
openSessionModal: () => updateSelectSessionModal(cardId),
|
||||
courseNumber,
|
||||
changeOrLeaveSessionMessage: formatMessage(messages.changeOrLeaveSessionButton),
|
||||
};
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
|
||||
import { utilHooks, useCourseData } from 'hooks';
|
||||
import { useSelectSessionModal } from 'data/context';
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useMemo: (fn) => fn(),
|
||||
}));
|
||||
|
||||
const updateSelectSessionModalMock = jest.fn().mockName('updateSelectSessionModal');
|
||||
jest.mock('data/context', () => ({
|
||||
useSelectSessionModal: jest.fn(),
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
...jest.requireActual('hooks'),
|
||||
useCourseData: jest.fn(),
|
||||
utilHooks: {
|
||||
useFormatDate: jest.fn(),
|
||||
},
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useCardProviderData: jest.fn(),
|
||||
useUpdateSelectSessionModalCallback: (...args) => ({ updateSelectSessionModalCallback: args }),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const { formatMessage } = jest.requireActual('testUtils');
|
||||
return {
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: () => ({
|
||||
formatMessage,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const cardId = 'my-test-card-id';
|
||||
const courseNumber = 'test-course-number';
|
||||
const useAccessMessage = 'test-access-message';
|
||||
@@ -50,15 +63,13 @@ describe('CourseCardDetails hooks', () => {
|
||||
const runHook = ({ provider = {}, entitlement = {} }) => {
|
||||
jest.spyOn(hooks, hookKeys.useAccessMessage)
|
||||
.mockImplementationOnce(mockAccessMessage);
|
||||
reduxHooks.useCardProviderData.mockReturnValueOnce({
|
||||
...providerData,
|
||||
...provider,
|
||||
useCourseData.mockReturnValue({
|
||||
courseProvider: { ...providerData, ...provider },
|
||||
course: { courseNumber },
|
||||
courseRun: {},
|
||||
entitlement: { ...entitlementData, ...entitlement },
|
||||
});
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({
|
||||
...entitlementData,
|
||||
...entitlement,
|
||||
});
|
||||
reduxHooks.useCardCourseData.mockReturnValueOnce({ courseNumber });
|
||||
useSelectSessionModal.mockReturnValue({ updateSelectSessionModal: updateSelectSessionModalMock });
|
||||
out = hooks.useCardDetailsData({ cardId });
|
||||
};
|
||||
beforeEach(() => {
|
||||
@@ -75,6 +86,10 @@ describe('CourseCardDetails hooks', () => {
|
||||
it('forward changeOrLeaveSessionMessage', () => {
|
||||
expect(out.changeOrLeaveSessionMessage).toEqual(formatMessage(messages.changeOrLeaveSessionButton));
|
||||
});
|
||||
it('calls updateSelectSessionModal when openSessionModal is called', () => {
|
||||
out.openSessionModal();
|
||||
expect(updateSelectSessionModalMock).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAccessMessage', () => {
|
||||
@@ -91,21 +106,16 @@ describe('CourseCardDetails hooks', () => {
|
||||
endDate: '10/20/2000',
|
||||
};
|
||||
const runHook = ({ enrollment = {}, courseRun = {} }) => {
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
...courseRunData,
|
||||
...courseRun,
|
||||
});
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
...enrollmentData,
|
||||
...enrollment,
|
||||
useCourseData.mockReturnValue({
|
||||
courseRun: { ...courseRunData, ...courseRun },
|
||||
enrollment: { ...enrollmentData, ...enrollment },
|
||||
});
|
||||
out = hooks.useAccessMessage({ cardId });
|
||||
};
|
||||
|
||||
it('loads data from enrollment and course run data based on course number', () => {
|
||||
runHook({});
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
|
||||
describe('if not started yet', () => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import { Badge } from '@openedx/paragon';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||
import verifiedRibbon from 'assets/verified-ribbon.png';
|
||||
import useActionDisabledState from './hooks';
|
||||
|
||||
@@ -15,11 +16,10 @@ const { courseImageClicked } = track.course;
|
||||
|
||||
export const CourseCardImage = ({ cardId, orientation }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { bannerImgSrc } = reduxHooks.useCardCourseData(cardId);
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const courseData = useCourseData(cardId);
|
||||
const { homeUrl } = courseData?.courseRun || {};
|
||||
const { disableCourseTitle } = useActionDisabledState(cardId);
|
||||
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
|
||||
const handleImageClicked = useCourseTrackingEvent(courseImageClicked, cardId, homeUrl);
|
||||
const wrapperClassName = `pgn__card-wrapper-image-cap d-inline-block overflow-visible ${orientation}`;
|
||||
const image = (
|
||||
<>
|
||||
@@ -27,11 +27,11 @@ export const CourseCardImage = ({ cardId, orientation }) => {
|
||||
// w-100 is necessary for images on Safari, otherwise stretches full height of the image
|
||||
// https://stackoverflow.com/a/44250830
|
||||
className="pgn__card-image-cap w-100 show"
|
||||
src={bannerImgSrc}
|
||||
src={courseData?.course?.bannerImgSrc && baseAppUrl(courseData.course.bannerImgSrc)}
|
||||
alt={formatMessage(messages.bannerAlt)}
|
||||
/>
|
||||
{
|
||||
isVerified && (
|
||||
courseData?.enrollment?.isVerified && (
|
||||
<span
|
||||
className="course-card-verify-ribbon-container"
|
||||
title={formatMessage(messages.verifiedHoverDescription)}
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
import useActionDisabledState from './hooks';
|
||||
import { CourseCardImage } from './CourseCardImage';
|
||||
import messages from '../messages';
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const homeUrl = 'https://example.com';
|
||||
const bannerImgSrc = 'banner-img-src.jpg';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(() => ({ bannerImgSrc })),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
|
||||
trackCourseEvent: { eventName, cardId, url },
|
||||
})),
|
||||
},
|
||||
useCourseData: jest.fn(() => ({
|
||||
course: { bannerImgSrc },
|
||||
courseRun: { homeUrl },
|
||||
enrollment: {},
|
||||
})),
|
||||
useCourseTrackingEvent: jest.fn((eventName, cardId, url) => ({
|
||||
trackCourseEvent: { eventName, cardId, url },
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
@@ -34,7 +30,13 @@ describe('CourseCardImage', () => {
|
||||
|
||||
it('renders course image with correct attributes', () => {
|
||||
useActionDisabledState.mockReturnValue({ disableCourseTitle: true });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
|
||||
useCourseData.mockReturnValue(
|
||||
{
|
||||
course: { bannerImgSrc },
|
||||
courseRun: { homeUrl },
|
||||
enrollment: { isVerified: true },
|
||||
},
|
||||
);
|
||||
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
||||
|
||||
const image = screen.getByRole('img', { name: formatMessage(messages.bannerAlt) });
|
||||
@@ -45,7 +47,13 @@ describe('CourseCardImage', () => {
|
||||
|
||||
it('isVerified, should render badge', () => {
|
||||
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
|
||||
useCourseData.mockReturnValue(
|
||||
{
|
||||
course: { bannerImgSrc },
|
||||
courseRun: { homeUrl },
|
||||
enrollment: { isVerified: true },
|
||||
},
|
||||
);
|
||||
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
||||
|
||||
const badge = screen.getByText(formatMessage(messages.verifiedBanner));
|
||||
@@ -56,7 +64,13 @@ describe('CourseCardImage', () => {
|
||||
|
||||
it('renders link with correct href if disableCourseTitle is false', () => {
|
||||
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: false });
|
||||
useCourseData.mockReturnValue(
|
||||
{
|
||||
course: { bannerImgSrc },
|
||||
courseRun: { homeUrl },
|
||||
enrollment: { isVerified: false },
|
||||
},
|
||||
);
|
||||
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
||||
|
||||
const link = screen.getByRole('link');
|
||||
@@ -65,12 +79,15 @@ describe('CourseCardImage', () => {
|
||||
describe('hooks', () => {
|
||||
it('initializes', () => {
|
||||
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
|
||||
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
|
||||
props.cardId,
|
||||
useCourseData.mockReturnValue(
|
||||
{
|
||||
course: { bannerImgSrc },
|
||||
courseRun: { homeUrl },
|
||||
enrollment: { isVerified: true },
|
||||
},
|
||||
);
|
||||
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
|
||||
expect(useCourseData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as ReactShare from 'react-share';
|
||||
|
||||
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown } from '@openedx/paragon';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { useCourseTrackingEvent, useCourseData, useIsMasquerading } from 'hooks';
|
||||
import { useCardSocialSettingsData } from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const testIds = {
|
||||
@@ -16,14 +16,15 @@ export const testIds = {
|
||||
|
||||
export const SocialShareMenu = ({ cardId, emailSettings }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const courseData = useCourseData(cardId);
|
||||
const courseName = courseData?.course?.courseName;
|
||||
const isExecEd2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(courseData.enrollment.mode);
|
||||
const isEmailEnabled = courseData?.enrollment?.isEmailEnabled ?? false;
|
||||
const { twitter, facebook } = useCardSocialSettingsData(cardId);
|
||||
const isMasquerading = useIsMasquerading();
|
||||
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const { isEmailEnabled, isExecEd2UCourse } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
|
||||
const handleTwitterShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'twitter');
|
||||
const handleFacebookShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'facebook');
|
||||
const handleTwitterShare = useCourseTrackingEvent(track.socialShare, cardId, 'twitter');
|
||||
const handleFacebookShare = useCourseTrackingEvent(track.socialShare, cardId, 'facebook');
|
||||
|
||||
if (isExecEd2UCourse) {
|
||||
return null;
|
||||
@@ -50,6 +51,7 @@ export const SocialShareMenu = ({ cardId, emailSettings }) => {
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
aria-label="facebook"
|
||||
>
|
||||
{formatMessage(messages.shareToFacebook)}
|
||||
</ReactShare.FacebookShareButton>
|
||||
@@ -64,6 +66,7 @@ export const SocialShareMenu = ({ cardId, emailSettings }) => {
|
||||
})}
|
||||
resetButtonStyle={false}
|
||||
className="pgn__dropdown-item dropdown-item"
|
||||
aria-label="twitter"
|
||||
>
|
||||
{formatMessage(messages.shareToTwitter)}
|
||||
</ReactShare.TwitterShareButton>
|
||||
|
||||
@@ -4,9 +4,9 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseTrackingEvent, useCourseData, useIsMasquerading } from 'hooks';
|
||||
|
||||
import { useEmailSettings } from './hooks';
|
||||
import { useEmailSettings, useCardSocialSettingsData } from './hooks';
|
||||
import SocialShareMenu from './SocialShareMenu';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -15,22 +15,15 @@ jest.mock('tracking', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardSocialSettingsData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn((...args) => ({ trackCourseEvent: args })),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
useCourseTrackingEvent: jest.fn((...args) => ({ trackCourseEvent: args })),
|
||||
useIsMasquerading: jest.fn(),
|
||||
}));
|
||||
jest.mock('./hooks', () => ({
|
||||
useEmailSettings: jest.fn(),
|
||||
useCardSocialSettingsData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const props = {
|
||||
cardId: 'test-card-id',
|
||||
emailSettings: { show: jest.fn() },
|
||||
@@ -61,23 +54,25 @@ const socialShare = {
|
||||
|
||||
const mockHooks = (returnVals = {}) => {
|
||||
mockHook(
|
||||
reduxHooks.useCardEnrollmentData,
|
||||
useCourseData,
|
||||
{
|
||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||
isExecEd2UCourse: !!returnVals.isExecEd2UCourse,
|
||||
enrollment: {
|
||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||
mode: returnVals.isExecEd2UCourse ? 'exec-ed-2u' : 'standard',
|
||||
},
|
||||
course: { courseName },
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
mockHook(reduxHooks.useCardCourseData, { courseName }, { isCardHook: true });
|
||||
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
|
||||
mockHook(
|
||||
reduxHooks.useCardSocialSettingsData,
|
||||
useCardSocialSettingsData,
|
||||
{
|
||||
facebook: { ...socialShare.facebook, isEnabled: !!returnVals.facebook?.isEnabled },
|
||||
twitter: { ...socialShare.twitter, isEnabled: !!returnVals.twitter?.isEnabled },
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
mockHook(useIsMasquerading, !!returnVals.isMasquerading);
|
||||
};
|
||||
|
||||
const renderComponent = () => render(<IntlProvider locale="en"><SocialShareMenu {...props} /></IntlProvider>);
|
||||
@@ -91,13 +86,12 @@ describe('SocialShareMenu', () => {
|
||||
it('initializes local hooks', () => {
|
||||
when(useEmailSettings).expectCalledWith();
|
||||
});
|
||||
it('initializes redux hook data ', () => {
|
||||
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useCardCourseData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useCardSocialSettingsData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
||||
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter');
|
||||
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook');
|
||||
it('initializes hook data ', () => {
|
||||
when(useCourseData).expectCalledWith(props.cardId);
|
||||
when(useCardSocialSettingsData).expectCalledWith(props.cardId);
|
||||
when(useIsMasquerading).expectCalledWith();
|
||||
when(useCourseTrackingEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter');
|
||||
when(useCourseTrackingEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook');
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||
import { useState } from 'react';
|
||||
import { StrictDict } from 'utils';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
isUnenrollConfirmVisible: (val) => useState(val), // eslint-disable-line
|
||||
@@ -27,7 +28,7 @@ export const useEmailSettings = () => {
|
||||
};
|
||||
|
||||
export const useHandleToggleDropdown = (cardId) => {
|
||||
const trackCourseEvent = reduxHooks.useTrackCourseEvent(
|
||||
const trackCourseEvent = useCourseTrackingEvent(
|
||||
track.course.courseOptionsDropdownClicked,
|
||||
cardId,
|
||||
);
|
||||
@@ -36,10 +37,30 @@ export const useHandleToggleDropdown = (cardId) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useCardSocialSettingsData = (cardId) => {
|
||||
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||
const courseData = useCourseData(cardId);
|
||||
const socialShareSettings = learnerHomeData?.socialShareSettings;
|
||||
const { socialShareUrl } = courseData?.course || {};
|
||||
const defaultSettings = { isEnabled: false, shareUrl: '' };
|
||||
|
||||
if (!socialShareSettings) {
|
||||
return { facebook: defaultSettings, twitter: defaultSettings };
|
||||
}
|
||||
const { facebook, twitter } = socialShareSettings;
|
||||
const loadSettings = (target) => ({
|
||||
isEnabled: target.isEnabled,
|
||||
shareUrl: `${socialShareUrl}?${target.utmParams}`,
|
||||
});
|
||||
return { facebook: loadSettings(facebook), twitter: loadSettings(twitter) };
|
||||
};
|
||||
|
||||
export const useOptionVisibility = (cardId) => {
|
||||
const { isEnrolled, isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
|
||||
const { isEarned } = reduxHooks.useCardCertificateData(cardId);
|
||||
const courseData = useCourseData(cardId);
|
||||
const isEmailEnabled = courseData?.enrollment?.isEmailEnabled ?? false;
|
||||
const isEnrolled = courseData?.enrollment?.isEnrolled ?? false;
|
||||
const { twitter, facebook } = useCardSocialSettingsData(cardId);
|
||||
const isEarned = courseData?.certificate?.isEarned ?? false;
|
||||
|
||||
const shouldShowUnenrollItem = isEnrolled && !isEarned;
|
||||
const shouldShowDropdown = (
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import track from 'tracking';
|
||||
import { MockUseState } from 'testUtils';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCertificateData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardSocialSettingsData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
useCourseTrackingEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
const trackCourseEvent = jest.fn();
|
||||
reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
|
||||
useCourseTrackingEvent.mockReturnValue(trackCourseEvent);
|
||||
const cardId = 'test-card-id';
|
||||
let out;
|
||||
|
||||
@@ -71,7 +72,7 @@ describe('CourseCardMenu hooks', () => {
|
||||
beforeEach(() => { out = hooks.useHandleToggleDropdown(cardId); });
|
||||
describe('behavior', () => {
|
||||
it('initializes course event tracker with event name and card ID', () => {
|
||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
||||
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||
track.course.courseOptionsDropdownClicked,
|
||||
cardId,
|
||||
);
|
||||
@@ -88,55 +89,61 @@ describe('CourseCardMenu hooks', () => {
|
||||
});
|
||||
|
||||
describe('useOptionVisibility', () => {
|
||||
const mockReduxHooks = (returnVals = {}) => {
|
||||
reduxHooks.useCardSocialSettingsData.mockReturnValueOnce({
|
||||
facebook: { isEnabled: !!returnVals.facebook?.isEnabled },
|
||||
twitter: { isEnabled: !!returnVals.twitter?.isEnabled },
|
||||
const mockHooks = (returnVals = {}) => {
|
||||
useInitializeLearnerHome.mockReturnValue({
|
||||
data: {
|
||||
socialShareSettings: {
|
||||
facebook: { isEnabled: !!returnVals.facebook?.isEnabled },
|
||||
twitter: { isEnabled: !!returnVals.twitter?.isEnabled },
|
||||
},
|
||||
},
|
||||
});
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
isEnrolled: !!returnVals.isEnrolled,
|
||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||
});
|
||||
reduxHooks.useCardCertificateData.mockReturnValueOnce({
|
||||
isEarned: !!returnVals.isEarned,
|
||||
useCourseData.mockReturnValue({
|
||||
enrollment: {
|
||||
isEnrolled: !!returnVals.isEnrolled,
|
||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||
},
|
||||
certificate: {
|
||||
isEarned: !!returnVals.isEarned,
|
||||
},
|
||||
});
|
||||
};
|
||||
describe('shouldShowUnenrollItem', () => {
|
||||
it('returns true if enrolled and not earned', () => {
|
||||
mockReduxHooks({ isEnrolled: true });
|
||||
mockHooks({ isEnrolled: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(true);
|
||||
});
|
||||
it('returns false if not enrolled', () => {
|
||||
mockReduxHooks();
|
||||
mockHooks();
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(false);
|
||||
});
|
||||
it('returns false if enrolled but also earned', () => {
|
||||
mockReduxHooks({ isEarned: true });
|
||||
mockHooks({ isEarned: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowUnenrollItem).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldShowDropdown', () => {
|
||||
it('returns false if not enrolled and both email and socials are disabled', () => {
|
||||
mockReduxHooks();
|
||||
mockHooks();
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(false);
|
||||
});
|
||||
it('returns false if enrolled but already earned, and both email and socials are disabled', () => {
|
||||
mockReduxHooks({ isEnrolled: true, isEarned: true });
|
||||
mockHooks({ isEnrolled: true, isEarned: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(false);
|
||||
});
|
||||
it('returns true if either social is enabled', () => {
|
||||
mockReduxHooks({ facebook: { isEnabled: true } });
|
||||
mockHooks({ facebook: { isEnabled: true } });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
mockReduxHooks({ twitter: { isEnabled: true } });
|
||||
mockHooks({ twitter: { isEnabled: true } });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
});
|
||||
it('returns true if email is enabled', () => {
|
||||
mockReduxHooks({ isEmailEnabled: true });
|
||||
mockHooks({ isEmailEnabled: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
});
|
||||
it('returns true if enrolled and not earned', () => {
|
||||
mockReduxHooks({ isEnrolled: true });
|
||||
mockHooks({ isEnrolled: true });
|
||||
expect(hooks.useOptionVisibility(cardId).shouldShowDropdown).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MoreVert } from '@openedx/paragon/icons';
|
||||
|
||||
import EmailSettingsModal from 'containers/EmailSettingsModal';
|
||||
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import SocialShareMenu from './SocialShareMenu';
|
||||
import {
|
||||
useEmailSettings,
|
||||
@@ -23,13 +23,15 @@ export const testIds = {
|
||||
|
||||
export const CourseCardMenu = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const courseData = useCourseData(cardId);
|
||||
|
||||
const isEmailEnabled = courseData?.enrollment?.isEmailEnabled ?? false;
|
||||
|
||||
const emailSettings = useEmailSettings();
|
||||
const unenrollModal = useUnenrollData();
|
||||
const handleToggleDropdown = useHandleToggleDropdown(cardId);
|
||||
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const isMasquerading = useIsMasquerading();
|
||||
|
||||
if (!shouldShowDropdown) {
|
||||
return null;
|
||||
|
||||
@@ -4,16 +4,14 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import * as hooks from './hooks';
|
||||
import CourseCardMenu from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
useIsMasquerading: jest.fn(),
|
||||
}));
|
||||
jest.mock('./SocialShareMenu', () => jest.fn(() => <div>SocialShareMenu</div>));
|
||||
jest.mock('containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
|
||||
@@ -25,10 +23,6 @@ jest.mock('./hooks', () => ({
|
||||
useOptionVisibility: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const props = {
|
||||
cardId: 'test-card-id',
|
||||
};
|
||||
@@ -73,10 +67,14 @@ const mockHooks = (returnVals = {}) => {
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
|
||||
mockHook(useIsMasquerading, !!returnVals.isMasquerading);
|
||||
mockHook(
|
||||
reduxHooks.useCardEnrollmentData,
|
||||
{ isEmailEnabled: !!returnVals.isEmailEnabled },
|
||||
useCourseData,
|
||||
{
|
||||
enrollment: {
|
||||
isEmailEnabled: !!returnVals.isEmailEnabled,
|
||||
},
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
};
|
||||
@@ -91,13 +89,10 @@ describe('CourseCardMenu', () => {
|
||||
});
|
||||
it('initializes local hooks', () => {
|
||||
when(hooks.useEmailSettings).expectCalledWith();
|
||||
when(hooks.useUnenrollData).expectCalledWith();
|
||||
when(hooks.useHandleToggleDropdown).expectCalledWith(props.cardId);
|
||||
when(hooks.useOptionVisibility).expectCalledWith(props.cardId);
|
||||
});
|
||||
it('initializes redux hook data ', () => {
|
||||
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
||||
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
||||
it('initializes hook data ', () => {
|
||||
when(useIsMasquerading).expectCalledWith();
|
||||
when(useCourseData).expectCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
|
||||
@@ -2,15 +2,16 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||
import useActionDisabledState from './hooks';
|
||||
|
||||
const { courseTitleClicked } = track.course;
|
||||
|
||||
export const CourseCardTitle = ({ cardId }) => {
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const handleTitleClicked = reduxHooks.useTrackCourseEvent(
|
||||
const courseData = useCourseData(cardId);
|
||||
const courseName = courseData?.course?.courseName;
|
||||
const homeUrl = courseData?.courseRun?.homeUrl;
|
||||
const handleTitleClicked = useCourseTrackingEvent(
|
||||
courseTitleClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from './hooks';
|
||||
import CourseCardTitle from './CourseCardTitle';
|
||||
@@ -12,19 +12,12 @@ jest.mock('tracking', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
useCourseTrackingEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
describe('CourseCardTitle', () => {
|
||||
const props = {
|
||||
cardId: 'test-card-id',
|
||||
@@ -36,9 +29,11 @@ describe('CourseCardTitle', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
reduxHooks.useCardCourseData.mockReturnValue({ courseName });
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
reduxHooks.useTrackCourseEvent.mockReturnValue(handleTitleClick);
|
||||
useCourseData.mockReturnValue({
|
||||
course: { courseName },
|
||||
courseRun: { homeUrl },
|
||||
});
|
||||
useCourseTrackingEvent.mockReturnValue(handleTitleClick);
|
||||
});
|
||||
|
||||
it('renders course name as link when not disabled', async () => {
|
||||
@@ -66,9 +61,8 @@ describe('CourseCardTitle', () => {
|
||||
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
|
||||
render(<CourseCardTitle {...props} />);
|
||||
|
||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
||||
expect(useCourseData).toHaveBeenCalledWith(props.cardId);
|
||||
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||
track.course.courseTitleClicked,
|
||||
props.cardId,
|
||||
homeUrl,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import * as module from './hooks';
|
||||
@@ -14,7 +14,8 @@ export const state = StrictDict({
|
||||
export const useRelatedProgramsBadgeData = ({ cardId }) => {
|
||||
const [isOpen, setIsOpen] = module.state.isOpen(false);
|
||||
const { formatMessage } = useIntl();
|
||||
const numPrograms = reduxHooks.useCardRelatedProgramsData(cardId).length;
|
||||
const courseData = useCourseData(cardId);
|
||||
const numPrograms = courseData?.programs?.relatedPrograms?.length || 0;
|
||||
let programsMessage = '';
|
||||
if (numPrograms) {
|
||||
programsMessage = formatMessage(
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData } from 'hooks';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardRelatedProgramsData: jest.fn(),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const { formatMessage } = jest.requireActual('testUtils');
|
||||
return {
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: () => ({
|
||||
formatMessage,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
@@ -29,8 +37,10 @@ describe('RelatedProgramsBadge hooks', () => {
|
||||
describe('useRelatedProgramsBadgeData', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({
|
||||
length: numPrograms,
|
||||
useCourseData.mockReturnValue({
|
||||
programs: {
|
||||
relatedPrograms: new Array(numPrograms).fill({}),
|
||||
},
|
||||
});
|
||||
out = hooks.useRelatedProgramsBadgeData({ cardId });
|
||||
});
|
||||
@@ -54,12 +64,12 @@ describe('RelatedProgramsBadge hooks', () => {
|
||||
expect(out.numPrograms).toEqual(numPrograms);
|
||||
});
|
||||
test('returns empty programsMessage if no programs', () => {
|
||||
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 0 });
|
||||
useCourseData.mockReturnValueOnce({ programs: { relatedPrograms: [] } });
|
||||
out = hooks.useRelatedProgramsBadgeData({ cardId });
|
||||
expect(out.programsMessage).toEqual('');
|
||||
});
|
||||
test('returns badgeLabelSingular programsMessage if 1 programs', () => {
|
||||
reduxHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 1 });
|
||||
useCourseData.mockReturnValueOnce({ programs: { relatedPrograms: [{}] } });
|
||||
out = hooks.useRelatedProgramsBadgeData({ cardId });
|
||||
expect(out.programsMessage).toEqual(formatMessage(
|
||||
messages.badgeLabelSingular,
|
||||
|
||||
@@ -7,10 +7,6 @@ import RelatedProgramsBadge from '.';
|
||||
jest.mock('containers/RelatedProgramsModal', () => 'RelatedProgramsModal');
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const hookProps = {
|
||||
isOpen: true,
|
||||
openModal: jest.fn().mockName('useRelatedProgramsBadge.openModal'),
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useCourseData, useEntitlementInfo, useIsMasquerading } from 'hooks';
|
||||
|
||||
export const useActionDisabledState = (cardId) => {
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const courseData = useCourseData(cardId);
|
||||
const isMasquerading = useIsMasquerading();
|
||||
|
||||
const {
|
||||
hasAccess, isAudit, isAuditAccessExpired,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
isAudit, isAuditAccessExpired,
|
||||
} = courseData.enrollment || {};
|
||||
const { isStaff, hasUnmetPrereqs, isTooEarly } = courseData.enrollment?.coursewareAccess || {};
|
||||
const hasAccess = isStaff || !(hasUnmetPrereqs || isTooEarly);
|
||||
const {
|
||||
isEntitlement, isFulfilled, canChange, hasSessions,
|
||||
} = reduxHooks.useCardEntitlementData(cardId);
|
||||
|
||||
const { resumeUrl, homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
} = useEntitlementInfo(courseData);
|
||||
|
||||
const { resumeUrl, homeUrl } = courseData.courseRun || {};
|
||||
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
|
||||
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useMemo: jest.fn((fn) => fn()),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
},
|
||||
...jest.requireActual('hooks'),
|
||||
useCourseData: jest.fn(),
|
||||
useIsMasquerading: jest.fn(),
|
||||
}));
|
||||
|
||||
const cardId = 'my-test-course-number';
|
||||
@@ -38,25 +39,38 @@ describe('useActionDisabledState', () => {
|
||||
isAuditAccessExpired,
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
availableSessions,
|
||||
} = { ...defaultData, ...args };
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
hasAccess,
|
||||
isAudit,
|
||||
isAuditAccessExpired,
|
||||
});
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
hasSessions,
|
||||
});
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
useIsMasquerading.mockReturnValue(isMasquerading);
|
||||
useCourseData.mockReturnValue({
|
||||
enrollment: {
|
||||
hasAccess,
|
||||
isAudit,
|
||||
isAuditAccessExpired,
|
||||
coursewareAccess: {
|
||||
isStaff: false,
|
||||
hasUnmetPrereqs: !hasAccess,
|
||||
isTooEarly: !hasAccess,
|
||||
},
|
||||
},
|
||||
entitlement: isEntitlement ? {
|
||||
isEntitlement: true,
|
||||
isFulfilled,
|
||||
canChange,
|
||||
hasSessions,
|
||||
availableSessions,
|
||||
} : {},
|
||||
courseRun: {
|
||||
resumeUrl,
|
||||
homeUrl,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const runHook = () => hooks.useActionDisabledState(cardId);
|
||||
describe('disableBeginCourse', () => {
|
||||
const testDisabled = (data, expected) => {
|
||||
@@ -142,6 +156,7 @@ describe('useActionDisabledState', () => {
|
||||
hasAccess: true,
|
||||
canChange: true,
|
||||
hasSessions: true,
|
||||
availableSessions: ['session1'],
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
export const useIsCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
return width < breakpoints.small.maxWidth;
|
||||
};
|
||||
|
||||
export const useCardData = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { title, bannerImgSrc } = reduxHooks.useCardCourseData(cardId);
|
||||
const { isEnrolled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
|
||||
return {
|
||||
isEnrolled,
|
||||
title,
|
||||
bannerImgSrc,
|
||||
formatMessage,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCardData;
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useWindowSize } from '@openedx/paragon';
|
||||
import { useIsCollapsed } from './hooks';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
jest.mock('@openedx/paragon', () => ({
|
||||
useWindowSize: jest.fn(),
|
||||
breakpoints: {
|
||||
small: {
|
||||
maxWidth: 576,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const cardId = 'my-test-course-number';
|
||||
|
||||
describe('CourseCard hooks', () => {
|
||||
let out;
|
||||
const { formatMessage } = useIntl();
|
||||
beforeEach(() => {
|
||||
describe('useIsCollapsed', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useCardData', () => {
|
||||
const courseData = {
|
||||
title: 'fake-title',
|
||||
bannerImgSrc: 'my-banner-url',
|
||||
};
|
||||
const runHook = ({ course = {} }) => {
|
||||
reduxHooks.useCardCourseData.mockReturnValueOnce({
|
||||
...courseData,
|
||||
...course,
|
||||
});
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: 'test-is-enrolled' });
|
||||
out = hooks.useCardData({ cardId });
|
||||
};
|
||||
beforeEach(() => {
|
||||
runHook({});
|
||||
});
|
||||
it('forwards formatMessage from useIntl', () => {
|
||||
expect(out.formatMessage).toEqual(formatMessage);
|
||||
});
|
||||
it('passes course title and banner URL form course data', () => {
|
||||
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
|
||||
expect(out.title).toEqual(courseData.title);
|
||||
expect(out.bannerImgSrc).toEqual(courseData.bannerImgSrc);
|
||||
});
|
||||
it('should return true when window width is smaller than small breakpoint', () => {
|
||||
useWindowSize.mockReturnValue({ width: 500 });
|
||||
const { result } = renderHook(() => useIsCollapsed());
|
||||
expect(result.current).toBe(true);
|
||||
expect(useWindowSize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when window width is larger than small breakpoint', () => {
|
||||
useWindowSize.mockReturnValue({ width: 800 });
|
||||
const { result } = renderHook(() => useIsCollapsed());
|
||||
expect(result.current).toBe(false);
|
||||
expect(useWindowSize).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,10 +24,6 @@ jest.mock('./components/CourseCardActions', () => jest.fn(() => <div>CourseCardA
|
||||
jest.mock('./components/CourseCardDetails', () => jest.fn(() => <div>CourseCardDetails</div>));
|
||||
jest.mock('./components/CourseCardTitle', () => jest.fn(() => <div>CourseCardTitle</div>));
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
describe('CourseCard component', () => {
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Button, Chip } from '@openedx/paragon';
|
||||
import { CloseSmall } from '@openedx/paragon/icons';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useFilters } from 'data/context';
|
||||
|
||||
import messages from './messages';
|
||||
import './index.scss';
|
||||
|
||||
export const ActiveCourseFilters = ({
|
||||
filters,
|
||||
handleRemoveFilter,
|
||||
}) => {
|
||||
export const ActiveCourseFilters = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const clearFilters = reduxHooks.useClearFilters();
|
||||
const { filters, clearFilters, removeFilter } = useFilters();
|
||||
|
||||
return (
|
||||
<div id="course-list-active-filters">
|
||||
{filters.map(filter => (
|
||||
<Chip
|
||||
key={filter}
|
||||
iconAfter={CloseSmall}
|
||||
onClick={handleRemoveFilter(filter)}
|
||||
onClick={() => removeFilter(filter)}
|
||||
>
|
||||
{formatMessage(messages[filter])}
|
||||
</Chip>
|
||||
@@ -32,9 +29,5 @@ export const ActiveCourseFilters = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
ActiveCourseFilters.propTypes = {
|
||||
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
handleRemoveFilter: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ActiveCourseFilters;
|
||||
|
||||
@@ -1,32 +1,54 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { useFilters } from 'data/context';
|
||||
|
||||
import { FilterKeys } from 'data/constants/app';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ActiveCourseFilters from './ActiveCourseFilters';
|
||||
import messages from './messages';
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const filters = Object.values(FilterKeys);
|
||||
|
||||
jest.mock('data/context', () => ({
|
||||
useFilters: jest.fn(),
|
||||
}));
|
||||
|
||||
const removeFiltersMock = jest.fn().mockName('removeFilter');
|
||||
const clearFiltersMock = jest.fn().mockName('clearFilters');
|
||||
useFilters.mockReturnValue({
|
||||
filters,
|
||||
removeFilter: removeFiltersMock,
|
||||
clearFilters: clearFiltersMock,
|
||||
});
|
||||
|
||||
describe('ActiveCourseFilters', () => {
|
||||
const props = {
|
||||
filters,
|
||||
handleRemoveFilter: jest.fn().mockName('handleRemoveFilter'),
|
||||
};
|
||||
it('renders chips correctly', () => {
|
||||
render(<IntlProvider locale="en"><ActiveCourseFilters {...props} /></IntlProvider>);
|
||||
render(<IntlProvider locale="en"><ActiveCourseFilters /></IntlProvider>);
|
||||
filters.map((key) => {
|
||||
const chip = screen.getByText(formatMessage(messages[key]));
|
||||
return expect(chip).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('renders button correctly', () => {
|
||||
render(<IntlProvider locale="en"><ActiveCourseFilters {...props} /></IntlProvider>);
|
||||
render(<IntlProvider locale="en"><ActiveCourseFilters /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: formatMessage(messages.clearAll) });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
it('should call onClick when button is clicked remove filter', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<IntlProvider locale="en"><ActiveCourseFilters /></IntlProvider>);
|
||||
const removeButton = screen.getByRole('button', { name: formatMessage(messages[filters[0]]) });
|
||||
await user.click(removeButton);
|
||||
expect(removeFiltersMock).toHaveBeenCalledTimes(1);
|
||||
expect(removeFiltersMock).toHaveBeenCalledWith(filters[0]);
|
||||
});
|
||||
it('should call onClick when button is clicked clear all filters', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<IntlProvider locale="en"><ActiveCourseFilters /></IntlProvider>);
|
||||
screen.debug();
|
||||
const clearAllButton = screen.getByRole('button', { name: formatMessage(messages.clearAll) });
|
||||
await user.click(clearAllButton);
|
||||
expect(clearFiltersMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
@@ -14,44 +13,51 @@ import {
|
||||
} from '@openedx/paragon';
|
||||
import { Close, Tune } from '@openedx/paragon/icons';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import { useFilters } from 'data/context';
|
||||
import FilterForm from './components/FilterForm';
|
||||
import SortForm from './components/SortForm';
|
||||
import useCourseFilterControlsData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const CourseFilterControls = ({
|
||||
sortBy,
|
||||
setSortBy,
|
||||
filters,
|
||||
}) => {
|
||||
export const CourseFilterControls = () => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [targetRef, setTargetRef] = React.useState(null);
|
||||
const { formatMessage } = useIntl();
|
||||
const hasCourses = reduxHooks.useHasCourses();
|
||||
const { data } = useInitializeLearnerHome();
|
||||
const hasCourses = React.useMemo(() => data?.courses?.length > 0, [data]);
|
||||
const {
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
target,
|
||||
setTarget,
|
||||
handleFilterChange,
|
||||
handleSortChange,
|
||||
} = useCourseFilterControlsData({
|
||||
filters,
|
||||
setSortBy,
|
||||
});
|
||||
filters, sortBy, setSortBy, addFilter, removeFilter,
|
||||
} = useFilters();
|
||||
|
||||
const openFiltersOptions = () => {
|
||||
track.filter.filterClicked();
|
||||
setIsOpen(true);
|
||||
};
|
||||
const closeFiltersOptions = () => {
|
||||
track.filter.filterOptionSelected(filters);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleSortChange = (event) => {
|
||||
setSortBy(event.target.value);
|
||||
};
|
||||
|
||||
const handleFilterChange = ({ target: { checked, value } }) => {
|
||||
const update = checked ? addFilter : removeFilter;
|
||||
update(value);
|
||||
};
|
||||
const { width } = useWindowSize();
|
||||
const isMobile = width < breakpoints.small.minWidth;
|
||||
|
||||
return (
|
||||
<div id="course-filter-controls">
|
||||
<Button
|
||||
ref={setTarget}
|
||||
ref={setTargetRef}
|
||||
variant="outline-primary"
|
||||
iconBefore={Tune}
|
||||
onClick={open}
|
||||
onClick={openFiltersOptions}
|
||||
disabled={!hasCourses}
|
||||
>
|
||||
{formatMessage(messages.refine)}
|
||||
@@ -63,7 +69,7 @@ export const CourseFilterControls = ({
|
||||
className="w-75"
|
||||
position="left"
|
||||
show={isOpen}
|
||||
onClose={close}
|
||||
onClose={closeFiltersOptions}
|
||||
>
|
||||
<div className="p-1 mr-3">
|
||||
<b>{formatMessage(messages.refine)}</b>
|
||||
@@ -76,16 +82,16 @@ export const CourseFilterControls = ({
|
||||
<SortForm {...{ sortBy, handleSortChange }} />
|
||||
</div>
|
||||
<div className="pgn__modal-close-container">
|
||||
<ModalCloseButton variant="tertiary" onClick={close}>
|
||||
<ModalCloseButton variant="tertiary" onClick={closeFiltersOptions}>
|
||||
<Icon src={Close} />
|
||||
</ModalCloseButton>
|
||||
</div>
|
||||
</Sheet>
|
||||
) : (
|
||||
<ModalPopup
|
||||
positionRef={target}
|
||||
positionRef={targetRef}
|
||||
isOpen={isOpen}
|
||||
onClose={close}
|
||||
onClose={closeFiltersOptions}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<div
|
||||
@@ -106,10 +112,5 @@ export const CourseFilterControls = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
CourseFilterControls.propTypes = {
|
||||
sortBy: PropTypes.string.isRequired,
|
||||
setSortBy: PropTypes.func.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
export default CourseFilterControls;
|
||||
|
||||
@@ -1,79 +1,150 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { FilterKeys, SortKeys } from 'data/constants/app';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import { useFilters } from 'data/context';
|
||||
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import messages from './messages';
|
||||
import CourseFilterControls from './CourseFilterControls';
|
||||
import useCourseFilterControlsData from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: { useHasCourses: jest.fn() },
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn().mockReturnValue({ data: { courses: [1, 2, 3] } }),
|
||||
}));
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
jest.mock('@openedx/paragon', () => ({
|
||||
...jest.requireActual('@openedx/paragon'),
|
||||
useWindowSize: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
filter: {
|
||||
filterClicked: jest.fn().mockName('segment.filterClicked'),
|
||||
filterOptionSelected: jest.fn().mockName('segment.filterOptionSelected'),
|
||||
},
|
||||
}));
|
||||
|
||||
const filters = Object.values(FilterKeys);
|
||||
|
||||
const mockControlsData = {
|
||||
isOpen: false,
|
||||
open: jest.fn().mockName('open'),
|
||||
close: jest.fn().mockName('close'),
|
||||
target: 'target-test',
|
||||
setTarget: jest.fn(),
|
||||
handleFilterChange: jest.fn().mockName('handleFilterChange'),
|
||||
handleSortChange: jest.fn().mockName('handleSortChange'),
|
||||
};
|
||||
jest.mock('data/context', () => ({
|
||||
useFilters: jest.fn(),
|
||||
}));
|
||||
|
||||
const setSortByMock = jest.fn().mockName('setSortBy');
|
||||
useFilters.mockReturnValue({
|
||||
filters,
|
||||
removeFilter: jest.fn().mockName('removeFilter'),
|
||||
clearFilters: jest.fn().mockName('clearFilters'),
|
||||
setSortBy: setSortByMock,
|
||||
addFilter: jest.fn().mockName('addFilter'),
|
||||
});
|
||||
|
||||
describe('CourseFilterControls', () => {
|
||||
const props = {
|
||||
sortBy: SortKeys.enrolled,
|
||||
setSortBy: jest.fn().mockName('setSortBy'),
|
||||
filters,
|
||||
};
|
||||
|
||||
describe('mobile and open', () => {
|
||||
it('should render sheet', () => {
|
||||
reduxHooks.useHasCourses.mockReturnValue(true);
|
||||
useCourseFilterControlsData.mockReturnValue({ ...mockControlsData, isOpen: true });
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth - 1 });
|
||||
render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
|
||||
const sheet = screen.getByRole('presentation', { hidden: true });
|
||||
expect(sheet).toBeInTheDocument();
|
||||
expect(sheet.parentElement).toHaveClass('sheet-container');
|
||||
it('should render sheet', async () => {
|
||||
const user = userEvent.setup();
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth - 1 });
|
||||
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||
const filtersButton = screen.getByRole('button', { name: 'Refine' });
|
||||
await user.click(filtersButton);
|
||||
await waitFor(() => {
|
||||
const sheet = screen.getByRole('presentation', { hidden: true });
|
||||
expect(sheet).toBeInTheDocument();
|
||||
expect(sheet.parentElement).toHaveClass('sheet-container');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('is not mobile', () => {
|
||||
it('should have button disabled', () => {
|
||||
reduxHooks.useHasCourses.mockReturnValue(true);
|
||||
useCourseFilterControlsData.mockReturnValue({ ...mockControlsData, isOpen: true });
|
||||
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
|
||||
render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
|
||||
const filterForm = screen.getByText(messages.courseStatus.defaultMessage);
|
||||
const modal = filterForm.closest('div.pgn__modal-popup__tooltip');
|
||||
expect(modal).toBeInTheDocument();
|
||||
it('should have button disabled', async () => {
|
||||
const user = userEvent.setup();
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||
const filtersButton = screen.getByRole('button', { name: 'Refine' });
|
||||
await user.click(filtersButton);
|
||||
await waitFor(() => {
|
||||
const filterForm = screen.getByText(messages.courseStatus.defaultMessage);
|
||||
const modal = filterForm.closest('div.pgn__modal-popup__tooltip');
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('no courses', () => {
|
||||
it('should have button disabled', () => {
|
||||
reduxHooks.useHasCourses.mockReturnValue(false);
|
||||
useCourseFilterControlsData.mockReturnValue(mockControlsData);
|
||||
useInitializeLearnerHome.mockReturnValue({ data: { courses: [] } });
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||
render(<IntlProvider locale="en"><CourseFilterControls {...props} /></IntlProvider>);
|
||||
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: formatMessage(messages.refine) });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
describe('with courses', () => {
|
||||
it('should have button enabled', () => {
|
||||
useInitializeLearnerHome.mockReturnValue({ data: { courses: [1, 2, 3] } });
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: formatMessage(messages.refine) });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toBeEnabled();
|
||||
});
|
||||
it('should call setSortBy on sort change', async () => {
|
||||
const user = userEvent.setup();
|
||||
useInitializeLearnerHome.mockReturnValue({ data: { courses: [1, 2, 3] } });
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||
const filtersButton = screen.getByRole('button', { name: 'Refine' });
|
||||
await user.click(filtersButton);
|
||||
await waitFor(async () => {
|
||||
const sortRadio = screen.getByRole('radio', { name: formatMessage(messages.sortTitle) });
|
||||
await user.click(sortRadio);
|
||||
expect(setSortByMock).toHaveBeenCalledWith(SortKeys.title);
|
||||
});
|
||||
});
|
||||
it('should call addFilter on filter check', async () => {
|
||||
const user = userEvent.setup();
|
||||
const addFilterMock = jest.fn().mockName('addFilter');
|
||||
const removeFilterMock = jest.fn().mockName('removeFilter');
|
||||
useFilters.mockReturnValue({
|
||||
filters: [],
|
||||
removeFilter: removeFilterMock,
|
||||
clearFilters: jest.fn().mockName('clearFilters'),
|
||||
setSortBy: jest.fn().mockName('setSortBy'),
|
||||
addFilter: addFilterMock,
|
||||
});
|
||||
useInitializeLearnerHome.mockReturnValue({ data: { courses: [1, 2, 3] } });
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||
const filtersButton = screen.getByRole('button', { name: 'Refine' });
|
||||
await user.click(filtersButton);
|
||||
await waitFor(async () => {
|
||||
const filterCheckbox = screen.getByText('In-Progress');
|
||||
await user.click(filterCheckbox);
|
||||
expect(addFilterMock).toHaveBeenCalledWith(FilterKeys.inProgress);
|
||||
});
|
||||
});
|
||||
it('should call removeFilter on filter uncheck', async () => {
|
||||
const user = userEvent.setup();
|
||||
const addFilterMock = jest.fn().mockName('addFilter');
|
||||
const removeFilterMock = jest.fn().mockName('removeFilter');
|
||||
useFilters.mockReturnValue({
|
||||
filters: [FilterKeys.inProgress],
|
||||
removeFilter: removeFilterMock,
|
||||
clearFilters: jest.fn().mockName('clearFilters'),
|
||||
setSortBy: jest.fn().mockName('setSortBy'),
|
||||
addFilter: addFilterMock,
|
||||
});
|
||||
useInitializeLearnerHome.mockReturnValue({ data: { courses: [1, 2, 3] } });
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.small.minWidth });
|
||||
render(<IntlProvider locale="en"><CourseFilterControls /></IntlProvider>);
|
||||
const filtersButton = screen.getByRole('button', { name: 'Refine' });
|
||||
await user.click(filtersButton);
|
||||
await waitFor(async () => {
|
||||
const filterCheckbox = screen.getByText('In-Progress');
|
||||
await user.click(filterCheckbox);
|
||||
expect(removeFilterMock).toHaveBeenCalledWith(FilterKeys.inProgress);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,10 +6,6 @@ import { FilterKeys } from 'data/constants/app';
|
||||
import Checkbox from './Checkbox';
|
||||
import messages from '../messages';
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
describe('Checkbox', () => {
|
||||
describe('renders correctly', () => {
|
||||
Object.keys(FilterKeys).forEach((filterKey) => {
|
||||
|
||||
@@ -5,10 +5,6 @@ import { FilterKeys } from 'data/constants/app';
|
||||
import { FilterForm, filterOrder } from './FilterForm';
|
||||
import messages from '../messages';
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
const mockHandleFilterChange = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
|
||||
@@ -6,10 +6,6 @@ import { SortKeys } from 'data/constants/app';
|
||||
import SortForm from './SortForm';
|
||||
import messages from '../messages';
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
describe('SortForm', () => {
|
||||
const props = {
|
||||
handleSortChange: jest.fn().mockName('handleSortChange'),
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
target: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets up a toggle for the modal as well as helper functions for handling changes to the form controls.
|
||||
*
|
||||
* @param {array} filters Currently active course filters
|
||||
* @param {function} setSortBy Set function for sorting the course list
|
||||
* @returns {object} data and functions for managing the CourseFilterControls component
|
||||
*/
|
||||
export const useCourseFilterControlsData = ({
|
||||
filters,
|
||||
setSortBy,
|
||||
}) => {
|
||||
const [isOpen, toggleOpen, toggleClose] = useToggle(false);
|
||||
const [target, setTarget] = module.state.target(null);
|
||||
|
||||
const addFilter = reduxHooks.useAddFilter();
|
||||
const removeFilter = reduxHooks.useRemoveFilter();
|
||||
|
||||
const handleFilterChange = ({ target: { checked, value } }) => {
|
||||
const update = checked ? addFilter : removeFilter;
|
||||
update(value);
|
||||
};
|
||||
const handleSortChange = ({ target: { value } }) => {
|
||||
setSortBy(value);
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
track.filter.filterClicked();
|
||||
toggleOpen();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
track.filter.filterOptionSelected(filters);
|
||||
toggleClose();
|
||||
};
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
target,
|
||||
setTarget,
|
||||
handleFilterChange,
|
||||
handleSortChange,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCourseFilterControlsData;
|
||||
@@ -1,113 +0,0 @@
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import track from 'tracking';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
filter: {
|
||||
filterClicked: jest.fn(),
|
||||
filterOptionSelected: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useAddFilter: jest.fn(),
|
||||
useRemoveFilter: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
describe('CourseFilterControls hooks', () => {
|
||||
let out;
|
||||
const filters = ['a', 'b', 'c'];
|
||||
const setSortBy = jest.fn();
|
||||
|
||||
const removeFilter = jest.fn();
|
||||
reduxHooks.useRemoveFilter.mockReturnValue(removeFilter);
|
||||
const addFilter = jest.fn();
|
||||
reduxHooks.useAddFilter.mockReturnValue(addFilter);
|
||||
|
||||
const toggleOpen = jest.fn();
|
||||
const toggleClose = jest.fn();
|
||||
|
||||
describe('state values', () => {
|
||||
state.testGetter(state.keys.target);
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useCourseFilterControlsData', () => {
|
||||
beforeEach(() => {
|
||||
useToggle.mockReturnValueOnce([false, toggleOpen, toggleClose]);
|
||||
state.mock();
|
||||
out = hooks.useCourseFilterControlsData({
|
||||
filters,
|
||||
setSortBy,
|
||||
});
|
||||
});
|
||||
afterEach(state.restore);
|
||||
|
||||
test('default state', () => {
|
||||
expect(out.isOpen).toEqual(false);
|
||||
expect(out.target).toEqual(state.stateVals.target);
|
||||
});
|
||||
|
||||
test('open calls toggleOpen and track.filter.filterClicked', () => {
|
||||
out.open();
|
||||
expect(toggleOpen).toHaveBeenCalled();
|
||||
expect(track.filter.filterClicked).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('close calls toggleClose and track.filter.filterOptionSelected', () => {
|
||||
out.close();
|
||||
expect(toggleClose).toHaveBeenCalled();
|
||||
expect(track.filter.filterOptionSelected).toHaveBeenCalledWith(filters);
|
||||
});
|
||||
|
||||
test('isOpen is true when target is set', () => {
|
||||
useToggle.mockReturnValueOnce([true, toggleOpen, toggleClose]);
|
||||
expect(out.target).toEqual(null);
|
||||
state.mockVal(state.keys.target, 'foo');
|
||||
out = hooks.useCourseFilterControlsData({
|
||||
filters,
|
||||
setSortBy,
|
||||
});
|
||||
expect(out.isOpen).toEqual(true);
|
||||
expect(out.target).toEqual('foo');
|
||||
});
|
||||
|
||||
test('handle filter change', () => {
|
||||
const value = 'a';
|
||||
out.handleFilterChange({
|
||||
target: {
|
||||
checked: true,
|
||||
value,
|
||||
},
|
||||
});
|
||||
expect(addFilter).toHaveBeenCalledWith(value);
|
||||
out.handleFilterChange({
|
||||
target: {
|
||||
checked: false,
|
||||
value,
|
||||
},
|
||||
});
|
||||
expect(removeFilter).toHaveBeenCalledWith(value);
|
||||
});
|
||||
test('handle sort change', () => {
|
||||
const value = 'a';
|
||||
out.handleSortChange({
|
||||
target: {
|
||||
value,
|
||||
},
|
||||
});
|
||||
expect(setSortBy).toHaveBeenCalledWith(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,14 +11,15 @@ import { useIsCollapsed } from './hooks';
|
||||
|
||||
export const CourseList = ({ courseListData }) => {
|
||||
const {
|
||||
filterOptions, setPageNumber, numPages, showFilters, visibleList,
|
||||
setPageNumber, numPages, visibleList, showFilters,
|
||||
} = courseListData;
|
||||
|
||||
const isCollapsed = useIsCollapsed();
|
||||
return (
|
||||
<>
|
||||
{showFilters && (
|
||||
<div id="course-list-active-filters-container">
|
||||
<ActiveCourseFilters {...filterOptions} />
|
||||
<ActiveCourseFilters />
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex flex-column flex-grow-1">
|
||||
@@ -42,7 +43,6 @@ export const CourseList = ({ courseListData }) => {
|
||||
export const courseListDataShape = PropTypes.shape({
|
||||
showFilters: PropTypes.bool.isRequired,
|
||||
visibleList: PropTypes.arrayOf(PropTypes.shape()).isRequired,
|
||||
filterOptions: PropTypes.shape().isRequired,
|
||||
numPages: PropTypes.number.isRequired,
|
||||
setPageNumber: PropTypes.func.isRequired,
|
||||
});
|
||||
|
||||
@@ -12,10 +12,6 @@ jest.mock('containers/CourseFilterControls', () => ({
|
||||
ActiveCourseFilters: jest.fn(() => <div>ActiveCourseFilters</div>),
|
||||
}));
|
||||
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@openedx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
describe('CourseList', () => {
|
||||
const defaultCourseListData = {
|
||||
filterOptions: {},
|
||||
|
||||
@@ -5,14 +5,15 @@ import { Search } from '@openedx/paragon/icons';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import emptyCourseSVG from 'assets/empty-course.svg';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import './index.scss';
|
||||
|
||||
export const NoCoursesView = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
const { data: learnerData } = useInitializeLearnerHome();
|
||||
const courseSearchUrl = learnerData?.platformSettings?.courseSearchUrl || '';
|
||||
return (
|
||||
<div
|
||||
id="no-courses-content-view"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user