Compare commits

..

80 Commits

Author SHA1 Message Date
Maxwell Frank
01a13380f2 chore(deps): removed unused dependencies 2024-08-15 18:27:16 +00:00
Justin Hynes
afe386566b chore: remove unused dependency -- axios-mock-adapter (#422) 2024-08-15 09:51:09 -04:00
Justin Hynes
d5da8ba62f chore: remove unused dependency -- axios-mock-adapter
Similarly to #421, the `axios-mock-adapter` dependency is not being used. Renovate is trying to update this from v1.x to 2.x, this PR removes the dependency instead.
2024-08-14 20:17:39 +00:00
Justin Hynes
24a3a2de65 chore: remove unused dependency (#421) 2024-08-14 16:13:49 -04:00
Justin Hynes
40f8b0e960 chore: remove unused dependency
Renovate opened a PR to update `fetch-mock` to v11.x from v9.x, when I went to see where it was used... the only reference I found to this package was in the package.json file. It seems like it's not used anymore.
2024-08-14 20:06:47 +00:00
Justin Hynes
d1813d3dcd chore(deps): update dependency copy-webpack-plugin to v12 (#408) 2024-08-14 15:08:49 -04:00
renovate[bot]
d5b02fbbb0 chore(deps): update dependency copy-webpack-plugin to v12 2024-08-14 18:00:28 +00:00
Justin Hynes
b3620a7832 chore(deps): update actions/setup-node action to v4 (#407) 2024-08-14 13:59:17 -04:00
renovate[bot]
5829e25fed chore(deps): update actions/setup-node action to v4 2024-08-14 17:48:07 +00:00
Justin Hynes
aa041245af chore(deps): update actions/checkout action to v4 (#406) 2024-08-14 13:40:06 -04:00
renovate[bot]
17aa646856 chore(deps): update actions/checkout action to v4 2024-08-12 06:56:29 +00:00
Ahtesham Quraish
7ef5d5b034 fix: replace the header with openedx header
Description:
Replace the header with openedx header
van-1914
2024-08-12 11:55:44 +05:00
renovate[bot]
68a46ac023 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.2.3 2024-08-12 06:40:36 +00:00
renovate[bot]
6310cc0452 chore(deps): update dependency @openedx/frontend-build to v14.0.15 2024-08-12 04:12:33 +00:00
renovate[bot]
9f52c61e4f fix(deps): update dependency react-router-dom to v6.26.0 2024-08-05 07:24:44 +00:00
renovate[bot]
6a62301c1c fix(deps): update dependency core-js to v3.38.0 2024-08-05 06:58:40 +00:00
renovate[bot]
e61eaa8264 fix(deps): update dependency @redux-devtools/extension to v3.3.0 2024-08-05 04:46:33 +00:00
Deborah Kaplan
c906ce0d3a fix(deps): replace dependency redux-devtools-extension with @redux-devtools/extension 3.0.0 (#314) 2024-07-30 14:58:34 -04:00
Deborah Kaplan
e9a1e3e40d chore: fixing jest
making mocked method match
2024-07-30 17:56:58 +00:00
Deborah Kaplan
f149f2c8cf chore: packkage and method name change
this renovate update changed the package. See:
https://github.com/reduxjs/redux-devtools/releases/tag/remotedev-redux-devtools-extension%403.0.0
2024-07-30 17:30:29 +00:00
renovate[bot]
3ee7c62119 fix(deps): replace dependency redux-devtools-extension with @redux-devtools/extension 3.0.0 2024-07-30 14:09:41 +00:00
Maxwell Frank
a5d1cb380d fix: remove Optimizely and surrounding components (#386) 2024-07-30 10:09:04 -04:00
renovate[bot]
e3b4e0956a fix(deps): update dependency regenerator-runtime to ^0.14.0 2024-07-29 13:48:49 +00:00
renovate[bot]
eb427f3772 fix(deps): update dependency redux-thunk to v2.4.2 2024-07-29 11:05:55 +00:00
renovate[bot]
a53c167558 fix(deps): update dependency @edx/frontend-platform to v8.1.1 2024-07-29 07:43:07 +00:00
renovate[bot]
61476422bb chore(deps): update dependency @openedx/frontend-build to v14.0.14 2024-07-29 04:30:28 +00:00
renovate[bot]
5b6c4004c7 fix(deps): update dependency redux to v4.2.1 2024-07-22 13:49:47 +00:00
renovate[bot]
d7bd32aae3 fix(deps): update dependency react-router-dom to v6.25.1 2024-07-22 12:00:05 +00:00
renovate[bot]
c14496ade9 fix(deps): update dependency react-intl to v6.6.8 2024-07-22 07:13:24 +00:00
renovate[bot]
4b117c7882 fix(deps): update dependency @openedx/paragon to v22.7.0 2024-07-22 04:58:26 +00:00
Deborah Kaplan
0162a62c56 feat: switching to 4 letter yaml extension (#388) 2024-07-17 08:46:03 -04:00
Deborah Kaplan
79ad701eca feat: switching to 4 letter yaml extension
it looks like the  processes which rely on catalog-info require the 4
letter extension.
2024-07-16 20:47:14 +00:00
renovate[bot]
3150c110a1 fix(deps): update dependency query-string to v7.1.3 2024-07-15 14:22:57 +00:00
renovate[bot]
9c3264c8a2 fix(deps): update dependency history to v5.3.0 2024-07-15 10:19:25 +00:00
renovate[bot]
95a35e17dc fix(deps): update dependency @openedx/paragon to v22.6.1 2024-07-15 06:15:11 +00:00
renovate[bot]
aa7296c3cd fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.3 2024-07-15 04:43:33 +00:00
Jason Wesson
89ae34c874 feat: change course recommendations endpoint (#371) 2024-07-08 11:19:08 -06:00
Jason Wesson
62a9cb0045 Merge branch 'master' into jwesson/update-course-recommendations-endpoint 2024-07-08 11:15:28 -06:00
renovate[bot]
a96c8fc6ab fix(deps): update dependency @edx/frontend-platform to v8.1.0 2024-07-08 14:35:17 +00:00
renovate[bot]
c0ad27077f fix(deps): update dependency prop-types to v15.8.1 2024-07-08 11:10:15 +00:00
renovate[bot]
08ead35644 fix(deps): update dependency dompurify to v2.5.6 2024-07-08 06:58:48 +00:00
renovate[bot]
362bb8b3cf fix(deps): update dependency @openedx/frontend-plugin-framework to v1.2.2 2024-07-08 04:38:24 +00:00
Jason Wesson
20eebf2f28 feat: change course recommendations endpoint 2024-07-03 16:47:53 +00:00
renovate[bot]
7e8dad41ec fix(deps): update dependency dompurify to v2.5.5 2024-07-01 11:24:42 +00:00
renovate[bot]
f6af646b80 chore(deps): update dependency @openedx/frontend-build to v14.0.10 2024-07-01 07:43:02 +00:00
Justin Hynes
3b25d04752 perf: version check workflow for lockfile updated (#361) 2024-06-25 16:02:31 -04:00
Justin Hynes
31eafb30ba Merge branch 'master' into huniafatima/lockfile-workflow-update 2024-06-25 15:59:08 -04:00
Deborah Kaplan
1705926f52 feat: changing ownership for the recommendations widget (#366) 2024-06-20 16:16:32 -04:00
Deborah Kaplan
f1128d63d7 feat: changing ownership for the recommendations widget
* changing ownership to aperture for this widget  (which is DEPRd  from
  the open source code base, and aperture is scheduled to remove it
  at some point).

FIXES: APER-3497
2024-06-20 16:17:48 +00:00
Adolfo R. Brandes
c9866af227 build: Update codecov and use token
Update codecov to the latest version and start using the org-wide token for uploads.

See https://github.com/openedx/wg-frontend/issues/179
2024-06-17 12:16:53 -03:00
renovate[bot]
a2ccda7b30 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.2.1 2024-06-17 10:40:26 +00:00
renovate[bot]
54cf4bb8fd chore(deps): update dependency @openedx/frontend-build to v14.0.9 2024-06-17 06:48:46 +00:00
Hunia Fatima
229436cddf perf: version check workflow for lockfile updated 2024-06-12 17:28:00 +05:00
Adolfo R. Brandes
e94dd56fb1 fix: Remove edX-specific reference
Remove edx-Specific reference from email confirmation banner.
2024-06-10 11:34:48 -03:00
renovate[bot]
3becef3468 fix(deps): update dependency @edx/openedx-atlas to v0.6.1 2024-06-10 10:22:25 +00:00
renovate[bot]
3570ead725 fix(deps): update dependency @edx/frontend-platform to v8.0.4 2024-06-10 07:19:45 +00:00
Bilal Qamar
17c5fd09f9 feat: updated frontend-build & frontend-platform major versions (#256) 2024-05-30 17:14:41 +05:00
Bilal Qamar
506f8f795f Merge branch master into bilalqamar95/jest-v29-upgrade 2024-05-30 17:09:44 +05:00
renovate[bot]
e73880b442 fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.2 2024-05-27 04:46:08 +00:00
Bilal Qamar
08050f458e Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2024-05-20 16:30:16 +05:00
renovate[bot]
75f19a28b7 fix(deps): update dependency core-js to v3.37.1 2024-05-20 11:12:50 +00:00
Bilal Qamar
ecc4c4c2e0 refactor: update package-lock 2024-05-20 16:08:06 +05:00
Bilal Qamar
28da100ca2 Merge branch master into bilalqamar95/jest-v29-upgrade 2024-05-20 16:07:50 +05:00
renovate[bot]
0fbac0715d fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.1 2024-05-20 07:01:59 +00:00
Adolfo R. Brandes
49f9e6e424 feat: use frontend-plugin-framework to provide a FooterSlot (#345) 2024-05-17 13:05:30 -03:00
Brian Smith
17eff1da7b feat: use frontend-plugin-framework to provide a FooterSlot 2024-05-17 10:54:53 -04:00
Muhammad Abdullah Waheed
0fcaa64a6e fix: fixed snapshots 2024-05-17 16:54:08 +05:00
Bilal Qamar
506d3f655c refactor: updated version for frontend-plugin-framework 2024-05-17 16:35:59 +05:00
Bilal Qamar
37bb54d28e refactor: updated package-lock 2024-05-16 16:42:33 +05:00
Bilal Qamar
1d53ef6153 refactor: react-unit-test-util & hotjar major version upgrades, updated snapshots 2024-05-16 16:19:50 +05:00
Bilal Qamar
59d90f5dc8 Merge branch master into bilalqamar95/jest-v29-upgrade 2024-05-16 16:19:13 +05:00
renovate[bot]
61d881b1cd fix(deps): update dependency core-js to v3.37.0 2024-05-13 09:08:02 +00:00
renovate[bot]
77adb60167 fix(deps): update dependency classnames to v2.5.1 2024-05-13 06:08:29 +00:00
renovate[bot]
b2b199e0e2 fix(deps): update dependency @openedx/paragon to v22.4.0 2024-05-13 04:07:20 +00:00
Bilal Qamar
92f712d670 feat: updated build and platform major versions, along with edx packages 2024-04-24 14:10:11 +05:00
Bilal Qamar
11b7e48080 refactor: updated snapshots for failing tests 2024-04-04 16:33:13 +05:00
Bilal Qamar
a051d712dc refactor: updated frontend-build 2024-04-04 16:32:59 +05:00
Bilal Qamar
8c76b5c689 Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2024-04-04 16:28:33 +05:00
Bilal Qamar
7fccd94d6b Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2023-12-22 16:07:09 +05:00
Bilal Qamar
c405bc63ea chore: bumped jest to v29 2023-12-05 16:17:36 +05:00
124 changed files with 5775 additions and 16741 deletions

1
.env
View File

@@ -40,5 +40,4 @@ ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
ENABLE_EDX_PERSONAL_DASHBOARD=false

View File

@@ -46,5 +46,4 @@ ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
ENABLE_EDX_PERSONAL_DASHBOARD=false

View File

@@ -45,5 +45,4 @@ ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY='SDK Key'
ENABLE_EDX_PERSONAL_DASHBOARD=true

7
.github/CODEOWNERS vendored
View File

@@ -1,8 +1 @@
# Root app is developed and owned by Aurora
* @openedx/2U-aperture
# WIDGETS and experiments are developed and owned by separate teams below
# Recommendations panel
/src/widgets/RecommendationsPanel @openedx/2U-vanguards
/src/widgets/LookingForChallengeWidget @openedx/2U-vanguards

View File

@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
@@ -39,7 +39,10 @@ jobs:
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Send failure notification
if: ${{ failure() }}

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
uses: openedx/.github/.github/workflows/lockfile-check.yml@master

View File

@@ -1,3 +1,5 @@
# TODO: MAKE GITHUB ISSUE FOR THIS — unused workflow
name: Release CI
on:
push:
@@ -10,7 +12,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -18,7 +20,7 @@ jobs:
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}

View File

@@ -69,6 +69,5 @@ module.exports = {
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
ENABLE_NOTICES: '',
CAREER_LINK_URL: '',
OPTIMIZELY_FULL_STACK_SDK_KEY: '',
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
};

57
junk.json Normal file
View File

@@ -0,0 +1,57 @@
{
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-component-header": "^5.3.1",
"@edx/frontend-enterprise-hotjar": "3.0.0",
"@edx/frontend-platform": "8.1.1",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "3.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",
"@openedx/frontend-plugin-framework": "^1.2.0",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.2.2",
"REMOVED_@redux-beacon/segment": "^1.1.0",
"@redux-devtools/extension": "3.3.0",
"@reduxjs/toolkit": "^1.6.1",
"REMOVED_@testing-library/user-event": "^13.5.0",
"UNUSED_axios": "^0.28.0",
"classnames": "^2.3.1",
"core-js": "3.38.0",
"REMOVED_dompurify": "^2.3.1",
"UNUSED_email-prop-type": "^3.0.1",
"UNUSED_file-saver": "^2.0.5",
"UNUSED_filesize": "^8.0.6",
"font-awesome": "4.7.0",
"UNUSED_history": "5.3.0",
"UNUSED_html-react-parser": "^1.3.0",
"DEV_jest": "^29.7.0",
"DEV_jest-environment-jsdom": "29.7.0",
"DEV_jest-when": "^3.6.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-intl": "6.6.8",
"UNUSED_react-pdf": "^7.0.0",
"react-redux": "^7.2.4",
"react-router-dom": "6.26.0",
"react-share": "^4.4.0",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"UNUSED_redux-beacon": "^2.1.0",
"redux-logger": "3.0.6",
"redux-thunk": "2.4.2",
"regenerator-runtime": "^0.14.0",
"reselect": "^4.0.0",
"UNUSED_universal-cookie": "^4.0.4",
"UNUSED_?util": "^0.12.4",
"UNUSED_whatwg-fetch": "^3.6.2"
}
}

17826
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,68 +28,55 @@
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-component-header": "^5.3.1",
"@edx/frontend-enterprise-hotjar": "3.0.0",
"@edx/frontend-platform": "^7.1.4",
"@edx/frontend-platform": "8.1.1",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "2.0.0",
"@edx/react-unit-test-utils": "3.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",
"@openedx/frontend-plugin-framework": "^1.1.2",
"@openedx/frontend-plugin-framework": "^1.2.0",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.2.2",
"@optimizely/react-sdk": "^2.9.2",
"@redux-beacon/segment": "^1.1.0",
"@redux-devtools/extension": "3.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.28.0",
"classnames": "^2.3.1",
"core-js": "3.16.2",
"dompurify": "^2.3.1",
"email-prop-type": "^3.0.1",
"file-saver": "^2.0.5",
"filesize": "^8.0.6",
"core-js": "3.38.0",
"font-awesome": "4.7.0",
"history": "5.0.1",
"html-react-parser": "^1.3.0",
"jest": "^29.7.0",
"jest-when": "^3.6.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "^15.7.2",
"query-string": "7.0.1",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-intl": "6.4.7",
"react-pdf": "^7.0.0",
"react-intl": "6.6.8",
"react-redux": "^7.2.4",
"react-router-dom": "6.15.0",
"react-router-dom": "6.26.0",
"react-share": "^4.4.0",
"react-zendesk": "^0.1.13",
"redux": "4.1.1",
"redux-beacon": "^2.1.0",
"redux-devtools-extension": "2.13.9",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-thunk": "2.3.0",
"regenerator-runtime": "^0.13.9",
"redux-thunk": "2.4.2",
"regenerator-runtime": "^0.14.0",
"reselect": "^4.0.0",
"universal-cookie": "^4.0.4",
"util": "^0.12.4",
"whatwg-fetch": "^3.6.2"
"util": "^0.12.4"
},
"devDependencies": {
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "13.1.4",
"@openedx/frontend-build": "14.0.15",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"axios-mock-adapter": "^1.20.0",
"copy-webpack-plugin": "^11.0.0",
"fetch-mock": "^9.11.0",
"copy-webpack-plugin": "^12.0.0",
"husky": "^7.0.0",
"identity-obj-proxy": "^3.0.0",
"jest-expect-message": "^1.0.2",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-expect-message": "^1.1.3",
"jest-when": "^3.6.0",
"react-dev-utils": "^11.0.4",
"react-test-renderer": "^17.0.2",
"redux-mock-store": "^1.5.4",

View File

@@ -18,7 +18,6 @@ import {
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import ZendeskFab from 'components/ZendeskFab';
import { ExperimentProvider } from 'ExperimentContext';
import track from 'tracking';
@@ -42,19 +41,6 @@ export const App = () => {
const { supportEmail } = reduxHooks.usePlatformSettingsData();
const loadData = reduxHooks.useLoadData();
const optimizelyScript = () => {
if (getConfig().OPTIMIZELY_URL) {
return <script src={getConfig().OPTIMIZELY_URL} />;
} if (getConfig().OPTIMIZELY_PROJECT_ID) {
return (
<script
src={`${getConfig().MARKETING_SITE_BASE_URL}/optimizelyjs/${getConfig().OPTIMIZELY_PROJECT_ID}.js`}
/>
);
}
return null;
};
React.useEffect(() => {
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
window.loadEmptyData = () => {
@@ -91,7 +77,6 @@ export const App = () => {
<Helmet>
<title>{formatMessage(messages.pageTitle)}</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
{optimizelyScript()}
</Helmet>
<div>
<AppWrapper>
@@ -103,9 +88,7 @@ export const App = () => {
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
<Dashboard />
)}
</main>
</AppWrapper>

View File

@@ -9,6 +9,7 @@ $fa-font-path: "~font-awesome/fonts";
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/_footer";
.text-ellipsis {

View File

@@ -10,7 +10,6 @@ import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import { ExperimentProvider } from 'ExperimentContext';
import { App } from './App';
import messages from './messages';
@@ -19,9 +18,6 @@ jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
jest.mock('containers/Dashboard', () => 'Dashboard');
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('ExperimentContext', () => ({
ExperimentProvider: 'ExperimentProvider',
}));
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
@@ -79,11 +75,10 @@ describe('App router component', () => {
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
});
});
@@ -97,11 +92,10 @@ describe('App router component', () => {
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
});
});
@@ -115,11 +109,10 @@ describe('App router component', () => {
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
});
});

View File

@@ -1,64 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { StrictDict } from 'utils';
import api from 'widgets/ProductRecommendations/api';
import * as module from './ExperimentContext';
export const state = StrictDict({
experiment: (val) => React.useState(val), // eslint-disable-line
countryCode: (val) => React.useState(val), // eslint-disable-line
});
export const useCountryCode = (setCountryCode) => {
React.useEffect(() => {
api
.fetchRecommendationsContext()
.then((response) => {
setCountryCode(response.data.countryCode);
})
.catch(() => {
setCountryCode('');
});
/* eslint-disable */
}, []);
};
export const ExperimentContext = React.createContext();
export const ExperimentProvider = ({ children }) => {
const [countryCode, setCountryCode] = module.state.countryCode(null);
const [experiment, setExperiment] = module.state.experiment({
isExperimentActive: false,
inRecommendationsVariant: true,
});
module.useCountryCode(setCountryCode);
const { width } = useWindowSize();
const isMobile = width < breakpoints.small.minWidth;
const contextValue = React.useMemo(
() => ({
experiment,
countryCode,
setExperiment,
setCountryCode,
isMobile,
}),
[experiment, countryCode, setExperiment, setCountryCode, isMobile]
);
return (
<ExperimentContext.Provider value={contextValue}>
{children}
</ExperimentContext.Provider>
);
};
export const useExperimentContext = () => React.useContext(ExperimentContext);
ExperimentProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default { useCountryCode, useExperimentContext };

View File

@@ -1,122 +0,0 @@
import React from 'react';
import { waitFor, render } from '@testing-library/react';
import { useWindowSize } from '@openedx/paragon';
import api from 'widgets/ProductRecommendations/api';
import { MockUseState } from 'testUtils';
import * as experiment from 'ExperimentContext';
const state = new MockUseState(experiment);
jest.unmock('react');
jest.spyOn(React, 'useEffect').mockImplementation((cb, prereqs) => ({ useEffect: { cb, prereqs } }));
jest.mock('widgets/ProductRecommendations/api', () => ({
fetchRecommendationsContext: jest.fn(),
}));
describe('experiments context', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('useCountryCode', () => {
describe('behaviour', () => {
describe('useEffect call', () => {
let calls;
let cb;
const setCountryCode = jest.fn();
const successfulFetch = { data: { countryCode: 'ZA' } };
beforeEach(() => {
experiment.useCountryCode(setCountryCode);
({ calls } = React.useEffect.mock);
[[cb]] = calls;
});
it('calls useEffect once', () => {
expect(calls.length).toEqual(1);
});
describe('successful fetch', () => {
it('sets the country code', async () => {
let resolveFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
new Promise((resolve) => {
resolveFn = resolve;
}),
);
cb();
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
expect(setCountryCode).not.toHaveBeenCalled();
resolveFn(successfulFetch);
await waitFor(() => {
expect(setCountryCode).toHaveBeenCalledWith(successfulFetch.data.countryCode);
});
});
});
describe('unsuccessful fetch', () => {
it('sets the country code to an empty string', async () => {
let rejectFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
new Promise((resolve, reject) => {
rejectFn = reject;
}),
);
cb();
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
expect(setCountryCode).not.toHaveBeenCalled();
rejectFn();
await waitFor(() => {
expect(setCountryCode).toHaveBeenCalledWith('');
});
});
});
});
});
});
describe('ExperimentProvider', () => {
const { ExperimentProvider } = experiment;
const TestComponent = () => {
const {
experiment: exp,
setExperiment,
countryCode,
setCountryCode,
isMobile,
} = experiment.useExperimentContext();
expect(exp.isExperimentActive).toBeFalsy();
expect(exp.inRecommendationsVariant).toBeTruthy();
expect(countryCode).toBeNull();
expect(isMobile).toBe(false);
expect(setExperiment).toBeDefined();
expect(setCountryCode).toBeDefined();
return (
<div />
);
};
it('allows access to child components with the context stateful values', () => {
const countryCodeSpy = jest.spyOn(experiment, 'useCountryCode').mockImplementationOnce(() => {});
useWindowSize.mockImplementationOnce(() => ({ width: 577, height: 943 }));
state.mock();
render(
<ExperimentProvider>
<TestComponent />
</ExperimentProvider>,
);
expect(countryCodeSpy).toHaveBeenCalledWith(state.setState.countryCode);
state.expectInitializedWith(state.keys.countryCode, null);
state.expectInitializedWith(state.keys.experiment, { isExperimentActive: false, inRecommendationsVariant: true });
});
});
});

View File

@@ -51,9 +51,7 @@ exports[`App router component component no network failure snapshot 1`] = `
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
@@ -75,17 +73,12 @@ exports[`App router component component no network failure with optimizely proje
rel="shortcut icon"
type="image/x-icon"
/>
<script
src="undefined/optimizelyjs/fakeId.js"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
@@ -107,17 +100,12 @@ exports[`App router component component no network failure with optimizely url s
rel="shortcut icon"
type="image/x-icon"
/>
<script
src="fake.url"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />

View File

@@ -9,7 +9,7 @@ exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPag
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<AppProvider
store={
Object {
{
"redux": "store",
}
}

View File

@@ -5,23 +5,23 @@ exports[`ZendeskFab snapshot 1`] = `
cookies={true}
defer={true}
webWidget={
Object {
"answerBot": Object {
"avatar": Object {
"name": Object {
{
"answerBot": {
"avatar": {
"name": {
"*": "edX Support",
},
"url": "https://edx-cdn.org/v3/prod/favicon.ico",
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": Object {
"title": {
"*": "edX Support",
},
},
"chat": Object {
"departments": Object {
"enabled": Array [
"chat": {
"departments": {
"enabled": [
"account settings",
"billing and payments",
"certificates",
@@ -33,17 +33,17 @@ exports[`ZendeskFab snapshot 1`] = `
},
"suppress": false,
},
"contactForm": Object {
"contactForm": {
"attachments": true,
"selectTicketForm": Object {
"selectTicketForm": {
"*": "Please choose your request type:",
},
"ticketForms": Array [
Object {
"fields": Array [
Object {
"ticketForms": [
{
"fields": [
{
"id": "description",
"prefill": Object {
"prefill": {
"*": "",
},
},
@@ -53,10 +53,10 @@ exports[`ZendeskFab snapshot 1`] = `
},
],
},
"contactOptions": Object {
"contactOptions": {
"enabled": false,
},
"helpCenter": Object {
"helpCenter": {
"originalArticleButton": true,
},
}

View File

@@ -6,8 +6,8 @@ exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
disabled={true}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
@@ -25,8 +25,8 @@ exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
disabled={false}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",

View File

@@ -6,8 +6,8 @@ exports[`ResumeButton snapshot disabled snapshot 1`] = `
disabled={true}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
@@ -25,8 +25,8 @@ exports[`ResumeButton snapshot enabled snapshot 1`] = `
disabled={false}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",

View File

@@ -7,8 +7,8 @@ exports[`UpgradeButton snapshot can upgrade 1`] = `
href="upgradeUrl"
iconBefore={[MockFunction icons.Locked]}
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.trackUpgradeClicked],
"upgradeUrl": "upgradeUrl",

View File

@@ -6,8 +6,8 @@ exports[`ViewCourseButton learner can view course 1`] = `
disabled={false}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",
@@ -25,8 +25,8 @@ exports[`ViewCourseButton learner cannot view course 1`] = `
disabled={true}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",

View File

@@ -10,14 +10,14 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
>
<format-message-function
message={
Object {
{
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
"description": "",
"id": "learner-dash.courseCard.banners.credit.error",
}
}
values={
Object {
{
"supportEmailLink": <MailtoLink
to="test-support-email"
>

View File

@@ -27,8 +27,8 @@ exports[`CreditContent component render with action snapshot 1`] = `
</ActionRow>
<CreditRequestForm
requestData={
Object {
"parameters": Object {
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",
@@ -48,8 +48,8 @@ exports[`CreditContent component render without action snapshot 1`] = `
</div>
<CreditRequestForm
requestData={
Object {
"parameters": Object {
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",

View File

@@ -13,12 +13,12 @@ exports[`RelatedProgramsBanner render with programs 1`] = `
</span>
<ProgramsList
programs={
Array [
Object {
[
{
"title": "Program 1",
"url": "http://example.com/program1",
},
Object {
{
"title": "Program 2",
"url": "http://example.com/program2",
},

View File

@@ -43,14 +43,14 @@ exports[`CertificateBanner snapshot is restricted and verified with billing emai
<format-message-function
message={
Object {
{
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
Object {
{
"billingEmail": <MailtoLink
to="billing@email"
>
@@ -68,14 +68,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support and
>
<format-message-function
message={
Object {
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
Object {
{
"supportEmail": <MailtoLink
to="suport@email"
>
@@ -87,14 +87,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support and
<format-message-function
message={
Object {
{
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
Object {
{
"billingEmail": <MailtoLink
to="billing@email"
>
@@ -112,14 +112,14 @@ exports[`CertificateBanner snapshot is restricted and verified with support emai
>
<format-message-function
message={
Object {
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
Object {
{
"supportEmail": <MailtoLink
to="suport@email"
>
@@ -147,14 +147,14 @@ exports[`CertificateBanner snapshot is restricted with support email 1`] = `
>
<format-message-function
message={
Object {
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
Object {
{
"supportEmail": <MailtoLink
to="suport@email"
>

View File

@@ -4,14 +4,14 @@ exports[`EntitlementBanner snapshot: expiration warning 1`] = `
<Banner>
<format-message-function
message={
Object {
{
"defaultMessage": "You must {selectSessionButton} by {changeDeadline} to access the course.",
"description": "Entitlement course message when the entitlement is expiring soon.",
"id": "learner-dash.courseCard.banners.entitlementExpiringSoon",
}
}
values={
Object {
{
"changeDeadline": "11/11/2022",
"selectSessionButton": <Button
className="m-0 p-0"
@@ -33,14 +33,14 @@ exports[`EntitlementBanner snapshot: no sessions available 1`] = `
>
<format-message-function
message={
Object {
{
"defaultMessage": "There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.",
"description": "Entitlement course message when no sessions are available",
"id": "learner-dash.courseCard.banners.entitlementUnavailable",
}
}
values={
Object {
{
"emailLink": <MailtoLink
to="test-support-email"
>

View File

@@ -17,7 +17,7 @@ exports[`CourseCardMenu render show dropdown hide unenroll item and disable emai
<SocialShareMenu
cardId="test-card-id"
emailSettings={
Object {
{
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
@@ -58,7 +58,7 @@ exports[`CourseCardMenu render show dropdown show unenroll and enable email snap
<SocialShareMenu
cardId="test-card-id"
emailSettings={
Object {
{
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],

View File

@@ -5,8 +5,8 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
className="pgn__card-wrapper-image-cap overflow-visible orientation"
href="home-url"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseImageClicked],
"upgradeUrl": "home-url",

View File

@@ -7,8 +7,8 @@ exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
data-testid="CourseCardTitle"
href="home-url"
onClick={
Object {
"trackCourseEvent": Object {
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseTitleClicked],
"upgradeUrl": "home-url",

View File

@@ -28,7 +28,7 @@ exports[`CourseFilterControls is not mobile snapshot 1`] = `
>
<FilterForm
filters={
Array [
[
"test-filter",
]
}
@@ -84,7 +84,7 @@ exports[`CourseFilterControls mobile snapshot 1`] = `
>
<FilterForm
filters={
Array [
[
"test-filter",
]
}
@@ -144,7 +144,7 @@ exports[`CourseFilterControls no courses snapshot 1`] = `
>
<FilterForm
filters={
Array [
[
"test-filter",
]
}

View File

@@ -11,7 +11,7 @@ exports[`FilterForm snapshot renders 1`] = `
name="course-status-filters"
onChange={[MockFunction handleFilterChange]}
value={
Array [
[
"test-filter",
]
}

View File

@@ -48,11 +48,11 @@ exports[`CoursesPanel with courses snapshot 1`] = `
id="course_list"
>
<CourseList
filterOptions={Object {}}
filterOptions={{}}
numPages={1}
setPageNumber={[MockFunction setPageNumber]}
showFilters={false}
visibleList={Array []}
visibleList={[]}
/>
</PluginSlot>
</div>

View File

@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
import { Container, Col, Row } from '@openedx/paragon';
import WidgetFooter from 'containers/WidgetContainers/WidgetFooter';
import WidgetSidebar from '../WidgetContainers/WidgetSidebar';
import hooks from './hooks';
export const columnConfig = {
@@ -23,11 +24,10 @@ export const columnConfig = {
},
};
export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
export const DashboardLayout = ({ children }) => {
const {
isCollapsed,
sidebarShowing,
setSidebarShowing,
} = hooks.useDashboardLayoutData();
const courseListColumnProps = sidebarShowing
@@ -42,12 +42,7 @@ export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
</Col>
<Col {...columnConfig.sidebar} className="sidebar-column">
{!isCollapsed && (<h2 className="course-list-title">&nbsp;</h2>)}
<Sidebar setSidebarShowing={setSidebarShowing} />
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
<WidgetSidebar />
</Col>
</Row>
</Container>
@@ -55,7 +50,6 @@ export const DashboardLayout = ({ children, sidebar: Sidebar }) => {
};
DashboardLayout.propTypes = {
children: PropTypes.node.isRequired,
sidebar: PropTypes.func.isRequired,
};
export default DashboardLayout;

View File

@@ -16,17 +16,13 @@ const hookProps = {
};
hooks.useDashboardLayoutData.mockReturnValue(hookProps);
const props = {
sidebar: jest.fn(() => 'test-sidebar-content'),
};
const children = 'test-children';
let el;
describe('DashboardLayout', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<DashboardLayout {...props}>{children}</DashboardLayout>);
el = shallow(<DashboardLayout>{children}</DashboardLayout>);
});
const testColumns = () => {
@@ -40,17 +36,13 @@ describe('DashboardLayout', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
expect(columns[0].children).not.toHaveLength(0);
});
it('displays sidebar prop in second column', () => {
it('displays WidgetSidebar in second column', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
expect(columns[1].findByType(props.sidebar)).toHaveLength(1);
});
it('displays a footer in the second row', () => {
const columns = el.instance.findByType(Row)[1].findByType(Col);
expect(columns[0].children[0].type).toEqual('WidgetFooter');
expect(columns[1].findByType('WidgetSidebar')).toHaveLength(1);
});
};
const testSidebarLayout = () => {
it('displays widthSidebar width for course list column', () => {
it('displays withSidebar width for course list column', () => {
const columns = el.instance.findByType(Row)[0].findByType(Col);
Object.keys(columnConfig.courseList.withSidebar).forEach(size => {
expect(columns[0].props[size]).toEqual(columnConfig.courseList.withSidebar[size]);

View File

@@ -9,13 +9,13 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 12,
}
@@ -26,26 +26,19 @@ exports[`DashboardLayout collapsed sidebar not showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 4,
}
}
>
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
<WidgetSidebar />
</Col>
</Row>
</Container>
@@ -60,13 +53,13 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 8,
}
@@ -77,26 +70,19 @@ exports[`DashboardLayout collapsed sidebar showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 4,
}
}
>
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
<WidgetSidebar />
</Col>
</Row>
</Container>
@@ -111,13 +97,13 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 12,
}
@@ -128,13 +114,13 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 4,
}
@@ -145,14 +131,7 @@ exports[`DashboardLayout not collapsed sidebar not showing snapshot 1`] = `
>
 
</h2>
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
<WidgetSidebar />
</Col>
</Row>
</Container>
@@ -167,13 +146,13 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
<Col
className="course-list-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 8,
}
@@ -184,13 +163,13 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
<Col
className="sidebar-column"
lg={
Object {
{
"offset": 0,
"span": 12,
}
}
xl={
Object {
{
"offset": 0,
"span": 4,
}
@@ -201,14 +180,7 @@ exports[`DashboardLayout not collapsed sidebar showing snapshot 1`] = `
>
 
</h2>
<mockConstructor
setSidebarShowing={[MockFunction hooks.setSidebarShowing]}
/>
</Col>
</Row>
<Row>
<Col>
<WidgetFooter />
<WidgetSidebar />
</Col>
</Row>
</Container>

View File

@@ -17,9 +17,7 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
data-testid="dashboard-content"
id="dashboard-content"
>
<DashboardLayout
sidebar="LoadedWidgetSidebar"
>
<DashboardLayout>
<CoursesPanel />
</DashboardLayout>
</div>
@@ -62,9 +60,7 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
data-testid="dashboard-content"
id="dashboard-content"
>
<DashboardLayout
sidebar="NoCoursesWidgetSidebar"
>
<DashboardLayout>
<CoursesPanel />
</DashboardLayout>
</div>

View File

@@ -26,7 +26,8 @@ export const useDashboardMessages = () => {
export const useDashboardLayoutData = () => {
const { width } = useWindowSize();
const [sidebarShowing, setSidebarShowing] = module.state.sidebarShowing(false);
const [sidebarShowing, setSidebarShowing] = module.state.sidebarShowing(true);
return {
isDashboardCollapsed: width < breakpoints.large.maxWidth,
sidebarShowing,

View File

@@ -40,9 +40,9 @@ describe('CourseCard hooks', () => {
describe('useDashboardLayoutData', () => {
beforeEach(() => { state.mock(); });
describe('behavior', () => {
it('initializes sidebarShowing to default false value', () => {
it('initializes sidebarShowing to default true value', () => {
hooks.useDashboardLayoutData();
state.expectInitializedWith(state.keys.sidebarShowing, false);
state.expectInitializedWith(state.keys.sidebarShowing, true);
});
});
describe('output', () => {

View File

@@ -6,9 +6,6 @@ import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
import CoursesPanel from 'containers/CoursesPanel';
import LoadedSidebar from 'containers/WidgetContainers/LoadedSidebar';
import NoCoursesSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
import LoadingView from './LoadingView';
import DashboardLayout from './DashboardLayout';
import hooks from './hooks';
@@ -35,7 +32,7 @@ export const Dashboard = () => {
{initIsPending
? (<LoadingView />)
: (
<DashboardLayout sidebar={hasCourses ? LoadedSidebar : NoCoursesSidebar}>
<DashboardLayout>
<CoursesPanel />
</DashboardLayout>
)}

View File

@@ -6,9 +6,6 @@ import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
import CoursesPanel from 'containers/CoursesPanel';
import LoadedWidgetSidebar from 'containers/WidgetContainers/LoadedSidebar';
import NoCoursesWidgetSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
import DashboardLayout from './DashboardLayout';
import LoadingView from './LoadingView';
import hooks from './hooks';
@@ -25,8 +22,6 @@ jest.mock('hooks', () => ({
jest.mock('containers/EnterpriseDashboardModal', () => 'EnterpriseDashboardModal');
jest.mock('containers/CoursesPanel', () => 'CoursesPanel');
jest.mock('containers/WidgetContainers/LoadedSidebar', () => 'LoadedWidgetSidebar');
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => 'NoCoursesWidgetSidebar');
jest.mock('./LoadingView', () => 'LoadingView');
jest.mock('./DashboardLayout', () => 'DashboardLayout');
@@ -116,7 +111,7 @@ describe('Dashboard', () => {
showSelectSessionModal: true,
},
content: ['LoadedView', (
<DashboardLayout sidebar={LoadedWidgetSidebar}><CoursesPanel /></DashboardLayout>
<DashboardLayout><CoursesPanel /></DashboardLayout>
)],
showEnterpriseModal: false,
showSelectSessionModal: true,
@@ -132,7 +127,7 @@ describe('Dashboard', () => {
showSelectSessionModal: false,
},
content: ['Dashboard layout with no courses sidebar and content', (
<DashboardLayout sidebar={NoCoursesWidgetSidebar}><CoursesPanel /></DashboardLayout>
<DashboardLayout><CoursesPanel /></DashboardLayout>
)],
showEnterpriseModal: true,
showSelectSessionModal: false,

View File

@@ -10,7 +10,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: false 1`] =
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}
@@ -54,7 +54,7 @@ exports[`EmailSettingsModal render snapshot: emails disabled, show: true 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}
@@ -98,7 +98,7 @@ exports[`EmailSettingsModal render snapshot: emails enabled, show: true 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}

View File

@@ -11,7 +11,7 @@ exports[`EnterpriseDashboard snapshot 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
@@ -10,7 +12,7 @@ export const BrandLogo = () => {
const dashboard = reduxHooks.useEnterpriseDashboardData();
return (
<a href={dashboard?.url || getConfig().LMS_BASE_URL} className="mx-auto">
<a href={dashboard?.url || '/'} className="mx-auto">
<img
className="logo py-3"
src={getConfig().LOGO_URL}

View File

@@ -1,7 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils';
import { getConfig } from '@edx/frontend-platform';
import { reduxHooks } from 'hooks';
import { reduxHooks } from 'hooks';
import BrandLogo from './BrandLogo';
jest.mock('hooks', () => ({
@@ -24,6 +23,6 @@ describe('BrandLogo', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
const wrapper = shallow(<BrandLogo />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.findByType('a')[0].props.href).toEqual(getConfig().LMS_BASE_URL);
expect(wrapper.instance.findByType('a')[0].props.href).toEqual('/');
});
});

View File

@@ -1,108 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Button, Badge } from '@openedx/paragon';
import urls from 'data/services/lms/urls';
import { reduxHooks, apiHooks } from 'hooks';
import { findCoursesNavDropdownClicked } from '../hooks';
import messages from '../messages';
export const CollapseMenuBody = ({ isOpen }) => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const { enabled: programsEnabled } = apiHooks.useProgramsConfig();
const dashboard = reduxHooks.useEnterpriseDashboardData();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const exploreCoursesClick = findCoursesNavDropdownClicked(
urls.baseAppUrl(courseSearchUrl),
);
if (!isOpen) {
return null;
}
return (
<div className="d-flex flex-column shadow-sm nav-small-menu">
<Button as="a" href={`${getConfig().LMS_BASE_URL}/dashboard/`} variant="inverse-primary">
{formatMessage(messages.course)}
</Button>
{programsEnabled && (
<Button as="a" href={urls.programsUrl()} variant="inverse-primary">
{formatMessage(messages.program)}
</Button>
)}
<Button
as="a"
href={urls.baseAppUrl(courseSearchUrl)}
variant="inverse-primary"
onClick={exploreCoursesClick}
>
{formatMessage(messages.discoverNew)}
</Button>
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
{formatMessage(messages.help)}
</Button>
{authenticatedUser && (
<>
{!!dashboard && (
<Button as="a" href={dashboard.url} variant="inverse-primary">
{formatMessage(messages.dashboard)}
</Button>
)}
{!dashboard && getConfig().CAREER_LINK_URL && (
<Button href={`${getConfig().CAREER_LINK_URL}`}>
{formatMessage(messages.career)}
<Badge className="px-2 mx-2" variant="warning">
{formatMessage(messages.newAlert)}
</Badge>
</Button>
)}
<Button
as="a"
href={`${getConfig().LMS_BASE_URL}/u/${authenticatedUser.username}`}
variant="inverse-primary"
>
{formatMessage(messages.profile)}
</Button>
<Button
as="a"
href={`${getConfig().LMS_BASE_URL}/account/settings`}
variant="inverse-primary"
>
{formatMessage(messages.account)}
</Button>
{getConfig().ORDER_HISTORY_URL && (
<Button
as="a"
variant="inverse-primary"
href={getConfig().ORDER_HISTORY_URL}
>
{formatMessage(messages.orderHistory)}
</Button>
)}
<Button
as="a"
href={getConfig().LOGOUT_URL}
variant="inverse-primary"
>
{formatMessage(messages.signOut)}
</Button>
</>
)}
</div>
);
};
CollapseMenuBody.propTypes = {
isOpen: PropTypes.bool.isRequired,
};
export default CollapseMenuBody;

View File

@@ -1,60 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import { AppContext } from '@edx/frontend-platform/react';
import { apiHooks } from 'hooks';
import CollapseMenuBody from './CollapseMenuBody';
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
username: 'username',
},
},
}));
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: () => ({
url: 'url',
}),
usePlatformSettingsData: () => ({
courseSearchUrl: '/courseSearchUrl',
}),
},
apiHooks: {
useProgramsConfig: () => ({
enabled: true,
}),
},
}));
jest.mock('../hooks', () => ({
findCoursesNavDropdownClicked: (url) => jest.fn().mockName(`findCoursesNavDropdownClicked("${url}")`),
}));
describe('CollapseMenuBody', () => {
test('render', () => {
const wrapper = shallow(<CollapseMenuBody isOpen />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('render empty if not open', () => {
const wrapper = shallow(<CollapseMenuBody isOpen={false} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
});
test('render unauthenticated', () => {
const { authenticatedUser } = AppContext;
AppContext.authenticatedUser = null;
const wrapper = shallow(<CollapseMenuBody isOpen />);
expect(wrapper.snapshot).toMatchSnapshot();
AppContext.authenticatedUser = authenticatedUser;
});
test('render with disabled programs', () => {
apiHooks.useProgramsConfig = () => ({ enabled: false });
const wrapper = shallow(<CollapseMenuBody isOpen />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

@@ -1,164 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollapseMenuBody render 1`] = `
<div
className="d-flex flex-column shadow-sm nav-small-menu"
>
<Button
as="a"
href="http://localhost:18000/dashboard/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
<Fragment>
<Button
as="a"
href="url"
variant="inverse-primary"
>
Dashboard
</Button>
<Button
as="a"
href="http://localhost:18000/u/username"
variant="inverse-primary"
>
Profile
</Button>
<Button
as="a"
href="http://localhost:18000/account/settings"
variant="inverse-primary"
>
Account
</Button>
<Button
as="a"
href="http://localhost:18000/logout"
variant="inverse-primary"
>
Sign Out
</Button>
</Fragment>
</div>
`;
exports[`CollapseMenuBody render empty if not open 1`] = `null`;
exports[`CollapseMenuBody render unauthenticated 1`] = `
<div
className="d-flex flex-column shadow-sm nav-small-menu"
>
<Button
as="a"
href="http://localhost:18000/dashboard/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
</div>
`;
exports[`CollapseMenuBody render with disabled programs 1`] = `
<div
className="d-flex flex-column shadow-sm nav-small-menu"
>
<Button
as="a"
href="http://localhost:18000/dashboard/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<Button
as="a"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
<Fragment>
<Button
as="a"
href="url"
variant="inverse-primary"
>
Dashboard
</Button>
<Button
as="a"
href="http://localhost:18000/u/username"
variant="inverse-primary"
>
Profile
</Button>
<Button
as="a"
href="http://localhost:18000/account/settings"
variant="inverse-primary"
>
Account
</Button>
<Button
as="a"
href="http://localhost:18000/logout"
variant="inverse-primary"
>
Sign Out
</Button>
</Fragment>
</div>
`;

View File

@@ -1,48 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollapsedHeader render nothing if not collapsed 1`] = `false`;
exports[`CollapsedHeader renders 1`] = `
<Fragment>
<header
className="d-flex shadow-sm align-items-center learner-variant-header"
>
<IconButton
alt="Close"
className="p-4"
iconAs="Icon"
invertColors={true}
isActive={true}
onClick={[MockFunction toggleIsOpen]}
variant="primary"
/>
<mockConstructor />
</header>
<mockConstructor
isOpen={false}
/>
</Fragment>
`;
exports[`CollapsedHeader renders with isOpen true 1`] = `
<Fragment>
<header
className="d-flex shadow-sm align-items-center learner-variant-header"
>
<IconButton
alt="Menu"
className="p-4"
iconAs="Icon"
invertColors={true}
isActive={true}
onClick={[MockFunction toggleIsOpen]}
src={[MockFunction icons.Close]}
variant="primary"
/>
<mockConstructor />
</header>
<mockConstructor
isOpen={true}
/>
</Fragment>
`;

View File

@@ -1,47 +0,0 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { MenuIcon, Close } from '@openedx/paragon/icons';
import { IconButton, Icon } from '@openedx/paragon';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
import CollapseMenuBody from './CollapseMenuBody';
import BrandLogo from '../BrandLogo';
import messages from '../messages';
export const CollapsedHeader = () => {
const { formatMessage } = useIntl();
const isCollapsed = useIsCollapsed();
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderData();
return (
isCollapsed && (
<>
<header className="d-flex shadow-sm align-items-center learner-variant-header">
<IconButton
invertColors
isActive
src={isOpen ? Close : MenuIcon}
iconAs={Icon}
alt={
isOpen
? formatMessage(messages.collapseMenuOpenAltText)
: formatMessage(messages.collapseMenuClosedAltText)
}
onClick={toggleIsOpen}
variant="primary"
className="p-4"
/>
<BrandLogo />
</header>
<CollapseMenuBody isOpen={isOpen} />
</>
)
);
};
CollapsedHeader.propTypes = {};
export default CollapsedHeader;

View File

@@ -1,38 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import CollapsedHeader from '.';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
jest.mock('../BrandLogo', () => jest.fn(() => 'BrandLogo'));
jest.mock('./CollapseMenuBody', () => jest.fn(() => 'CollapseMenuBody'));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(() => true),
useLearnerDashboardHeaderData: jest.fn(() => ({
isOpen: false,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
})),
}));
describe('CollapsedHeader', () => {
it('renders', () => {
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
it('render nothing if not collapsed', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
it('renders with isOpen true', () => {
useLearnerDashboardHeaderData.mockReturnValueOnce({
isOpen: true,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
});
const wrapper = shallow(<CollapsedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

@@ -9,14 +9,14 @@ exports[`ConfirmEmailBanner snapshot Show on unverified 1`] = `
>
<format-message-function
message={
Object {
{
"defaultMessage": "Remember to confirm your email so that you can keep learning! {confirmNowButton}.",
"description": "Text for reminding user to confirm email",
"id": "leanerDashboard.confirmEmailTextReminderBanner",
}
}
values={
Object {
{
"confirmNowButton": <Button
className="confirm-email-now-button"
onClick={[MockFunction openConfirmModalButtonClick]}

View File

@@ -1,78 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { AvatarButton, Dropdown, Badge } from '@openedx/paragon';
import { reduxHooks } from 'hooks';
import messages from '../messages';
export const AuthenticatedUserDropdown = () => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const dashboard = reduxHooks.useEnterpriseDashboardData();
return (
authenticatedUser && (
<Dropdown className="user-dropdown pr4">
<Dropdown.Toggle
as={AvatarButton}
src={authenticatedUser.profileImage}
id="user"
variant="light"
className="p-4"
>
<span data-hj-suppress className="d-md-inline">
{authenticatedUser.username}
</span>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
{ getConfig().ENABLE_EDX_PERSONAL_DASHBOARD && (
<>
<Dropdown.Header>{formatMessage(messages.dashboardSwitch)}</Dropdown.Header>
<Dropdown.Item as="a" href="/edx-dashboard" className="active">
{formatMessage(messages.dashboardPersonal)}
</Dropdown.Item>
{!!dashboard && (
<Dropdown.Item as="a" href={dashboard.url} key={dashboard.label}>
{dashboard.label} {formatMessage(messages.dashboard)}
</Dropdown.Item>
)}
<Dropdown.Divider />
</>
)}
{!dashboard && getConfig().CAREER_LINK_URL && (
<Dropdown.Item href={`${getConfig().CAREER_LINK_URL}`}>
{formatMessage(messages.career)}
<Badge className="px-2 mx-2" variant="warning">
{formatMessage(messages.newAlert)}
</Badge>
</Dropdown.Item>
)}
<Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`}>
{formatMessage(messages.profile)}
</Dropdown.Item>
<Dropdown.Item href={getConfig().ACCOUNT_SETTINGS_URL}>
{formatMessage(messages.account)}
</Dropdown.Item>
{getConfig().ORDER_HISTORY_URL && (
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
{formatMessage(messages.orderHistory)}
</Dropdown.Item>
)}
<Dropdown.Divider />
<Dropdown.Item href={getConfig().LOGOUT_URL}>
{formatMessage(messages.signOut)}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
);
};
AuthenticatedUserDropdown.propTypes = {};
export default AuthenticatedUserDropdown;

View File

@@ -1,81 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import { getConfig } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
import { AuthenticatedUserDropdown } from './AuthenticatedUserDropdown';
import { useIsCollapsed } from '../hooks';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
profileImage: 'profileImage',
username: 'username',
},
},
}));
const COURSE_SEARCH_URL = 'test-course-search-url';
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: jest.fn(),
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: COURSE_SEARCH_URL,
})),
},
}));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavDropdownClicked: (href) => jest.fn().mockName(`findCoursesNavDropdownClicked('${href}')`),
}));
jest.mock('data/services/lms/urls', () => ({
baseAppUrl: (url) => (url),
programsUrl: 'http://localhost:18000/dashboard/programs',
}));
const config = {
ACCOUNT_PROFILE_URL: 'http://account-profile-url.test',
ACCOUNT_SETTINGS_URL: 'http://account-settings-url.test',
LOGOUT_URL: 'http://logout-url.test',
ORDER_HISTORY_URL: 'http://order-history-url.test',
SUPPORT_URL: 'http://localhost:18000/support',
CAREER_LINK_URL: 'http://localhost:18000/career',
LMS_BASE_URL: 'http:/localhost:18000',
ENABLE_EDX_PERSONAL_DASHBOARD: true,
};
getConfig.mockReturnValue(config);
describe('AuthenticatedUserDropdown', () => {
const defaultDashboardData = {
label: 'label',
url: 'url',
};
describe('snapshots', () => {
test('no auth render empty', () => {
const { authenticatedUser } = AppContext;
AppContext.authenticatedUser = null;
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
AppContext.authenticatedUser = authenticatedUser;
});
test('with enterprise dashboard', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(defaultDashboardData);
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('without enterprise dashboard and expanded', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<AuthenticatedUserDropdown />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
});

View File

@@ -1,139 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AuthenticatedUserDropdown snapshots no auth render empty 1`] = `null`;
exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
<Dropdown
className="user-dropdown pr4"
>
<Dropdown.Toggle
className="p-4"
id="user"
src="profileImage"
variant="light"
>
<span
className="d-md-inline"
data-hj-suppress={true}
>
username
</span>
</Dropdown.Toggle>
<Dropdown.Menu
className="dropdown-menu-right"
>
<Fragment>
<Dropdown.Header>
SWITCH DASHBOARD
</Dropdown.Header>
<Dropdown.Item
as="a"
className="active"
href="/edx-dashboard"
>
Personal
</Dropdown.Item>
<Dropdown.Item
as="a"
href="url"
key="label"
>
label
Dashboard
</Dropdown.Item>
<Dropdown.Divider />
</Fragment>
<Dropdown.Item
href="http://account-profile-url.test/u/username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="http://account-settings-url.test"
>
Account
</Dropdown.Item>
<Dropdown.Item
href="http://order-history-url.test"
>
Order History
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://logout-url.test"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
`;
exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and expanded 1`] = `
<Dropdown
className="user-dropdown pr4"
>
<Dropdown.Toggle
className="p-4"
id="user"
src="profileImage"
variant="light"
>
<span
className="d-md-inline"
data-hj-suppress={true}
>
username
</span>
</Dropdown.Toggle>
<Dropdown.Menu
className="dropdown-menu-right"
>
<Fragment>
<Dropdown.Header>
SWITCH DASHBOARD
</Dropdown.Header>
<Dropdown.Item
as="a"
className="active"
href="/edx-dashboard"
>
Personal
</Dropdown.Item>
<Dropdown.Divider />
</Fragment>
<Dropdown.Item
href="http://localhost:18000/career"
>
Career
<Badge
className="px-2 mx-2"
variant="warning"
>
New
</Badge>
</Dropdown.Item>
<Dropdown.Item
href="http://account-profile-url.test/u/username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="http://account-settings-url.test"
>
Account
</Dropdown.Item>
<Dropdown.Item
href="http://order-history-url.test"
>
Order History
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://logout-url.test"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
`;

View File

@@ -1,93 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExpandedHeader render 1`] = `
<header
className="d-flex shadow-sm align-items-center learner-variant-header pl-4"
>
<div
className="flex-grow-1 d-flex align-items-center"
>
<BrandLogo />
<Button
as="a"
className="p-4 course-link"
href="http://localhost:18000/dashboard/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
className="p-4"
href="programsUrl"
variant="inverse-primary"
>
Programs
</Button>
<Button
as="a"
className="p-4"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<span
className="flex-grow-1"
/>
<Button
as="a"
className="p-4"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
</div>
<AuthenticatedUserDropdown />
</header>
`;
exports[`ExpandedHeader render empty if collapsed 1`] = `null`;
exports[`ExpandedHeader render with disabled programs 1`] = `
<header
className="d-flex shadow-sm align-items-center learner-variant-header pl-4"
>
<div
className="flex-grow-1 d-flex align-items-center"
>
<BrandLogo />
<Button
as="a"
className="p-4 course-link"
href="http://localhost:18000/dashboard/"
variant="inverse-primary"
>
Courses
</Button>
<Button
as="a"
className="p-4"
href="http://localhost:18000/courseSearchUrl"
onClick={[MockFunction findCoursesNavClicked("http://localhost:18000/courseSearchUrl")]}
variant="inverse-primary"
>
Discover New
</Button>
<span
className="flex-grow-1"
/>
<Button
as="a"
className="p-4"
href="http://localhost:18000/support"
variant="inverse-primary"
>
Help
</Button>
</div>
<AuthenticatedUserDropdown />
</header>
`;

View File

@@ -1,80 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import urls from 'data/services/lms/urls';
import { reduxHooks, apiHooks } from 'hooks';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
import messages from '../messages';
import BrandLogo from '../BrandLogo';
export const ExpandedHeader = () => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const isCollapsed = useIsCollapsed();
const { enabled: programsEnabled } = apiHooks.useProgramsConfig();
const exploreCoursesClick = findCoursesNavClicked(
urls.baseAppUrl(courseSearchUrl),
);
if (isCollapsed) {
return null;
}
return (
<header className="d-flex shadow-sm align-items-center learner-variant-header pl-4">
<div className="flex-grow-1 d-flex align-items-center">
<BrandLogo />
<Button
as="a"
href={`${getConfig().LMS_BASE_URL}/dashboard/`}
variant="inverse-primary"
className="p-4 course-link"
>
{formatMessage(messages.course)}
</Button>
{programsEnabled && (
<Button
as="a"
href={urls.programsUrl()}
variant="inverse-primary"
className="p-4"
>
{formatMessage(messages.program)}
</Button>
)}
<Button
as="a"
href={urls.baseAppUrl(courseSearchUrl)}
variant="inverse-primary"
className="p-4"
onClick={exploreCoursesClick}
>
{formatMessage(messages.discoverNew)}
</Button>
<span className="flex-grow-1" />
<Button
as="a"
href={getConfig().SUPPORT_URL}
variant="inverse-primary"
className="p-4"
>
{formatMessage(messages.help)}
</Button>
</div>
<AuthenticatedUserDropdown />
</header>
);
};
ExpandedHeader.propTypes = {};
export default ExpandedHeader;

View File

@@ -1,53 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import { apiHooks } from 'hooks';
import ExpandedHeader from '.';
import { useIsCollapsed } from '../hooks';
jest.mock('data/services/lms/urls', () => ({
programsUrl: () => 'programsUrl',
baseAppUrl: url => (`http://localhost:18000${url}`),
}));
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: () => ({
courseSearchUrl: '/courseSearchUrl',
}),
},
apiHooks: {
useProgramsConfig: () => ({
enabled: true,
}),
},
}));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
}));
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
jest.mock('../BrandLogo', () => 'BrandLogo');
describe('ExpandedHeader', () => {
test('render', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('render empty if collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true);
});
test('render with disabled programs', () => {
apiHooks.useProgramsConfig = () => ({ enabled: false });
const wrapper = shallow(<ExpandedHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,75 @@
import { getConfig } from '@edx/frontend-platform';
import urls from 'data/services/lms/urls';
import messages from './messages';
const getLearnerHeaderMenu = (
formatMessage,
courseSearchUrl,
authenticatedUser,
exploreCoursesClick,
) => ({
mainMenu: [
{
type: 'item',
href: '/',
content: formatMessage(messages.course),
isActive: true,
},
{
type: 'item',
href: `${urls.programsUrl()}`,
content: formatMessage(messages.program),
},
{
type: 'item',
href: `${urls.baseAppUrl(courseSearchUrl)}`,
content: formatMessage(messages.discoverNew),
onClick: (e) => {
exploreCoursesClick(e);
},
},
],
secondaryMenu: [
{
type: 'item',
href: `${getConfig().SUPPORT_URL}`,
content: formatMessage(messages.help),
},
],
userMenu: [
{
heading: '',
items: [
{
type: 'item',
href: `${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser?.username}`,
content: formatMessage(messages.profile),
},
{
type: 'item',
href: `${getConfig().ACCOUNT_SETTINGS_URL}`,
content: formatMessage(messages.account),
},
...(getConfig().ORDER_HISTORY_URL ? [{
type: 'item',
href: getConfig().ORDER_HISTORY_URL,
content: formatMessage(messages.orderHistory),
}] : []),
],
},
{
heading: '',
items: [
{
type: 'item',
href: `${getConfig().LOGOUT_URL}`,
content: formatMessage(messages.signOut),
},
],
},
],
});
export default getLearnerHeaderMenu;

View File

@@ -16,7 +16,7 @@ exports[`BrandLogo dashboard defined 1`] = `
exports[`BrandLogo dashboard undefined 1`] = `
<a
className="mx-auto"
href="http://localhost:18000"
href="/"
>
<img
alt="edX, Inc. Dashboard"

View File

@@ -3,8 +3,72 @@
exports[`LearnerDashboardHeader render 1`] = `
<Fragment>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<Header
mainMenuItems={
[
{
"content": "Courses",
"href": "/",
"isActive": true,
"type": "item",
},
{
"content": "Programs",
"href": "http://localhost:18000/dashboard/programs",
"type": "item",
},
{
"content": "Discover New",
"href": "http://localhost:18000/course-search-url",
"onClick": [Function],
"type": "item",
},
]
}
secondaryMenuItems={
[
{
"content": "Help",
"href": "http://localhost:18000/support",
"type": "item",
},
]
}
userMenuItems={
[
{
"heading": "",
"items": [
{
"content": "Profile",
"href": "http://account-profile-url.test/u/undefined",
"type": "item",
},
{
"content": "Account",
"href": "http://account-settings-url.test",
"type": "item",
},
{
"content": "Order History",
"href": "test-url",
"type": "item",
},
],
},
{
"heading": "",
"items": [
{
"content": "Sign Out",
"href": "http://localhost:18000/logout",
"type": "item",
},
],
},
]
}
/>
<MasqueradeBar />
</Fragment>
`;

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { StrictDict } from 'utils';
import { linkNames } from 'tracking/constants';
import getLearnerHeaderMenu from './LearnerDashboardMenu';
import * as module from './hooks';
export const state = StrictDict({
@@ -24,6 +27,13 @@ export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCou
linkName: linkNames.learnerHomeNavDropdownExplore,
});
export const useLearnerDashboardHeaderMenu = ({
courseSearchUrl, authenticatedUser, exploreCoursesClick,
}) => {
const { formatMessage } = useIntl();
return getLearnerHeaderMenu(formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick);
};
export const useLearnerDashboardHeaderData = () => {
const [isOpen, setIsOpen] = module.state.isOpen(false);
const toggleIsOpen = () => setIsOpen(!isOpen);
@@ -39,4 +49,5 @@ export default {
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderData,
useLearnerDashboardHeaderMenu,
};

View File

@@ -13,6 +13,7 @@ const {
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderData,
useLearnerDashboardHeaderMenu,
} = hooks;
jest.mock('tracking', () => ({
@@ -48,6 +49,17 @@ describe('LearnerDashboardHeader hooks', () => {
});
});
describe('getLearnerDashboardHeaderMenu', () => {
test('calls header menu data hook', () => {
const courseSearchUrl = '/courses';
const authenticatedUser = {
username: 'test',
};
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({ courseSearchUrl, authenticatedUser });
expect(learnerHomeHeaderMenu.mainMenu.length).toBe(3);
});
});
describe('findCoursesNavDropdownClicked', () => {
test('calls tracking with dropdown link name', () => {
findCoursesNavDropdownClicked(url);

View File

@@ -1,21 +1,43 @@
import React from 'react';
import MasqueradeBar from 'containers/MasqueradeBar';
import { AppContext } from '@edx/frontend-platform/react';
import Header from '@edx/frontend-component-header';
import { reduxHooks } from 'hooks';
import urls from 'data/services/lms/urls';
import ConfirmEmailBanner from './ConfirmEmailBanner';
import CollapsedHeader from './CollapsedHeader';
import ExpandedHeader from './ExpandedHeader';
import { useLearnerDashboardHeaderMenu, findCoursesNavClicked } from './hooks';
import './index.scss';
export const LearnerDashboardHeader = () => (
<>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</>
);
export const LearnerDashboardHeader = () => {
const { authenticatedUser } = React.useContext(AppContext);
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const exploreCoursesClick = () => {
findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
};
const learnerHomeHeaderMenu = useLearnerDashboardHeaderMenu({
courseSearchUrl,
authenticatedUser,
exploreCoursesClick,
});
return (
<>
<ConfirmEmailBanner />
<Header
mainMenuItems={learnerHomeHeaderMenu.mainMenu}
secondaryMenuItems={learnerHomeHeaderMenu.secondaryMenu}
userMenuItems={learnerHomeHeaderMenu.userMenu}
/>
<MasqueradeBar />
</>
);
};
LearnerDashboardHeader.propTypes = {};

View File

@@ -1,18 +1,35 @@
import { mergeConfig } from '@edx/frontend-platform';
import { shallow } from '@edx/react-unit-test-utils';
import LearnerDashboardHeader from '.';
import Header from '@edx/frontend-component-header';
import urls from 'data/services/lms/urls';
import LearnerDashboardHeader from '.';
import { findCoursesNavClicked } from './hooks';
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: '/course-search-url',
})),
},
}));
jest.mock('./hooks', () => ({
...jest.requireActual('./hooks'),
findCoursesNavClicked: jest.fn(),
}));
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');
jest.mock('./CollapsedHeader', () => 'CollapsedHeader');
jest.mock('./ConfirmEmailBanner', () => 'ConfirmEmailBanner');
jest.mock('./ExpandedHeader', () => 'ExpandedHeader');
jest.mock('@edx/frontend-component-header', () => 'Header');
describe('LearnerDashboardHeader', () => {
test('render', () => {
mergeConfig({ ORDER_HISTORY_URL: 'test-url' });
const wrapper = shallow(<LearnerDashboardHeader />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.findByType('ConfirmEmailBanner')).toHaveLength(1);
expect(wrapper.instance.findByType('MasqueradeBar')).toHaveLength(1);
expect(wrapper.instance.findByType('CollapsedHeader')).toHaveLength(1);
expect(wrapper.instance.findByType('ExpandedHeader')).toHaveLength(1);
expect(wrapper.instance.findByType(Header)).toHaveLength(1);
wrapper.instance.findByType(Header)[0].props.mainMenuItems[2].onClick();
expect(findCoursesNavClicked).toHaveBeenCalledWith(urls.baseAppUrl('/course-search-url'));
});
});

View File

@@ -33,7 +33,7 @@ exports[`MasqueradeBar snapshot can masquerade 1`] = `
className="mr-3"
disabled={true}
labels={
Object {
{
"default": "Submit",
}
}
@@ -79,7 +79,7 @@ exports[`MasqueradeBar snapshot can masquerade with input 1`] = `
className="mr-3"
disabled={false}
labels={
Object {
{
"default": "Submit",
}
}
@@ -131,7 +131,7 @@ exports[`MasqueradeBar snapshot is masquerading failed with error 1`] = `
className="mr-3"
disabled={true}
labels={
Object {
{
"default": "Submit",
}
}
@@ -177,7 +177,7 @@ exports[`MasqueradeBar snapshot is masquerading pending 1`] = `
className="mr-3"
disabled={true}
labels={
Object {
{
"default": "Submit",
}
}

View File

@@ -37,8 +37,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program1",
},
"programUrl": "program-1-url",
@@ -53,8 +53,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program2",
},
"programUrl": "program-2-url",
@@ -69,8 +69,8 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program3",
},
"programUrl": "program-3-url",
@@ -121,8 +121,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program1",
},
"programUrl": "program-1-url",
@@ -137,8 +137,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program2",
},
"programUrl": "program-2-url",
@@ -153,8 +153,8 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
>
<ProgramCard
data={
Object {
"programData": Object {
{
"programData": {
"dataFor": "program3",
},
"programUrl": "program-3-url",

View File

@@ -7,7 +7,7 @@ exports[`RelatedProgramsModal ProgramCard snapshot 1`] = `
href="props.data.programUrl"
isClickable={true}
style={
Object {
{
"color": "white",
"width": "18rem",
}

View File

@@ -11,7 +11,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.confirm 1`] = `
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}
@@ -35,7 +35,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason g
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}
@@ -59,7 +59,7 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason s
<div
className="bg-white p-3 rounded shadow"
style={
Object {
{
"textAlign": "start",
}
}
@@ -83,14 +83,14 @@ exports[`UnenrollConfirmModal component snapshot: modalStates.reason, should be
<div
className="bg-white p-3 rounded"
style={
Object {
{
"textAlign": "start",
}
}
>
<ReasonPane
reason={
Object {
{
"isSkipped": false,
"reasonProps": "other",
}

View File

@@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetSidebar snapshots default 1`] = `
<div
className="widget-sidebar"
>
<div
className="d-flex flex-column"
>
<PluginSlot
id="widget_sidebar_plugin_slot"
/>
</div>
</div>
`;

View File

@@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import hooks from 'widgets/ProductRecommendations/hooks';
export const WidgetSidebar = ({ setSidebarShowing }) => {
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
if (!inRecommendationsVariant && isExperimentActive) {
setSidebarShowing(true);
return (
<div className="widget-sidebar">
<div className="d-flex flex-column">
<PluginSlot id="widget_sidebar_plugin_slot" />
</div>
</div>
);
}
return null;
};
WidgetSidebar.propTypes = {
setSidebarShowing: PropTypes.func.isRequired,
};
export default WidgetSidebar;

View File

@@ -1,52 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import hooks from 'widgets/ProductRecommendations/hooks';
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
import WidgetSidebar from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('widgets/ProductRecommendations/hooks', () => ({
useShowRecommendationsFooter: jest.fn(),
}));
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: 'PluginSlot',
}));
describe('WidgetSidebar', () => {
beforeEach(() => jest.resetAllMocks());
const props = {
setSidebarShowing: jest.fn(),
};
describe('snapshots', () => {
test('default', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeControl,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
test('is hidden when the has the default values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.default,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
test('is hidden when the has the treatment values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeTreatment,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
});

View File

@@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import hooks from 'widgets/ProductRecommendations/hooks';
export const WidgetSidebar = ({ setSidebarShowing }) => {
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
if (!inRecommendationsVariant && isExperimentActive) {
setSidebarShowing(true);
return (
<div className="widget-sidebar px-2">
<div className="d-flex">
<PluginSlot id="widget_sidebar_plugin_slot" />
</div>
</div>
);
}
return null;
};
WidgetSidebar.propTypes = {
setSidebarShowing: PropTypes.func.isRequired,
};
export default WidgetSidebar;

View File

@@ -1,52 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import hooks from 'widgets/ProductRecommendations/hooks';
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
import WidgetSidebar from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('widgets/ProductRecommendations/hooks', () => ({
useShowRecommendationsFooter: jest.fn(),
}));
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: 'PluginSlot',
}));
describe('WidgetSidebar', () => {
beforeEach(() => jest.resetAllMocks());
const props = {
setSidebarShowing: jest.fn(),
};
describe('snapshots', () => {
test('default', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeControl,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).toHaveBeenCalledWith(true);
expect(wrapper.snapshot).toMatchSnapshot();
});
});
test('is hidden when the has the default values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.default,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
test('is hidden when the has the treatment values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeTreatment,
);
const wrapper = shallow(<WidgetSidebar {...props} />);
expect(props.setSidebarShowing).not.toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
});

View File

@@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetFooter snapshots default 1`] = `
<div
className="widget-footer"
>
<ProductRecommendations />
</div>
`;

View File

@@ -1,21 +0,0 @@
import React from 'react';
import ProductRecommendations from 'widgets/ProductRecommendations';
import hooks from 'widgets/ProductRecommendations/hooks';
export const WidgetFooter = () => {
hooks.useActivateRecommendationsExperiment();
const { inRecommendationsVariant, isExperimentActive } = hooks.useShowRecommendationsFooter();
if (inRecommendationsVariant && isExperimentActive) {
return (
<div className="widget-footer">
<ProductRecommendations />
</div>
);
}
return null;
};
export default WidgetFooter;

View File

@@ -1,45 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import hooks from 'widgets/ProductRecommendations/hooks';
import { mockFooterRecommendationsHook } from 'widgets/ProductRecommendations/testData';
import WidgetFooter from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('widgets/ProductRecommendations/hooks', () => ({
useActivateRecommendationsExperiment: jest.fn(),
useShowRecommendationsFooter: jest.fn(),
}));
describe('WidgetFooter', () => {
describe('snapshots', () => {
test('default', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeTreatment,
);
const wrapper = shallow(<WidgetFooter />);
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
expect(wrapper.snapshot).toMatchSnapshot();
});
});
test('is hidden when the experiment has the default values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.default,
);
const wrapper = shallow(<WidgetFooter />);
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
test('is hidden when the experiment has the control values', () => {
hooks.useShowRecommendationsFooter.mockReturnValueOnce(
mockFooterRecommendationsHook.activeControl,
);
const wrapper = shallow(<WidgetFooter />);
expect(hooks.useActivateRecommendationsExperiment).toHaveBeenCalled();
expect(wrapper.shallowWrapper).toBeNull();
});
});

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetSidebar snapshots default 1`] = `
exports[`WidgetSidebar snapshots 1`] = `
<div
className="widget-sidebar px-2"
>

View File

@@ -0,0 +1,23 @@
import React from 'react';
import classNames from 'classnames';
import { reduxHooks } from 'hooks';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
// eslint-disable-next-line arrow-body-style
export const WidgetSidebar = () => {
const hasCourses = reduxHooks.useHasCourses();
const widgetSidebarClassNames = classNames('widget-sidebar', { 'px-2': !hasCourses });
const innerWrapperClassNames = classNames('d-flex', { 'flex-column': hasCourses });
return (
<div className={widgetSidebarClassNames}>
<div className={innerWrapperClassNames}>
<PluginSlot id="widget_sidebar_plugin_slot" />
</div>
</div>
);
};
export default WidgetSidebar;

View File

@@ -0,0 +1,18 @@
import { shallow } from '@edx/react-unit-test-utils';
import WidgetSidebar from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: 'PluginSlot',
}));
describe('WidgetSidebar', () => {
beforeEach(() => jest.resetAllMocks());
test('snapshots', () => {
const wrapper = shallow(<WidgetSidebar />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

@@ -20,8 +20,6 @@ export const initializeList = ({ user } = {}) => get(
stringifyUrl(urls.getInitApiUrl(), { [apiKeys.user]: user }),
);
export const getProgramsConfig = () => get(urls.programsConfigUrl());
export const updateEntitlementEnrollment = ({ uuid, courseId }) => post(
urls.entitlementEnrollment(uuid),
{ [apiKeys.courseRunId]: courseId },
@@ -77,7 +75,6 @@ export default {
initializeList,
unenrollFromCourse,
updateEmailSettings,
getProgramsConfig,
updateEntitlementEnrollment,
deleteEntitlementEnrollment,
logEvent,

View File

@@ -22,7 +22,6 @@ export const baseAppUrl = (url) => updateUrl(getBaseUrl(), url);
export const learningMfeUrl = (url) => updateUrl(getConfig().LEARNING_BASE_URL, url);
// static view url
const programsConfigUrl = () => baseAppUrl('/config/programs');
const programsUrl = () => baseAppUrl('/dashboard/programs');
export const creditPurchaseUrl = (courseId) => `${getEcommerceUrl()}/credit/checkout/${courseId}/`;
@@ -39,6 +38,5 @@ export default StrictDict({
getInitApiUrl,
learningMfeUrl,
programsUrl,
programsConfigUrl,
updateEmailSettings,
});

View File

@@ -1,6 +1,8 @@
import * as redux from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import {
composeWithDevToolsLogOnlyInProduction,
} from '@redux-devtools/extension';
import { createLogger } from 'redux-logger';
import apiTestUtils from 'data/services/lms/fakeData/testUtils';
@@ -14,7 +16,7 @@ export const createStore = () => {
const store = redux.createStore(
reducer,
composeWithDevTools(redux.applyMiddleware(...middleware)),
composeWithDevToolsLogOnlyInProduction(redux.applyMiddleware(...middleware)),
);
/**

View File

@@ -1,6 +1,8 @@
import { applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import {
composeWithDevToolsLogOnlyInProduction,
} from '@redux-devtools/extension';
import { createLogger } from 'redux-logger';
import rootReducer, { actions, selectors } from 'data/redux';
@@ -22,8 +24,8 @@ jest.mock('redux', () => ({
applyMiddleware: (...middleware) => ({ applied: middleware }),
createStore: (reducer, middleware) => ({ reducer, middleware }),
}));
jest.mock('redux-devtools-extension/logOnlyInProduction', () => ({
composeWithDevTools: (middleware) => ({ withDevTools: middleware }),
jest.mock('@redux-devtools/extension', () => ({
composeWithDevToolsLogOnlyInProduction: (middleware) => ({ withDevTools: middleware }),
}));
describe('store aggregator module', () => {
@@ -37,7 +39,7 @@ describe('store aggregator module', () => {
describe('middleware', () => {
it('exports thunk and logger middleware, composed and applied with dev tools', () => {
expect(createStore().middleware).toEqual(
composeWithDevTools(applyMiddleware(thunkMiddleware, createLogger())),
composeWithDevToolsLogOnlyInProduction(applyMiddleware(thunkMiddleware, createLogger())),
);
});
});

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { AppContext } from '@edx/frontend-platform/react';
import { logError } from '@edx/frontend-platform/logging';
import { RequestKeys } from 'data/constants/requests';
import { post } from 'data/services/lms/utils';
@@ -32,25 +31,6 @@ export const useInitializeApp = () => {
});
};
export const useProgramsConfig = () => {
const [config, setConfig] = React.useState({});
const fetchProgramsConfig = React.useCallback(async () => {
try {
const { data } = await api.getProgramsConfig();
setConfig(data);
} catch (error) {
logError(`Error accessing programs configuration: ${error.message}`);
}
}, []);
React.useEffect(() => {
fetchProgramsConfig();
}, [fetchProgramsConfig]);
return config;
};
export const useNewEntitlementEnrollment = (cardId) => {
const { uuid } = reduxHooks.useCardEntitlementData(cardId);
const onSuccess = module.useInitializeApp();

View File

@@ -9,7 +9,6 @@ import {
within,
prettyDOM,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
initialize,
mergeConfig,
@@ -42,9 +41,7 @@ jest.unmock('react-redux');
jest.unmock('reselect');
jest.unmock('hooks');
jest.mock('containers/WidgetContainers/LoadedSidebar', () => jest.fn(() => 'loaded-widget-sidebar'));
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => jest.fn(() => 'no-courses-widget-sidebar'));
jest.mock('containers/WidgetContainers/WidgetFooter', () => 'product-recommendations-footer');
jest.mock('containers/WidgetContainers/WidgetSidebar', () => jest.fn(() => 'widget-sidebar'));
jest.mock('components/NoticesWrapper', () => 'notices-wrapper');
jest.mock('@edx/frontend-platform', () => ({
@@ -61,10 +58,6 @@ jest.mock('@edx/frontend-platform/auth', () => ({
getLoginRedirectUrl: jest.fn(),
}));
jest.mock('ExperimentContext', () => ({
ExperimentProvider: 'div'
}));
jest.mock('@edx/frontend-enterprise-hotjar', () => ({
initializeHotjar: jest.fn(),
}));

View File

@@ -24,14 +24,14 @@ exports[`LookingForChallengeWidget snapshots default 1`] = `
>
<format-message-function
message={
Object {
{
"defaultMessage": "Find a course {arrow}",
"description": "Button to explore more courses",
"id": "WidgetSidebar.findCoursesButton",
}
}
values={
Object {
{
"arrow": <Icon
className="mx-1"
/>,

View File

@@ -1,110 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProductRecommendations matches snapshot 1`] = `
<LoadedView
crossProductCourses={
Array [
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "executive-education-2u",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
"name": "Harvard University",
},
],
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "bootcamp-2u",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
"name": "Harvard University",
},
],
"title": "Introduction to Computer Science",
},
]
}
openCourses={
Array [
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "verified-audit",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
"name": "Harvard University",
},
],
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "audit",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
"name": "Harvard University",
},
],
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "verified",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
"name": "Harvard University",
},
],
"title": "Introduction to Computer Science",
},
Object {
"courseRunKey": "course-v1:Test+Course+2022T2",
"courseType": "course",
"image": Object {
"src": "https://www.image-2.com/ed79a49b-64c1-48d2-afdc-054bf921e38d-6a76ceb47dea.small.jpg",
},
"marketingUrl": "https://www.edx.org/course/some-course?utm_source=source",
"owners": Array [
Object {
"key": "HarvardX",
"logoImageUrl": "http://www.image.com/ef72daf3-c9a1-4c00-ba37-b3514392bdcf-8839c516815a.png",
"name": "Harvard University",
},
],
"title": "Introduction to Computer Science",
},
]
}
/>
`;

View File

@@ -1,19 +0,0 @@
import { get, stringifyUrl } from 'data/services/lms/utils';
import urls from 'data/services/lms/urls';
export const crossProductAndAmplitudeRecommendationsUrl = (courseId) => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/cross_product/${courseId}/`;
export const amplitudeRecommendationsUrl = () => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/amplitude/v2/`;
export const recommendationsContextUrl = () => `${urls.getApiUrl()}/edx_recommendations/learner_dashboard/recommendations_context/`;
const fetchRecommendationsContext = () => get(stringifyUrl(recommendationsContextUrl()));
const fetchCrossProductRecommendations = (courseId) => (
get(stringifyUrl(crossProductAndAmplitudeRecommendationsUrl(courseId)))
);
const fetchAmplitudeRecommendations = () => get(stringifyUrl(amplitudeRecommendationsUrl()));
export default {
fetchCrossProductRecommendations,
fetchAmplitudeRecommendations,
fetchRecommendationsContext,
};

View File

@@ -1,34 +0,0 @@
import { get, stringifyUrl } from 'data/services/lms/utils';
import api, { crossProductAndAmplitudeRecommendationsUrl, amplitudeRecommendationsUrl, recommendationsContextUrl } from './api';
jest.mock('data/services/lms/utils', () => ({
stringifyUrl: (...args) => ({ stringifyUrl: args }),
get: (...args) => ({ get: args }),
}));
describe('productRecommendationCourses api', () => {
describe('fetchCrossProductRecommendations', () => {
it('calls get with the correct recommendation courses URL', () => {
expect(api.fetchCrossProductRecommendations('CourseRunKey')).toEqual(
get(stringifyUrl(crossProductAndAmplitudeRecommendationsUrl('CourseRunKey'))),
);
});
});
describe('fetchAmplitudeRecommendations', () => {
it('calls get with the correct recommendation courses URL', () => {
expect(api.fetchAmplitudeRecommendations()).toEqual(
get(stringifyUrl(amplitudeRecommendationsUrl())),
);
});
});
describe('fetchRecommendationsContext', () => {
it('calls get with the correct recommendation courses URL', () => {
expect(api.fetchRecommendationsContext()).toEqual(
get(stringifyUrl(recommendationsContextUrl())),
);
});
});
});

View File

@@ -1,46 +0,0 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Container } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../messages';
import { courseShape, courseTypeToProductTypeMap } from '../utils';
import ProductCardContainer from './ProductCardContainer';
const LoadedView = ({ crossProductCourses, openCourses }) => {
const { formatMessage } = useIntl();
const includesCrossProductTypes = crossProductCourses.length === 2;
const finalProductList = useMemo(() => {
if (includesCrossProductTypes) {
const openCourseList = openCourses.slice(0, 2);
return crossProductCourses.concat(openCourseList);
}
return openCourses;
}, [crossProductCourses, openCourses, includesCrossProductTypes]);
const courseTypes = [...new Set(finalProductList.map((item) => courseTypeToProductTypeMap[item.courseType]))];
return (
<Container
size="lg"
className="recommendations-container pt-sm-5 pt-4.5 pb-2 pb-sm-4.5"
>
<h2>
{formatMessage(messages.recommendationsHeading)}
</h2>
<ProductCardContainer finalProductList={finalProductList} courseTypes={courseTypes} />
</Container>
);
};
LoadedView.propTypes = {
crossProductCourses: PropTypes.arrayOf(
PropTypes.shape(courseShape),
).isRequired,
openCourses: PropTypes.arrayOf(
PropTypes.shape(courseShape),
).isRequired,
};
export default LoadedView;

View File

@@ -1,34 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { mockCrossProductCourses, mockOpenCourses } from '../testData';
import LoadedView from './LoadedView';
describe('ProductRecommendations LoadedView', () => {
it('matches snapshot', () => {
expect(
shallow(
<LoadedView
crossProductCourses={mockCrossProductCourses}
openCourses={mockOpenCourses}
/>,
).snapshot,
).toMatchSnapshot();
});
describe('with less than 2 cross product courses', () => {
it('passes in one course type and 4 open courses to the ProductCardContainer props', () => {
const wrapper = shallow(
<LoadedView
crossProductCourses={[mockCrossProductCourses[0]]}
openCourses={mockOpenCourses}
/>,
);
const productCardContainerProps = wrapper.instance.findByType('ProductCardContainer')[0].props;
expect(productCardContainerProps.courseTypes.length).toEqual(1);
expect(productCardContainerProps.courseTypes[0]).toEqual('Course');
expect(productCardContainerProps.finalProductList).toEqual(mockOpenCourses);
});
});
});

View File

@@ -1,8 +0,0 @@
import React from 'react';
import { Skeleton } from '@openedx/paragon';
export const LoadingView = () => (
<Skeleton height={100} />
);
export default LoadingView;

View File

@@ -1,10 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import LoadingView from './LoadingView';
describe('ProductRecommendations LoadingView', () => {
it('matches snapshot', () => {
expect(shallow(<LoadingView />).snapshot).toMatchSnapshot();
});
});

View File

@@ -1,98 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Badge,
Card,
Truncate,
Hyperlink,
} from '@openedx/paragon';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { trackProductCardClicked, trackCourseCardClicked } from '../optimizelyExperiment';
import { productCardClicked, discoveryCardClicked } from '../track';
import { bootCamp, executiveEducation } from '../constants';
const ProductCard = ({
title,
subtitle,
headerImage,
courseRunKey,
schoolLogo,
courseType,
url,
}) => {
const handleClick = (type, link) => {
const crossProductTypes = [executiveEducation, bootCamp];
const userId = getAuthenticatedUser().userId.toString();
if (crossProductTypes.includes(type)) {
trackProductCardClicked(userId);
productCardClicked(courseRunKey, title, type, link);
} else {
trackCourseCardClicked(userId);
discoveryCardClicked(courseRunKey, title, link);
}
};
const getRedirectUrl = (link) => {
const urlObj = new URL(link);
const hasQueryStringParameters = urlObj.search !== '';
if (hasQueryStringParameters) {
return `${link}&linked_from=recommender`;
}
return `${link}?linked_from=recommender`;
};
const redirectUrl = getRedirectUrl(url);
return (
<Card
className="base-card d-flex text-decoration-none"
as={Hyperlink}
destination={redirectUrl}
onClick={() => {
handleClick(courseType, redirectUrl);
}}
isClickable
>
<Card.ImageCap
src={headerImage}
srcAlt={`header image for ${title}`}
logoSrc={schoolLogo}
logoAlt={`logo for ${subtitle}`}
/>
<Card.Header
className="mt-2"
title={(
<Truncate lines={3} ellipsis="…" className="product-card-title font-weight-bold">
{title}
</Truncate>
)}
subtitle={(
<Truncate lines={1} className="product-card-subtitle font-weight-normal">
{subtitle}
</Truncate>
)}
/>
<Card.Section>
<div className="product-badge position-absolute">
<Badge className="bg-light-500 text-dark-500">{courseType}</Badge>
</div>
</Card.Section>
</Card>
);
};
ProductCard.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
headerImage: PropTypes.string.isRequired,
courseRunKey: PropTypes.string.isRequired,
schoolLogo: PropTypes.string.isRequired,
courseType: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
};
export default ProductCard;

View File

@@ -1,83 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { mockCrossProductCourses, mockOpenCourses, mockFallbackOpenCourse } from '../testData';
import { trackProductCardClicked, trackCourseCardClicked } from '../optimizelyExperiment';
import { productCardClicked, discoveryCardClicked } from '../track';
import ProductCard from './ProductCard';
import { courseTypeToProductTypeMap } from '../utils';
jest.mock('../optimizelyExperiment', () => ({
trackProductCardClicked: jest.fn(),
trackCourseCardClicked: jest.fn(),
}));
jest.mock('../track', () => ({
productCardClicked: jest.fn(),
discoveryCardClicked: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({ userId: '1' })),
}));
describe('ProductRecommendations ProductCard', () => {
const getProps = (course) => {
const {
title,
owners: [{ name: subtitle }],
image: { src: headerImage },
owners: [{ logoImageUrl: schoolLogo }],
} = course;
return {
title,
subtitle,
headerImage,
schoolLogo,
courseType: courseTypeToProductTypeMap[course.courseType],
courseRunKey: course.courseRunKey,
url: course.marketingUrl,
};
};
const crossProductProps = getProps(mockCrossProductCourses[0]);
const openCourseProps = getProps(mockOpenCourses[0]);
const fallbackOpenCourseProps = getProps(mockFallbackOpenCourse[0]);
it('matches snapshot', () => {
expect(shallow(<ProductCard {...crossProductProps} />).snapshot).toMatchSnapshot();
});
it('has the query string parameter attached to a fallback recommendations url', () => {
const wrapper = shallow(<ProductCard {...fallbackOpenCourseProps} />);
const cardUrl = wrapper.instance.findByType('Card')[0].props.destination;
expect(cardUrl).toEqual('https://www.edx.org/course/some-course?linked_from=recommender');
});
it('send outs experiment events related to open courses when clicked', () => {
const wrapper = shallow(<ProductCard {...openCourseProps} />);
const { courseRunKey, title, url } = openCourseProps;
wrapper.instance.props.onClick();
expect(trackCourseCardClicked).toHaveBeenCalledWith('1');
expect(discoveryCardClicked).toHaveBeenCalledWith(courseRunKey, title, `${url}&linked_from=recommender`);
});
it('send outs experiment events related to cross product courses when clicked', () => {
const wrapper = shallow(<ProductCard {...crossProductProps} />);
const {
courseRunKey,
title,
courseType,
url,
} = crossProductProps;
wrapper.instance.props.onClick();
expect(trackProductCardClicked).toHaveBeenCalledWith('1');
expect(productCardClicked).toHaveBeenCalledWith(courseRunKey, title, courseType, `${url}&linked_from=recommender`);
});
});

View File

@@ -1,48 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { courseShape, courseTypeToProductTypeMap } from '../utils';
import { course } from '../constants';
import ProductCard from './ProductCard';
import ProductCardHeader from './ProductCardHeader';
const ProductCardContainer = ({ finalProductList, courseTypes }) => (
<div className="product-card-container d-flex">
{finalProductList
&& courseTypes.map((type) => (
<div key={type}>
<ProductCardHeader courseType={type} />
<div
className={classNames('d-flex', {
'course-subcontainer': type === course,
})}
>
{finalProductList
.filter((courseObj) => courseTypeToProductTypeMap[courseObj.courseType] === type)
.map((item) => (
<ProductCard
key={item.title}
url={item.marketingUrl}
title={item.title}
subtitle={item.owners[0].name}
headerImage={item.image.src}
courseRunKey={item.courseRunKey}
schoolLogo={item.owners[0].logoImageUrl}
courseType={type}
/>
))}
</div>
</div>
))}
</div>
);
ProductCardContainer.propTypes = {
finalProductList: PropTypes.arrayOf(
PropTypes.shape(courseShape),
).isRequired,
courseTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
};
export default ProductCardContainer;

View File

@@ -1,44 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { mockCrossProductCourses, mockOpenCourses } from '../testData';
import ProductCardContainer from './ProductCardContainer';
import { executiveEducation, bootCamp, course } from '../constants';
describe('ProductRecommendations ProductCardContainer', () => {
const props = {
finalProductList: [...mockCrossProductCourses, ...mockOpenCourses],
courseTypes: [executiveEducation, bootCamp, course],
};
it('matches snapshot', () => {
expect(shallow(<ProductCardContainer {...props} />).snapshot).toMatchSnapshot();
});
describe('with finalCourseList containing cross product and open courses', () => {
it('renders 3 ProductCardHeaders with the 3 different course types', () => {
const wrapper = shallow(<ProductCardContainer {...props} />);
const productCardHeaders = wrapper.instance.findByType('ProductCardHeader');
expect(productCardHeaders.length).toEqual(3);
productCardHeaders.forEach((header, index) => {
expect(header.props.courseType).toEqual(props.courseTypes[index]);
});
});
});
describe('with finalCourseList containing only open courses', () => {
it('renders 1 ProductHeader with the one course type', () => {
const openCoursesProps = {
finalProductList: [...mockOpenCourses, ...mockOpenCourses],
courseTypes: ['Course'],
};
const wrapper = shallow(<ProductCardContainer {...openCoursesProps} />);
const productCardHeaders = wrapper.instance.findByType('ProductCardHeader');
expect(productCardHeaders.length).toEqual(1);
expect(productCardHeaders[0].props.courseType).toEqual(openCoursesProps.courseTypes[0]);
});
});
});

Some files were not shown because too many files have changed in this diff Show More