Compare commits

..

63 Commits

Author SHA1 Message Date
Deimer Morales
2b1c56e850 fix: include frontend component header translation (#793) [Ulmo backport] (#800) 2026-02-19 13:46:43 -05:00
dependabot[bot]
0db621b134 chore(deps): bump actions/setup-node from 5 to 6 (#737) 2025-10-23 13:58:00 -04:00
renovate[bot]
aaf2e36fe9 chore(deps): update dependency @reduxjs/toolkit to v2.9.1 (#736)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 15:56:50 +00:00
renovate[bot]
b527fbcfba chore(deps): update dependency @openedx/paragon to v23.14.9 (#735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 05:13:03 +00:00
Feanil Patel
fa3f7b27cf fix: Run npm audit fix to update dependencies. (#734) 2025-10-17 13:23:06 -04:00
Feanil Patel
3b0c58f376 fix: Run npm audit fix to update dependencies. 2025-10-15 10:10:56 -04:00
renovate[bot]
f903392ca1 fix(deps): update dependency core-js to v3.46.0 (#733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 08:39:42 +00:00
renovate[bot]
89032f09f4 chore(deps): update dependency @openedx/paragon to v23.14.8 (#732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 04:46:26 +00:00
renovate[bot]
03f146cce9 chore(deps): update dependency @testing-library/jest-dom to v6.9.1 (#728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 06:04:32 +00:00
renovate[bot]
7dd2bda86e chore(deps): update dependency @openedx/paragon to v23.14.4 (#724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 02:33:18 -04:00
Feanil Patel
410f2f730f build: remove unused reactifex packages
Remove reactifex and/or @edx/reactifex packages from devDependencies
as they are no longer needed. Translation extraction functionality has
been verified to work correctly without these dependencies.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 14:07:40 -03:00
PKulkoRaccoonGang
de13749443 test: Remove support for Node 20 2025-09-25 09:59:23 -03:00
dependabot[bot]
0e66b2031d chore(deps): bump actions/setup-node from 4 to 5 (#715)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 16:23:41 -04:00
Peter Kulko
697c9ff2c8 build: Upgrade to Node 24 (#702) 2025-09-24 16:15:55 -04:00
renovate[bot]
fdb5d2f68c fix(deps): update dependency @reduxjs/toolkit to v2.9.0 (#722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:47:27 +00:00
renovate[bot]
7cf79f8931 chore(deps): update dependency @testing-library/jest-dom to v6.8.0 (#721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 04:57:59 +00:00
Peter Kulko
73b7b7f5d0 test: Add Node 24 to CI matrix (#701) 2025-09-16 09:36:20 -04:00
renovate[bot]
516544c7cc fix(deps): update dependency @edx/frontend-platform to v8.5.1 (#719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 21:06:02 +00:00
renovate[bot]
2134df5478 fix(deps): update dependency @edx/frontend-component-footer to v14.9.2 (#718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 05:49:50 +00:00
renovate[bot]
5345d2eef2 fix(deps): update dependency @edx/frontend-component-header to v6.6.1 (#714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 10:42:20 +00:00
renovate[bot]
d94ac8dd62 fix(deps): update dependency @edx/frontend-component-footer to v14.9.1 (#713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 04:33:27 +00:00
Mubbshar Anwar
ff925b06f1 mubbsharanwar/unenrollment process improvement (#704)
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
2025-09-03 14:39:49 -04:00
Samuel Allan
2696486e5b fix: update frontend-build to fix install issues (#712) 2025-09-02 13:23:36 -04:00
renovate[bot]
5854b00d08 fix(deps): update dependency core-js to v3.45.1 (#711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 09:46:49 +00:00
renovate[bot]
13e9b1a85f fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.6 (#710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 04:58:44 +00:00
Maxwell Frank
8116956a4b feat: remove widgets in favor of plugins (#708) 2025-08-20 11:35:58 -04:00
Maxwell Frank
8d23e7585b chore: upgrade frontend-component-header 6.6.0 (#709) 2025-08-19 19:43:37 -04:00
Maxwell Frank
b63a40006e feat: add tsconfig.json (#707) 2025-08-19 17:24:18 -04:00
renovate[bot]
92243063b9 fix(deps): update dependency @openedx/paragon to v23.14.2 (#706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 08:58:22 +00:00
renovate[bot]
a2673399aa chore(deps): update dependency copy-webpack-plugin to v13.0.1 (#705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 05:46:31 +00:00
dependabot[bot]
cedcb4172d chore(deps): bump actions/checkout from 4 to 5 (#700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-14 13:44:21 -04:00
renovate[bot]
9e4faf1569 fix(deps): update dependency @edx/frontend-platform to v8.5.0 (#698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 08:59:39 +00:00
renovate[bot]
3a9acc981b fix(deps): update dependency core-js to v3.45.0 (#699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 05:22:27 +00:00
renovate[bot]
e3c18698d8 fix(deps): update dependency @openedx/paragon to v23.14.1 (#695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 05:45:04 +00:00
Kyle McCormick
eb38beedc3 chore: Delete CODEOWNERS (#691) 2025-07-31 16:08:12 -04:00
renovate[bot]
01faf5a30a fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.3 (#690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 12:15:42 +00:00
renovate[bot]
f4807614e2 chore(deps): update dependency @testing-library/jest-dom to v6.6.4 (#689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 06:51:58 +00:00
renovate[bot]
ab448e52f2 chore(deps): update dependency copy-webpack-plugin to v13 (#688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 12:09:13 -04:00
renovate[bot]
3383016176 fix(deps): update dependency @edx/frontend-component-header to v6.4.2 (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 04:43:49 +00:00
renovate[bot]
054cd57d4b fix(deps): update dependency react-router-dom to v6.30.1 (#686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 10:34:49 +00:00
renovate[bot]
0fc3cc4d53 fix(deps): update dependency core-js to v3.44.0 (#685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 06:17:55 +00:00
renovate[bot]
e5c1244e59 fix(deps): update dependency @openedx/paragon to v23.14.0 (#683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 14:10:26 +00:00
renovate[bot]
3a389e14e1 fix(deps): update dependency @edx/frontend-component-header to v6.4.1 (#682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 08:00:29 +00:00
Diana Villalvazo
6de409d7cc test: Deprecate react-unit-test-utils 15/15 (#680) 2025-07-02 12:23:55 -04:00
Diana Villalvazo
517b8424b3 test: Deprecate react-unit-test-utils 13/15 (#678) 2025-07-02 12:18:50 -04:00
Diana Villalvazo
49e527d810 test: Deprecate react-unit-test-utils 14/15 (#672) 2025-07-01 13:16:20 -04:00
Diana Villalvazo
fdc58a671a test: Transform snapshots into rtl tests (#674) 2025-06-30 15:35:13 -04:00
Muhammad Noyan Aziz
90aa652ca6 feat: added a generic creditPurchase Url logic (#675)
Co-authored-by: Muhammad Noyan  Aziz <noyan.aziz@A006-01474.local>
2025-06-30 10:35:32 -04:00
renovate[bot]
05a08101a2 fix(deps): update dependency @openedx/paragon to v23.13.0 (#677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 10:17:18 +00:00
renovate[bot]
cf94ea99b4 chore(deps): update dependency @openedx/frontend-build to v14.6.1 (#676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 04:54:58 +00:00
Diana Villalvazo
7f1e509ecf test: Deprecate react-unit-test-utils 12/15 (#671) 2025-06-27 12:13:33 -04:00
Diana Villalvazo
31c5445722 test: Deprecate react-unit-test-utils 11/15 (#670) 2025-06-26 15:53:29 -04:00
Diana Villalvazo
2acf7fbd73 test: Deprecate react-unit-test-utils 10/15 (#658) 2025-06-25 08:22:38 -04:00
Diana Villalvazo
cae7b1bba0 test: Deprecate react-unit-test-utils 9/15 (#668) 2025-06-24 16:50:11 +00:00
Diana Villalvazo
6ae8180f99 test: Deprecate react-unit-test-utils 8/15 (#664) 2025-06-24 12:38:13 -04:00
Diana Villalvazo
ab0f139d75 test: Deprecate react-unit-test-utils 7/15 (#662) 2025-06-24 09:44:25 -04:00
Diana Villalvazo
8a0b9dca5d test: Deprecate react-unit-test-utils 6/15 (#660) 2025-06-23 14:22:21 -04:00
Brian Smith
2e59e24876 feat!: add design tokens support (#665)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 15:05:17 -04:00
Diana Villalvazo
75865290bf test: Deprecate react-unit-test-utils 5/15 (#659) 2025-06-17 20:32:59 -04:00
Diana Villalvazo
57fe161b16 test: Deprecate react-unit-test-utils 4/15 (#656) 2025-06-17 20:09:36 -04:00
Diana Villalvazo
5c4dfd5de3 test: Deprecate react-unit-test-utils 3/15 (#655) 2025-06-17 20:00:44 -04:00
Diana Villalvazo
b0daefa2bf test: Deprecate react-unit-test-utils 2/15 (#643) 2025-06-17 19:47:36 -04:00
Diana Villalvazo
42a21180c7 test: Deprecate react-unit-test-utils 1/15 (#640) 2025-06-17 12:52:56 -04:00
320 changed files with 9578 additions and 6581 deletions

10
.dockerignore Executable file
View File

@@ -0,0 +1,10 @@
node_modules
npm-debug.log
README.md
LICENSE
.babelrc
.eslintignore
.eslintrc.json
.gitignore
.npmignore
commitlint.config.js

46
.env Normal file
View File

@@ -0,0 +1,46 @@
NODE_ENV='production'
NODE_PATH=./src
BASE_URL=''
LMS_BASE_URL=''
ECOMMERCE_BASE_URL=''
LOGIN_URL=''
LOGOUT_URL=''
CSRF_TOKEN_API_PATH=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
DATA_API_BASE_URL=''
SEGMENT_KEY=''
FEATURE_FLAGS={}
ACCESS_TOKEN_COOKIE_NAME=''
NEW_RELIC_APP_ID=''
NEW_RELIC_LICENSE_KEY=''
SITE_NAME=''
MARKETING_SITE_BASE_URL=''
SUPPORT_URL=''
CONTACT_URL=''
OPEN_SOURCE_URL=''
TERMS_OF_SERVICE_URL=''
PRIVACY_POLICY_URL=''
FACEBOOK_URL=''
TWITTER_URL=''
YOU_TUBE_URL=''
LINKED_IN_URL=''
REDDIT_URL=''
APPLE_APP_STORE_URL=''
GOOGLE_PLAY_URL=''
ENTERPRISE_MARKETING_URL=''
ENTERPRISE_MARKETING_UTM_SOURCE=''
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
LEARNING_BASE_URL=''
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
# Fallback in local style files
PARAGON_THEME_URLS={}

52
.env.development Normal file
View File

@@ -0,0 +1,52 @@
NODE_ENV='development'
PORT=1996
BASE_URL='localhost:1996'
LMS_BASE_URL='http://localhost:18000'
ECOMMERCE_BASE_URL='http://localhost:18130'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
USER_INFO_COOKIE_NAME='edx-user-info'
SITE_NAME=localhost
DATA_API_BASE_URL='http://localhost:8000'
// LMS_CLIENT_ID should match the lms DOT client application id your LMS containe
LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL=''
CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
FACEBOOK_URL='https://www.facebook.com'
TWITTER_URL='https://twitter.com'
YOU_TUBE_URL='https://www.youtube.com'
LINKED_IN_URL='https://www.linkedin.com'
REDDIT_URL='https://www.reddit.com'
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
GOOGLE_PLAY_URL='https://play.google.com/store'
ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
LEARNING_BASE_URL='http://localhost:2000'
SESSION_COOKIE_DOMAIN='localhost'
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
# Fallback in local style files
PARAGON_THEME_URLS={}

50
.env.test Normal file
View File

@@ -0,0 +1,50 @@
NODE_ENV='test'
PORT=1996
BASE_URL='localhost:1996'
LMS_BASE_URL='http://localhost:18000'
ECOMMERCE_BASE_URL='http://localhost:18130'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
USER_INFO_COOKIE_NAME='edx-user-info'
SITE_NAME=localhost
DATA_API_BASE_URL='http://localhost:8000'
// LMS_CLIENT_ID should match the lms DOT client application id your LMS containe
LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL=''
CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
FACEBOOK_URL='https://www.facebook.com'
TWITTER_URL='https://twitter.com'
YOU_TUBE_URL='https://www.youtube.com'
LINKED_IN_URL='https://www.linkedin.com'
REDDIT_URL='https://www.reddit.com'
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
GOOGLE_PLAY_URL='https://play.google.com/store'
ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
LEARNING_BASE_URL='http://localhost:2000'
HOTJAR_APP_ID='hot-jar-app-id'
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=true
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
PARAGON_THEME_URLS={}

5
.eslintignore Executable file
View File

@@ -0,0 +1,5 @@
coverage/*
dist/
node_modules/
src/postcss.config.js
src/segment.js

22
.eslintrc.js Normal file
View File

@@ -0,0 +1,22 @@
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('eslint', {
rules: {
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-self-import': 'off',
'import/no-import-module-exports': 'off',
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
},
});
config.settings = {
"import/resolver": {
node: {
paths: ["src", "node_modules"],
extensions: [".js", ".jsx"],
},
},
};
module.exports = config;

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.snap linguist-generated=false

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @openedx/2U-aperture

View File

@@ -14,10 +14,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
@@ -30,6 +30,9 @@ jobs:
- name: Lint - name: Lint
run: npm run lint run: npm run lint
- name: Type check
run: npm run types
- name: Test - name: Test
run: npm run test run: npm run test

30
.gitignore vendored
View File

@@ -1,15 +1,29 @@
.DS_Store
.eslintcache
env.config.*
node_modules node_modules
npm-debug.log npm-debug.log
coverage coverage
module.config.js
dist/ dist/
/*.tgz public/samples/
### i18n ### ### pyenv ###
src/i18n/transifex_input.json .python-version
### Editors ### ### Emacs ###
.DS_Store
*~ *~
/temp *.swo
/.vscode *.swp
### Development environments ###
.idea
.vscode
# Local package dependencies
module.config.js
### transifex ###
src/i18n/transifex_input.json
temp
src/i18n/messages

View File

@@ -1,6 +1,12 @@
__mocks__ .eslintignore
.eslintrc.json
.gitignore
docker-compose.yml
Dockerfile
Makefile
npm-debug.log
config
coverage
node_modules node_modules
*.test.js public
*.test.jsx
*.test.ts
*.test.tsx

View File

@@ -24,19 +24,6 @@ test.npm.%: validate-no-uncommitted-package-lock-changes
requirements: ## install ci requirements requirements: ## install ci requirements
npm ci npm ci
clean:
rm -rf dist
build: clean
tsc --project tsconfig.build.json
tsc-alias -p tsconfig.build.json
find src -type f \( -name '*.scss' -o -name '*.png' -o -name '*.svg' \) -exec sh -c '\
for f in "$$@"; do \
d="dist/$${f#src/}"; \
mkdir -p "$$(dirname "$$d")"; \
cp "$$f" "$$d"; \
done' sh {} +
i18n.extract: i18n.extract:
# Pulling display strings from .jsx files into .json files... # Pulling display strings from .jsx files into .json files...
rm -rf $(transifex_temp) rm -rf $(transifex_temp)
@@ -58,11 +45,13 @@ pull_translations:
mkdir src/i18n/messages mkdir src/i18n/messages
cd src/i18n/messages \ cd src/i18n/messages \
&& atlas pull $(ATLAS_OPTIONS) \ && atlas pull $(ATLAS_OPTIONS) \
translations/frontend-base/src/i18n/messages:frontend-base \ translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/paragon/src/i18n/messages:paragon \ translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
$(intl_imports) frontend-base paragon frontend-app-learner-dashboard $(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-learner-dashboard
# This target is used by CI. # This target is used by CI.
validate-no-uncommitted-package-lock-changes: validate-no-uncommitted-package-lock-changes:

View File

@@ -18,7 +18,7 @@ frontend-app-learner-dashboard
The Learner Home app is a microfrontend (MFE) course listing experience for the Open edX Learning Management System The Learner Home app is a microfrontend (MFE) course listing experience for the Open edX Learning Management System
(LMS). This experience was designed to provide a clean and functional interface to allow learners to view all of their (LMS). This experience was designed to provide a clean and functional interface to allow learners to view all of their
open enrollments, as well as take relevant actions on those enrollments. It also serves as host to a number of exposed open enrollments, as well as take relevant actions on those enrollments. It also serves as host to a number of exposed
"widget" containers to provide upsell and discovery widgets as sidebar components. "widget" containers to provide upsell and discovery widgets as sidebar/footer components.
Quickstart Quickstart
---------- ----------
@@ -30,10 +30,21 @@ To start the MFE and enable the feature in LMS:
From there, simply load the configured address/port. You should be prompted to log into your LMS if you are not From there, simply load the configured address/port. You should be prompted to log into your LMS if you are not
already, and then redirected to your home page. already, and then redirected to your home page.
Widgets Plugins
------- -------
This MFE can be customized with widgets. The parts of this MFE that can be customized in that manner are documented This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
`here </src/slots>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
Contributing
------------
Contributions are very welcome. Please read `So you want to contribute to Open edX <https://docs.openedx.org/en/latest/developers/quickstarts/so_you_want_to_contribute.html>`_ for details on how to get started as an Open edX contributor.
This project is currently accepting all types of contributions — bug fixes, security fixes, maintenance work, or new features.
However, if you intend to add a new feature, make sure it has gone through the `Product Review process <https://openedx.atlassian.net/wiki/spaces/COMM/pages/3875962884/How+to+submit+an+open+source+contribution+for+Product+Review>`_.
When proposing a change, create an issue in this repo to get the discussion started.
License License
------- -------

10
app.d.ts vendored
View File

@@ -1,10 +0,0 @@
/// <reference types="@openedx/frontend-base" />
declare module 'site.config' {
export default SiteConfig;
}
declare module '*.svg' {
const content: string;
export default content;
}

View File

@@ -1,3 +0,0 @@
const { createConfig } = require('@openedx/frontend-base/tools');
module.exports = createConfig('babel');

View File

@@ -1,22 +0,0 @@
// @ts-check
const { createLintConfig } = require('@openedx/frontend-base/tools');
module.exports = createLintConfig(
{
files: [
'src/**/*',
'site.config.*',
],
},
{
ignores: [
'coverage/*',
'dist/*',
'documentation/*',
'node_modules/*',
'**/__mocks__/*',
'**/__snapshots__/*',
],
},
);

73
example.env.config.js Normal file
View File

@@ -0,0 +1,73 @@
/*
Learner Dashboard is now able to handle JS-based configuration!
For the time being, the `.env.*` files are still made available when cloning down this repo or pulling from
the master branch. To switch to using `env.config.js`, make a copy of `example.env.config.js` and configure as needed.
For testing with Jest Snapshot, there is a mock in `/src/setupTest.jsx` for `getConfig` that will need to be
uncommented.
Note: having both .env and env.config.js files will follow a predictable order, in which non-empty values in the
JS-based config will overwrite the .env environment variables.
frontend-platform's getConfig loads configuration in the following sequence:
- .env file config
- optional handlers (commonly used to merge MFE-specific config in via additional process.env variables)
- env.config.js file config
- runtime config
*/
module.exports = {
NODE_ENV: 'development',
NODE_PATH: './src',
PORT: 1996,
BASE_URL: 'localhost:1996',
LMS_BASE_URL: 'http://localhost:18000',
ECOMMERCE_BASE_URL: 'http://localhost:18130',
CREDIT_PURCHASE_URL: 'http://localhost:8140',
LOGIN_URL: 'http://localhost:18000/login',
LOGOUT_URL: 'http://localhost:18000/logout',
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
USER_INFO_COOKIE_NAME: 'edx-user-info',
SITE_NAME: 'localhost',
DATA_API_BASE_URL: 'http://localhost:8000',
// LMS_CLIENT_ID should match the lms DOT client application in your LMS container
LMS_CLIENT_ID: 'login-service-client-id',
SEGMENT_KEY: '',
FEATURE_FLAGS: {},
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
SUPPORT_URL: 'http://localhost:18000/support',
CONTACT_URL: 'http://localhost:18000/contact',
OPEN_SOURCE_URL: 'http://localhost:18000/openedx',
TERMS_OF_SERVICE_URL: 'http://localhost:18000/terms-of-service',
PRIVACY_POLICY_URL: 'http://localhost:18000/privacy-policy',
FACEBOOK_URL: 'https://www.facebook.com',
TWITTER_URL: 'https://twitter.com',
YOU_TUBE_URL: 'https://www.youtube.com',
LINKED_IN_URL: 'https://www.linkedin.com',
REDDIT_URL: 'https://www.reddit.com',
APPLE_APP_STORE_URL: 'https://www.apple.com/ios/app-store/',
GOOGLE_PLAY_URL: 'https://play.google.com/store',
ENTERPRISE_MARKETING_URL: 'http://example.com',
ENTERPRISE_MARKETING_UTM_SOURCE: 'example.com',
ENTERPRISE_MARKETING_UTM_CAMPAIGN: 'example.com Referral',
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
LEARNING_BASE_URL: 'http://localhost:2000',
SESSION_COOKIE_DOMAIN: 'localhost',
HOTJAR_APP_ID: '',
HOTJAR_VERSION: 6,
HOTJAR_DEBUG: '',
NEW_RELIC_APP_ID: '',
NEW_RELIC_LICENSE_KEY: '',
ACCOUNT_SETTINGS_URL: 'http://localhost:1997',
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
ENABLE_NOTICES: '',
CAREER_LINK_URL: '',
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
};

View File

@@ -1,22 +1,18 @@
const { createConfig } = require('@openedx/frontend-base/tools'); const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('test', { module.exports = createConfig('jest', {
setupFilesAfterEnv: [ setupFilesAfterEnv: [
'jest-expect-message', 'jest-expect-message',
'<rootDir>/src/setupTest.jsx', '<rootDir>/src/setupTest.jsx',
], ],
modulePaths: ['<rootDir>/src/'],
coveragePathIgnorePatterns: [ coveragePathIgnorePatterns: [
'src/segment.js', 'src/segment.js',
'src/postcss.config.js', 'src/postcss.config.js',
'testUtils', // don't unit test jest mocking tools 'testUtils', // don't unit test jest mocking tools
'src/data/services/lms/fakeData', // don't unit test mock data 'src/data/services/lms/fakeData', // don't unit test mock data
'src/test', // don't unit test integration test utils 'src/test', // don't unit test integration test utils
'src/__mocks__',
], ],
moduleNameMapper: {
// Asset mocks
'\\.svg$': '<rootDir>/src/__mocks__/svg.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/__mocks__/file.js',
},
testTimeout: 120000, testTimeout: 120000,
testEnvironment: 'jsdom',
}); });

10532
package-lock.json generated

File diff suppressed because it is too large Load Diff

86
package.json Normal file → Executable file
View File

@@ -1,69 +1,79 @@
{ {
"name": "@openedx/frontend-app-learner-dashboard", "name": "@edx/frontend-app-learner-dashboard",
"version": "1.0.0-alpha.6", "version": "0.0.1",
"description": "", "description": "",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git" "url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
}, },
"exports": {
".": "./dist/index.js",
"./app.scss": "./dist/app.scss"
},
"files": [
"/dist"
],
"browserslist": [ "browserslist": [
"extends @edx/browserslist-config" "extends @edx/browserslist-config"
], ],
"sideEffects": [
"*.css",
"*.scss"
],
"scripts": { "scripts": {
"build": "make build", "build": "fedx-scripts webpack",
"clean": "make clean", "i18n_extract": "fedx-scripts formatjs extract",
"dev": "PORT=1996 PUBLIC_PATH=/learner-dashboard openedx dev", "lint": "fedx-scripts eslint --ext .jsx,.js src/",
"i18n_extract": "openedx formatjs extract", "lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"lint": "openedx lint .", "semantic-release": "semantic-release",
"lint:fix": "openedx lint --fix .", "start": "fedx-scripts webpack-dev-server --progress",
"prepack": "npm run build", "dev": "PUBLIC_PATH=/learner-dashboard/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "openedx test --coverage --passWithNoTests" "test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
"quality": "npm run lint-fix && npm run test",
"watch-tests": "jest --watch",
"types": "tsc --noEmit"
}, },
"author": "Open edX", "author": "edX",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-learner-dashboard#readme", "homepage": "",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"bugs": {
"url": "https://github.com/openedx/frontend-app-learner-dashboard/issues"
},
"dependencies": { "dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.6.0",
"@edx/frontend-enterprise-hotjar": "7.2.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.7.0", "@edx/openedx-atlas": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4", "@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.5",
"@redux-devtools/extension": "3.3.0", "@redux-devtools/extension": "3.3.0",
"@reduxjs/toolkit": "^2.0.0", "@reduxjs/toolkit": "^2.0.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"core-js": "3.46.0",
"filesize": "^10.0.0", "filesize": "^10.0.0",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"history": "5.3.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-intl": "6.8.9",
"react-redux": "^7.2.4",
"react-router-dom": "6.30.1",
"react-share": "^4.4.0", "react-share": "^4.4.0",
"redux": "4.2.1",
"redux-logger": "3.0.6", "redux-logger": "3.0.6",
"redux-thunk": "2.4.2", "redux-thunk": "2.4.2",
"reselect": "^4.0.0" "regenerator-runtime": "^0.14.0",
"reselect": "^4.0.0",
"universal-cookie": "^4.0.4",
"util": "^0.12.4"
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "^1.5.0", "@edx/browserslist-config": "^1.3.0",
"@edx/typescript-config": "^1.1.0",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"copy-webpack-plugin": "^13.0.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
@@ -71,20 +81,6 @@
"jest-when": "^3.6.0", "jest-when": "^3.6.0",
"react-dev-utils": "^12.0.0", "react-dev-utils": "^12.0.0",
"react-test-renderer": "^18.3.1", "react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4", "redux-mock-store": "^1.5.4"
"tsc-alias": "^1.8.16"
},
"peerDependencies": {
"@openedx/frontend-base": "^1.0.0-alpha.13",
"@openedx/paragon": "^23",
"@tanstack/react-query": "^5",
"@types/react": "^18",
"@types/react-dom": "^18",
"react": "^18",
"react-dom": "^18",
"react-redux": "^8",
"react-router": "^6",
"react-router-dom": "^6",
"redux": "^4"
} }
} }

View File

@@ -1,7 +1,6 @@
<!doctype html> <!doctype html>
<html lang="en-us" dir="ltr"> <html lang="en-us" dir="ltr">
<head> <head>
<title>Learner Dashboard Development Site></title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>

2
public/robots.txt Normal file
View File

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

View File

@@ -1,40 +0,0 @@
import { EnvironmentTypes, SiteConfig, footerApp, headerApp, shellApp } from '@openedx/frontend-base';
import { learnerDashboardApp } from './src';
import './src/app.scss';
const siteConfig: SiteConfig = {
siteId: 'learner-dashboard-dev',
siteName: 'Learner Dashboard Dev',
baseUrl: 'http://apps.local.openedx.io:1996',
lmsBaseUrl: 'http://local.openedx.io:8000',
loginUrl: 'http://local.openedx.io:8000/login',
logoutUrl: 'http://local.openedx.io:8000/logout',
environment: EnvironmentTypes.DEVELOPMENT,
apps: [
shellApp,
headerApp,
footerApp,
learnerDashboardApp
],
externalRoutes: [
{
role: 'org.openedx.frontend.role.profile',
url: 'http://apps.local.openedx.io:1995/profile/'
},
{
role: 'org.openedx.frontend.role.account',
url: 'http://apps.local.openedx.io:1997/account/'
},
{
role: 'org.openedx.frontend.role.logout',
url: 'http://local.openedx.io:8000/logout'
},
],
accessTokenCookieName: 'edx-jwt-cookie-header-payload',
};
export default siteConfig;

View File

@@ -1,27 +0,0 @@
import { EnvironmentTypes, SiteConfig } from '@openedx/frontend-base';
import { appId } from './src/constants';
const siteConfig: SiteConfig = {
siteId: 'learner-dashboard-test-site',
siteName: 'Learner Dashboard Test Site',
baseUrl: 'http://localhost:1996',
lmsBaseUrl: 'http://localhost:8000',
loginUrl: 'http://localhost:8000/login',
logoutUrl: 'http://localhost:8000/logout',
environment: EnvironmentTypes.TEST,
apps: [{
appId,
config: {
ECOMMERCE_BASE_URL: 'http://localhost:18130',
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
LEARNING_BASE_URL: 'http://localhost:2000',
},
}],
accessTokenCookieName: 'edx-jwt-cookie-header-payload',
segmentKey: '',
};
export default siteConfig;

100
src/App.jsx Executable file
View File

@@ -0,0 +1,100 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { useIntl } from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging';
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
import { FooterSlot } from '@edx/frontend-component-footer';
import { Alert } from '@openedx/paragon';
import { RequestKeys } from 'data/constants/requests';
import store from 'data/store';
import {
selectors,
actions,
} from 'data/redux';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import track from 'tracking';
import fakeData from 'data/services/lms/fakeData/courses';
import AppWrapper from 'containers/AppWrapper';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
import './App.scss';
export const App = () => {
const { authenticatedUser } = React.useContext(AppContext);
const { formatMessage } = useIntl();
const isFailed = {
initialize: reduxHooks.useRequestIsFailed(RequestKeys.initialize),
refreshList: reduxHooks.useRequestIsFailed(RequestKeys.refreshList),
};
const hasNetworkFailure = isFailed.initialize || isFailed.refreshList;
const { supportEmail } = reduxHooks.usePlatformSettingsData();
const loadData = reduxHooks.useLoadData();
React.useEffect(() => {
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
window.loadEmptyData = () => {
loadData({ ...fakeData.globalData, courses: [] });
};
window.loadMockData = () => {
loadData({
...fakeData.globalData,
courses: [
...fakeData.courseRunData,
...fakeData.entitlementData,
],
});
};
window.store = store;
window.selectors = selectors;
window.actions = actions;
window.track = track;
}
if (getConfig().HOTJAR_APP_ID) {
try {
initializeHotjar({
hotjarId: getConfig().HOTJAR_APP_ID,
hotjarVersion: getConfig().HOTJAR_VERSION,
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
});
} catch (error) {
logError(error);
}
}
}, [authenticatedUser, loadData]);
return (
<>
<Helmet>
<title>{formatMessage(messages.pageTitle)}</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main id="main">
{hasNetworkFailure
? (
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (
<Dashboard />
)}
</main>
</AppWrapper>
<FooterSlot />
</div>
</>
);
};
export default App;

66
src/App.scss Executable file
View File

@@ -0,0 +1,66 @@
// frontend-app-*/src/index.scss
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // 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 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.alert.alert-info .alert-icon {
color: black;
}
#root {
// Removing a odd 1.5 scaling on checkboxes.:
input[type=checkbox] {
transform: none;
}
display: flex;
flex-direction: column;
min-height: 100vh;
main {
flex-grow: 1;
}
header {
flex: 0 0 auto;
.logo {
display: block;
box-sizing: content-box;
position: relative;
top: 0.1em;
height: 1.75rem;
margin-right: 1rem;
img {
display: block;
height: 100%;
}
}
}
footer {
flex: 0;
}
}
#paragon-portal-root {
.pgn__modal-layer {
.pgn__modal-close-container {
right: 1rem !important;
}
}
.confirm-modal .pgn__modal-body {
overflow: hidden;
}
}

135
src/App.test.jsx Normal file
View File

@@ -0,0 +1,135 @@
import { render, screen, waitFor } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { RequestKeys } from 'data/constants/requests';
import { reduxHooks } from 'hooks';
import { App } from './App';
import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => ({
FooterSlot: jest.fn(() => <div>FooterSlot</div>),
}));
jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => <div>LearnerDashboardHeader</div>));
jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
actions: 'redux.actions',
thunkActions: 'redux.thunkActions',
}));
jest.mock('hooks', () => ({
reduxHooks: {
useRequestIsFailed: jest.fn(),
usePlatformSettingsData: jest.fn(),
useLoadData: jest.fn(),
},
}));
jest.mock('data/store', () => 'data/store');
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({})),
}));
jest.mock('@edx/frontend-platform/react', () => ({
...jest.requireActual('@edx/frontend-platform/react'),
ErrorPage: () => 'ErrorPage',
}));
const loadData = jest.fn();
reduxHooks.useLoadData.mockReturnValue(loadData);
const supportEmail = 'test@support.com';
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
describe('App router component', () => {
describe('component', () => {
const runBasicTests = () => {
it('displays title in helmet component', async () => {
await waitFor(() => expect(document.title).toEqual(messages.pageTitle.defaultMessage));
});
it('displays learner dashboard header', () => {
const learnerDashboardHeader = screen.getByText('LearnerDashboardHeader');
expect(learnerDashboardHeader).toBeInTheDocument();
});
it('wraps the header and main components in an AppWrapper widget container', () => {
const appWrapper = screen.getByText('LearnerDashboardHeader').parentElement;
expect(appWrapper).toHaveClass('AppWrapper');
expect(appWrapper.children[1].id).toEqual('main');
});
it('displays footer slot', () => {
const footerSlot = screen.getByText('FooterSlot');
expect(footerSlot).toBeInTheDocument();
});
};
describe('no network failure', () => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({});
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
});
});
describe('no network failure with optimizely url', () => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
});
});
describe('no network failure with optimizely project id', () => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
});
});
describe('initialize failure', () => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
getConfig.mockReturnValue({});
render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
});
runBasicTests();
it('loads error page', () => {
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
const errorPage = screen.getByText('ErrorPage');
expect(errorPage).toBeInTheDocument();
});
});
describe('refresh failure', () => {
beforeEach(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
getConfig.mockReturnValue({});
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads error page', () => {
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
const errorPage = screen.getByText('ErrorPage');
expect(errorPage).toBeInTheDocument();
});
});
});
});

View File

@@ -1,20 +0,0 @@
import { Provider as ReduxProvider } from 'react-redux';
import { CurrentAppProvider, PageWrap } from '@openedx/frontend-base';
import { appId } from './constants';
import store from './data/store';
import Dashboard from './containers/Dashboard';
import './app.scss';
const Main = () => (
<CurrentAppProvider appId={appId}>
<ReduxProvider store={store}>
<PageWrap>
<Dashboard />
</PageWrap>
</ReduxProvider>
</CurrentAppProvider>
);
export default Main;

View File

@@ -1 +0,0 @@
module.exports = 'FileMock';

View File

@@ -1 +0,0 @@
module.exports = 'SvgURL';

View File

@@ -1,38 +0,0 @@
@use "@openedx/frontend-base/shell/app.scss";
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
#learnerDashboardRoot {
main {
flex-grow: 1;
}
// Removing a odd 1.5 scaling on checkboxes.:
input[type=checkbox] {
transform: none;
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.alert.alert-info .alert-icon {
color: black;
}
#paragon-portal-root {
.pgn__modal-layer {
.pgn__modal-close-container {
right: 1rem !important;
}
}
.confirm-modal .pgn__modal-body {
overflow: hidden;
}
}
}

View File

@@ -1,23 +0,0 @@
import { App } from '@openedx/frontend-base';
import { appId } from './constants';
import routes from './routes';
import providers from './providers';
import messages from './i18n';
import slots from './slots';
const app: App = {
appId,
routes,
providers,
messages,
slots,
config: {
LEARNING_BASE_URL: 'http://apps.local.openedx.io:2000',
ENABLE_PROGRAMS: false,
ECOMMERCE_BASE_URL: '',
ORDER_HISTORY_URL: '',
SUPPORT_URL: '',
}
};
export default app;

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

15
src/assets/top_stripe.svg Normal file
View File

@@ -0,0 +1,15 @@
<svg width="1350" height="7" viewBox="0 0 1350 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3302_26079)">
<rect width="1350" height="6.75" transform="translate(0 -0.375)" fill="#03C7E8"/>
<rect y="-0.375" width="585.562" height="6.75" fill="#D23228"/>
<path d="M549.281 -0.375H933.188L929.491 6.375H549.281V-0.375Z" fill="#002121"/>
<path d="M550.129 13.125L545.062 -10.5L555.188 -10.5L550.129 13.125Z" fill="#D23228"/>
<path d="M931.082 13.125L925.594 -6.28125L936.563 -6.28125L931.082 13.125Z" fill="#002121"/>
<path d="M0 -0.375H106.312L105.289 6.375H0V-0.375Z" fill="#921108"/>
</g>
<defs>
<clipPath id="clip0_3302_26079">
<rect width="1350" height="6.75" fill="white" transform="translate(0 -0.375)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 765 B

View File

@@ -0,0 +1,25 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { logError, logInfo } from '@edx/frontend-platform/logging';
export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
export const getNotices = ({ onLoad, notFoundMessage }) => {
const authenticatedUser = getAuthenticatedUser();
const handleError = async (e) => {
// Error probably means that notices is not installed, which is fine.
const { customAttributes: { httpErrorStatus } } = e;
if (httpErrorStatus === 404) {
logInfo(`${e}. ${notFoundMessage}`);
} else {
logError(e);
}
};
if (authenticatedUser) {
return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
}
return null;
};
export default { getNotices };

View File

@@ -0,0 +1,65 @@
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import * as api from './api';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({
LMS_BASE_URL: 'test-lms-url',
})),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
getAuthenticatedUser: jest.fn(),
}));
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
logInfo: jest.fn(),
}));
const testData = 'test-data';
const successfulGet = () => Promise.resolve(testData);
const error404 = { customAttributes: { httpErrorStatus: 404 }, test: 'error' };
const error404Get = () => Promise.reject(error404);
const error500 = { customAttributes: { httpErrorStatus: 500 }, test: 'error' };
const error500Get = () => Promise.reject(error500);
const get = jest.fn().mockImplementation(successfulGet);
getAuthenticatedHttpClient.mockReturnValue({ get });
const authenticatedUser = { fake: 'user' };
getAuthenticatedUser.mockReturnValue(authenticatedUser);
const onLoad = jest.fn();
describe('getNotices api method', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
describe('not authenticated', () => {
it('does not fetch anything', () => {
getAuthenticatedUser.mockReturnValueOnce(null);
api.getNotices({ onLoad });
expect(get).not.toHaveBeenCalled();
});
});
describe('authenticated', () => {
it('fetches noticesUrl with onLoad behavior', async () => {
await api.getNotices({ onLoad });
expect(get).toHaveBeenCalledWith(api.noticesUrl, {});
expect(onLoad).toHaveBeenCalledWith(testData);
});
it('calls logInfo if fetch fails with 404', async () => {
get.mockImplementation(error404Get);
await api.getNotices({ onLoad });
expect(logInfo).toHaveBeenCalledWith(`${error404}. ${api.error404Message}`);
});
it('calls logError if fetch fails with non-404 error', async () => {
get.mockImplementation(error500Get);
await api.getNotices({ onLoad });
expect(logError).toHaveBeenCalledWith(error500);
});
});
});
});

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import { getNotices } from './api';
import * as module from './hooks';
import messages from './messages';
/**
* This component uses the platform-plugin-notices plugin to function.
* If the user has an unacknowledged notice, they will be rerouted off
* course home and onto a full-screen notice page. If the plugin is not
* installed, or there are no notices, we just passthrough this component.
*/
export const state = StrictDict({
isRedirected: (val) => React.useState(val), // eslint-disable-line
});
export const useNoticesWrapperData = () => {
const [isRedirected, setIsRedirected] = module.state.isRedirected();
const { formatMessage } = useIntl();
React.useEffect(() => {
if (getConfig().ENABLE_NOTICES) {
getNotices({
onLoad: (data) => {
if (data?.data?.results?.length > 0) {
setIsRedirected(true);
window.location.replace(`${data.data.results[0]}?next=${window.location.href}`);
}
},
notFoundMessage: formatMessage(messages.error404Message),
});
}
}, [setIsRedirected, formatMessage]);
return { isRedirected };
};
export default useNoticesWrapperData;

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { MockUseState, formatMessage } from 'testUtils';
import { getConfig } from '@edx/frontend-platform';
import { getNotices } from './api';
import * as hooks from './hooks';
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
jest.mock('./api', () => ({ getNotices: jest.fn() }));
jest.mock('react', () => ({
...jest.requireActual('react'),
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => {
const { formatMessage: fn } = jest.requireActual('testUtils');
return {
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({
formatMessage: fn,
}),
};
});
getConfig.mockReturnValue({ ENABLE_NOTICES: true });
const state = new MockUseState(hooks);
let hook;
describe('NoticesWrapper hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('state hooks', () => {
state.testGetter(state.keys.isRedirected);
});
describe('useNoticesWrapperData', () => {
beforeEach(() => {
state.mock();
});
describe('behavior', () => {
it('initializes state hooks', () => {
hooks.useNoticesWrapperData();
expect(hooks.state.isRedirected).toHaveBeenCalledWith();
});
describe('effects', () => {
it('does not call notices if not enabled', () => {
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
hooks.useNoticesWrapperData();
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb();
expect(getNotices).not.toHaveBeenCalled();
});
describe('getNotices call (if enabled) onLoad behavior', () => {
it('does not redirect if there are no results', () => {
hooks.useNoticesWrapperData();
expect(React.useEffect).toHaveBeenCalled();
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb();
expect(getNotices).toHaveBeenCalled();
const { onLoad } = getNotices.mock.calls[0][0];
onLoad({});
expect(state.setState.isRedirected).not.toHaveBeenCalled();
onLoad({ data: {} });
expect(state.setState.isRedirected).not.toHaveBeenCalled();
onLoad({ data: { results: [] } });
expect(state.setState.isRedirected).not.toHaveBeenCalled();
});
it('redirects and set isRedirected if results are returned', () => {
delete window.location;
window.location = { replace: jest.fn(), href: 'test-old-href' };
hooks.useNoticesWrapperData();
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb();
expect(getNotices).toHaveBeenCalled();
const { onLoad } = getNotices.mock.calls[0][0];
const target = 'url-target';
onLoad({ data: { results: [target] } });
expect(state.setState.isRedirected).toHaveBeenCalledWith(true);
expect(window.location.replace).toHaveBeenCalledWith(
`${target}?next=${window.location.href}`,
);
});
});
});
});
describe('output', () => {
it('forwards isRedirected from state call', () => {
hook = hooks.useNoticesWrapperData();
expect(hook.isRedirected).toEqual(state.stateVals.isRedirected);
});
});
});
});

View File

@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import useNoticesWrapperData from './hooks';
/**
* This component uses the platform-plugin-notices plugin to function.
* If the user has an unacknowledged notice, they will be rerouted off
* course home and onto a full-screen notice page. If the plugin is not
* installed, or there are no notices, we just passthrough this component.
*/
const NoticesWrapper = ({ children }) => {
const { isRedirected } = useNoticesWrapperData();
return (
<div>
{isRedirected === true ? null : children}
</div>
);
};
NoticesWrapper.propTypes = {
children: PropTypes.node.isRequired,
};
export default NoticesWrapper;

View File

@@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react';
import useNoticesWrapperData from './hooks';
import NoticesWrapper from '.';
jest.mock('./hooks', () => jest.fn());
const hookProps = { isRedirected: false };
const children = [<b key={1}>some</b>, <i key={2}>children</i>];
describe('NoticesWrapper component', () => {
beforeEach(() => {
useNoticesWrapperData.mockClear();
});
describe('behavior', () => {
it('initializes hooks', () => {
useNoticesWrapperData.mockReturnValue(hookProps);
render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(useNoticesWrapperData).toHaveBeenCalledWith();
});
});
describe('output', () => {
it('does not show children if redirected', () => {
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(screen.queryByText('some')).not.toBeInTheDocument();
expect(screen.queryByText('children')).not.toBeInTheDocument();
});
it('shows children if not redirected', () => {
useNoticesWrapperData.mockReturnValue(hookProps);
render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(screen.getByText('some')).toBeInTheDocument();
expect(screen.getByText('children')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
error404Message: {
id: 'learner-dash.notices.error404Message',
defaultMessage: 'This probably happened because the notices plugin is not installed on platform.',
description: 'Error message when notices API returns 404',
},
});
export default messages;

28
src/config/index.js Normal file
View File

@@ -0,0 +1,28 @@
const configuration = {
// BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
CREDIT_PURCHASE_URL: process.env.CREDIT_PURCHASE_URL,
// LOGIN_URL: process.env.LOGIN_URL,
// LOGOUT_URL: process.env.LOGOUT_URL,
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
// REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
// DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
// SECURE_COOKIES: process.env.NODE_ENV !== 'development',
SEGMENT_KEY: process.env.SEGMENT_KEY,
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
SUPPORT_URL: process.env.SUPPORT_URL || null,
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
LOGO_URL: process.env.LOGO_URL,
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
ENABLE_PROGRAMS: process.env.ENABLE_PROGRAMS === 'true',
NON_BROWSABLE_COURSES: process.env.NON_BROWSABLE_COURSES === 'true',
};
const features = {};
export { configuration, features };

View File

@@ -1 +0,0 @@
export const appId = 'org.openedx.frontend.app.learnerDashboard';

View File

@@ -0,0 +1,13 @@
import PropTypes from 'prop-types';
export const AppWrapper = ({
children,
}) => children;
AppWrapper.propTypes = {
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]).isRequired,
};
export default AppWrapper;

View File

@@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import track from '../../../../tracking';
import { reduxHooks } from '../../../../hooks';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton'; import ActionButton from './ActionButton';
import messages from './messages'; import messages from './messages';

View File

@@ -1,18 +1,18 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import track from '@src/tracking'; import track from 'tracking';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import BeginCourseButton from './BeginCourseButton'; import BeginCourseButton from './BeginCourseButton';
jest.mock('@src/tracking', () => ({ jest.mock('tracking', () => ({
course: { course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'), enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
}, },
})); }));
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCourseRunData: jest.fn(), useCardCourseRunData: jest.fn(),
useCardExecEdTrackingParam: jest.fn(), useCardExecEdTrackingParam: jest.fn(),

View File

@@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import track from '../../../../tracking';
import { reduxHooks } from '../../../../hooks';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton'; import ActionButton from './ActionButton';
import messages from './messages'; import messages from './messages';

View File

@@ -1,19 +1,19 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import track from '@src/tracking'; import track from 'tracking';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ResumeButton from './ResumeButton'; import ResumeButton from './ResumeButton';
jest.mock('@src/tracking', () => ({ jest.mock('tracking', () => ({
course: { course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'), enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
}, },
})); }));
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCourseRunData: jest.fn(), useCardCourseRunData: jest.fn(),
useCardExecEdTrackingParam: jest.fn(), useCardExecEdTrackingParam: jest.fn(),

View File

@@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '../../../../hooks';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton'; import ActionButton from './ActionButton';
import messages from './messages'; import messages from './messages';

View File

@@ -1,13 +1,13 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import SelectSessionButton from './SelectSessionButton'; import SelectSessionButton from './SelectSessionButton';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useUpdateSelectSessionModalCallback: jest.fn(), useUpdateSelectSessionModalCallback: jest.fn(),
}, },

View File

@@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import track from '../../../../tracking';
import { reduxHooks } from '../../../../hooks';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton'; import ActionButton from './ActionButton';
import messages from './messages'; import messages from './messages';

View File

@@ -1,19 +1,19 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import track from '@src/tracking'; import track from 'tracking';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ViewCourseButton from './ViewCourseButton'; import ViewCourseButton from './ViewCourseButton';
jest.mock('@src/tracking', () => ({ jest.mock('tracking', () => ({
course: { course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'), enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
}, },
})); }));
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })), useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
useTrackCourseEvent: jest.fn(), useTrackCourseEvent: jest.fn(),

View File

@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
import { ActionRow } from '@openedx/paragon'; import { ActionRow } from '@openedx/paragon';
import { reduxHooks } from '../../../../hooks'; import { reduxHooks } from 'hooks';
import CourseCardActionSlot from '../../../../slots/CourseCardActionSlot';
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
import SelectSessionButton from './SelectSessionButton'; import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton'; import BeginCourseButton from './BeginCourseButton';
import ResumeButton from './ResumeButton'; import ResumeButton from './ResumeButton';

View File

@@ -1,9 +1,9 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import CourseCardActions from '.'; import CourseCardActions from '.';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCourseRunData: jest.fn(), useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(), useCardEnrollmentData: jest.fn(),
@@ -12,7 +12,7 @@ jest.mock('@src/hooks', () => ({
}, },
})); }));
jest.mock('@src/slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>)); jest.mock('plugin-slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
jest.mock('./SelectSessionButton', () => jest.fn(() => <div>SelectSessionButton</div>)); jest.mock('./SelectSessionButton', () => jest.fn(() => <div>SelectSessionButton</div>));
jest.mock('./ViewCourseButton', () => jest.fn(() => <div>ViewCourseButton</div>)); jest.mock('./ViewCourseButton', () => jest.fn(() => <div>ViewCourseButton</div>));
jest.mock('./BeginCourseButton', () => jest.fn(() => <div>BeginCourseButton</div>)); jest.mock('./BeginCourseButton', () => jest.fn(() => <div>BeginCourseButton</div>));

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@openedx/frontend-base'; import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({ const messages = defineMessages({
beginCourse: { beginCourse: {

View File

@@ -4,10 +4,10 @@ import PropTypes from 'prop-types';
import { MailtoLink, Hyperlink } from '@openedx/paragon'; import { MailtoLink, Hyperlink } from '@openedx/paragon';
import { CheckCircle } from '@openedx/paragon/icons'; import { CheckCircle } from '@openedx/paragon/icons';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from '../../../../hooks'; import { utilHooks, reduxHooks } from 'hooks';
import Banner from '../../../../components/Banner'; import Banner from 'components/Banner';
import messages from './messages'; import messages from './messages';
@@ -31,7 +31,7 @@ export const CertificateBanner = ({ cardId }) => {
if (certificate.isRestricted) { if (certificate.isRestricted) {
return ( return (
<Banner variant="danger"> <Banner variant="danger">
{supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)} { supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
{isVerified && ' '} {isVerified && ' '}
{isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))} {isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))}
</Banner> </Banner>

View File

@@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import CertificateBanner from './CertificateBanner'; import CertificateBanner from './CertificateBanner';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
utilHooks: { utilHooks: {
useFormatDate: jest.fn(() => date => date), useFormatDate: jest.fn(() => date => date),
}, },

View File

@@ -2,11 +2,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Hyperlink } from '@openedx/paragon'; import { Hyperlink } from '@openedx/paragon';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from '../../../../hooks';
import Banner from '../../../../components/Banner';
import { utilHooks, reduxHooks } from 'hooks';
import Banner from 'components/Banner';
import messages from './messages'; import messages from './messages';
export const CourseBanner = ({ cardId }) => { export const CourseBanner = ({ cardId }) => {

View File

@@ -1,13 +1,13 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import { formatMessage } from '@src/testUtils'; import { formatMessage } from 'testUtils';
import { CourseBanner } from './CourseBanner'; import { CourseBanner } from './CourseBanner';
import messages from './messages'; import messages from './messages';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
utilHooks: { utilHooks: {
useFormatDate: () => date => date, useFormatDate: () => date => date,
}, },

View File

@@ -1,5 +1,6 @@
import { StrictDict } from '../../../../../utils'; import { StrictDict } from 'utils';
import { reduxHooks } from '../../../../../hooks';
import { reduxHooks } from 'hooks';
import ApprovedContent from './views/ApprovedContent'; import ApprovedContent from './views/ApprovedContent';
import EligibleContent from './views/EligibleContent'; import EligibleContent from './views/EligibleContent';
@@ -16,9 +17,7 @@ export const statusComponents = StrictDict({
export const useCreditBannerData = (cardId) => { export const useCreditBannerData = (cardId) => {
const credit = reduxHooks.useCardCreditData(cardId); const credit = reduxHooks.useCardCreditData(cardId);
const { supportEmail } = reduxHooks.usePlatformSettingsData(); const { supportEmail } = reduxHooks.usePlatformSettingsData();
if (!credit.isEligible) { if (!credit.isEligible) { return null; }
return null;
}
const { error, purchased, requestStatus } = credit; const { error, purchased, requestStatus } = credit;
let ContentComponent = EligibleContent; let ContentComponent = EligibleContent;

View File

@@ -1,5 +1,5 @@
import { keyStore } from '@src/utils'; import { keyStore } from 'utils';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import ApprovedContent from './views/ApprovedContent'; import ApprovedContent from './views/ApprovedContent';
import EligibleContent from './views/EligibleContent'; import EligibleContent from './views/EligibleContent';
@@ -9,7 +9,7 @@ import RejectedContent from './views/RejectedContent';
import * as hooks from './hooks'; import * as hooks from './hooks';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCreditData: jest.fn(), useCardCreditData: jest.fn(),
usePlatformSettingsData: jest.fn(), usePlatformSettingsData: jest.fn(),

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import Banner from '../../../../../components/Banner'; import Banner from 'components/Banner';
import { MailtoLink } from '@openedx/paragon'; import { MailtoLink } from '@openedx/paragon';
import hooks from './hooks'; import hooks from './hooks';

View File

@@ -1,5 +1,5 @@
import { screen, render } from '@testing-library/react'; import { screen, render } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import hooks from './hooks'; import hooks from './hooks';
import { CreditBanner } from '.'; import { CreditBanner } from '.';

View File

@@ -1,14 +1,14 @@
import { defineMessages } from '@openedx/frontend-base'; import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({ const messages = defineMessages({
error: { error: {
id: 'learner-dash.courseCard.banners.credit.error', id: 'learner-dash.courseCard.banners.credit.error',
description: 'Error message for credit transaction with support email link', description: '',
defaultMessage: 'An error occurred with this transaction. For help, contact {supportEmailLink}.', defaultMessage: 'An error occurred with this transaction. For help, contact {supportEmailLink}.',
}, },
errorNoEmail: { errorNoEmail: {
id: 'learner-dash.courseCard.banners.credit.errorNoEmail', id: 'learner-dash.courseCard.banners.credit.errorNoEmail',
description: 'Error message for credit transaction without support email', description: '',
defaultMessage: 'An error occurred with this transaction.', defaultMessage: 'An error occurred with this transaction.',
}, },
}); });

View File

@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useIntl } from '@openedx/frontend-base';
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '../../../../../../hooks';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent'; import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink'; import ProviderLink from './components/ProviderLink';
@@ -11,7 +11,7 @@ import messages from './messages';
export const ApprovedContent = ({ cardId }) => { export const ApprovedContent = ({ cardId }) => {
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId); const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
const { isMasquerading } = useContext(MasqueradeUserContext); const { isMasquerading } = reduxHooks.useMasqueradeData();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<CreditContent <CreditContent

View File

@@ -1,14 +1,14 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from '@src/testUtils'; import { formatMessage } from 'testUtils';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import messages from './messages'; import messages from './messages';
import ApprovedContent from './ApprovedContent'; import ApprovedContent from './ApprovedContent';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCreditData: jest.fn(), useCardCreditData: jest.fn(),
useMasqueradeData: jest.fn(),
}, },
})); }));
@@ -18,19 +18,12 @@ const credit = {
providerName: 'test-credit-provider-name', providerName: 'test-credit-provider-name',
}; };
reduxHooks.useCardCreditData.mockReturnValue(credit); reduxHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
const renderWithMasquerading = (isMasquerading = false) => render(
<IntlProvider locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<ApprovedContent cardId={cardId} />
</MasqueradeUserContext.Provider>
</IntlProvider>
);
describe('ApprovedContent component', () => { describe('ApprovedContent component', () => {
describe('hooks', () => { describe('hooks', () => {
it('initializes credit data with cardId', () => { it('initializes credit data with cardId', () => {
renderWithMasquerading(); render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId); expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
}); });
}); });
@@ -38,7 +31,7 @@ describe('ApprovedContent component', () => {
describe('rendered CreditContent component', () => { describe('rendered CreditContent component', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
renderWithMasquerading(); render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
}); });
it('action.message is formatted viewCredit message', () => { it('action.message is formatted viewCredit message', () => {
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage }); const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
@@ -63,7 +56,8 @@ describe('ApprovedContent component', () => {
}); });
describe('when masquerading', () => { describe('when masquerading', () => {
beforeEach(() => { beforeEach(() => {
renderWithMasquerading(true); reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
}); });
it('disables the action button', () => { it('disables the action button', () => {

View File

@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '../../../../../../hooks'; import { reduxHooks } from 'hooks';
import track from '../../../../../../tracking'; import track from 'tracking';
import CreditContent from './components/CreditContent'; import CreditContent from './components/CreditContent';
import messages from './messages'; import messages from './messages';

View File

@@ -1,21 +1,21 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import track from '@src/tracking'; import track from 'tracking';
import messages from './messages'; import messages from './messages';
import EligibleContent from './EligibleContent'; import EligibleContent from './EligibleContent';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCreditData: jest.fn(), useCardCreditData: jest.fn(),
useCardCourseRunData: jest.fn(), useCardCourseRunData: jest.fn(),
}, },
})); }));
jest.mock('@src/tracking', () => ({ jest.mock('tracking', () => ({
credit: { credit: {
purchase: jest.fn(), purchase: jest.fn(),
}, },

View File

@@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useIntl } from '@openedx/frontend-base';
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent'; import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink'; import ProviderLink from './components/ProviderLink';
import hooks from './hooks'; import hooks from './hooks';
@@ -12,7 +13,7 @@ import messages from './messages';
export const MustRequestContent = ({ cardId }) => { export const MustRequestContent = ({ cardId }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId); const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
const { isMasquerading } = useContext(MasqueradeUserContext); const { isMasquerading } = reduxHooks.useMasqueradeData();
return ( return (
<CreditContent <CreditContent
action={{ action={{

View File

@@ -1,9 +1,8 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import messages from './messages'; import messages from './messages';
import hooks from './hooks'; import hooks from './hooks';
import MustRequestContent from './MustRequestContent'; import MustRequestContent from './MustRequestContent';
@@ -12,8 +11,9 @@ jest.mock('./hooks', () => ({
useCreditRequestData: jest.fn(), useCreditRequestData: jest.fn(),
})); }));
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useMasqueradeData: jest.fn(),
useCardCreditData: jest.fn(), useCardCreditData: jest.fn(),
}, },
})); }));
@@ -31,11 +31,9 @@ const providerName = 'test-credit-provider-name';
const providerStatusUrl = 'test-credit-provider-status-url'; const providerStatusUrl = 'test-credit-provider-status-url';
const createCreditRequest = jest.fn().mockName('createCreditRequest'); const createCreditRequest = jest.fn().mockName('createCreditRequest');
const renderMustRequestContent = (isMasquerading = false) => render( const renderMustRequestContent = () => render(
<IntlProvider locale="en" messages={messages}> <IntlProvider locale="en" messages={messages}>
<MasqueradeUserContext.Provider value={{ isMasquerading }}> <MustRequestContent cardId={cardId} />
<MustRequestContent cardId={cardId} />
</MasqueradeUserContext.Provider>
</IntlProvider>, </IntlProvider>,
); );
@@ -46,6 +44,7 @@ describe('MustRequestContent component', () => {
requestData, requestData,
createCreditRequest, createCreditRequest,
}); });
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
reduxHooks.useCardCreditData.mockReturnValue({ reduxHooks.useCardCreditData.mockReturnValue({
providerName, providerName,
providerStatusUrl, providerStatusUrl,
@@ -91,13 +90,14 @@ describe('MustRequestContent component', () => {
describe('when masquerading', () => { describe('when masquerading', () => {
beforeEach(() => { beforeEach(() => {
renderMustRequestContent(true); reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
renderMustRequestContent();
}); });
it('disables the request credit button', () => { it('disables the request credit button', () => {
const button = screen.getByRole('button', { name: /request credit/i }); const button = screen.getByRole('button', { name: /request credit/i });
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveClass('disabled'); expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });
}); });

View File

@@ -1,15 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useIntl } from '@openedx/frontend-base';
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '../../../../../../hooks';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent'; import CreditContent from './components/CreditContent';
import messages from './messages'; import messages from './messages';
export const PendingContent = ({ cardId }) => { export const PendingContent = ({ cardId }) => {
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId); const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
const { isMasquerading } = useContext(MasqueradeUserContext); const { isMasquerading } = reduxHooks.useMasqueradeData();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<CreditContent <CreditContent

View File

@@ -1,14 +1,13 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
import messages from './messages'; import messages from './messages';
import PendingContent from './PendingContent'; import PendingContent from './PendingContent';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { useCardCreditData: jest.fn() }, reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
})); }));
const cardId = 'test-card-id'; const cardId = 'test-card-id';
@@ -18,12 +17,11 @@ reduxHooks.useCardCreditData.mockReturnValue({
providerName, providerName,
providerStatusUrl, providerStatusUrl,
}); });
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
const renderPendingContent = (isMasquerading = false) => render( const renderPendingContent = () => render(
<IntlProvider messages={{}} locale="en"> <IntlProvider messages={{}} locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}> <PendingContent cardId={cardId} />
<PendingContent cardId={cardId} />
</MasqueradeUserContext.Provider>
</IntlProvider>, </IntlProvider>,
); );
describe('PendingContent component', () => { describe('PendingContent component', () => {
@@ -58,9 +56,9 @@ describe('PendingContent component', () => {
}); });
describe('when masqueradeData is true', () => { describe('when masqueradeData is true', () => {
it('disables the view details button', () => { it('disables the view details button', () => {
renderPendingContent(true); reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage }); const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveClass('disabled'); expect(button).toHaveClass('disabled');
}); });
}); });

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '../../../../../../hooks'; import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent'; import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink'; import ProviderLink from './components/ProviderLink';
import messages from './messages'; import messages from './messages';

View File

@@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import RejectedContent from './RejectedContent'; import RejectedContent from './RejectedContent';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCreditData: jest.fn(), useCardCreditData: jest.fn(),
}, },

View File

@@ -1,6 +1,6 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { keyStore } from '@src/utils'; import { keyStore } from 'utils';
import useCreditRequestFormData from './hooks'; import useCreditRequestFormData from './hooks';
import CreditRequestForm from '.'; import CreditRequestForm from '.';

View File

@@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { reduxHooks } from '../../../../../../../hooks'; import { reduxHooks } from 'hooks';
import { Hyperlink } from '@openedx/paragon'; import { Hyperlink } from '@openedx/paragon';
export const ProviderLink = ({ cardId }) => { export const ProviderLink = ({ cardId }) => {

View File

@@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import ProviderLink from './ProviderLink'; import ProviderLink from './ProviderLink';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCreditData: jest.fn(), useCardCreditData: jest.fn(),
}, },

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { StrictDict } from '../../../../../../utils'; import { StrictDict } from 'utils';
import { apiHooks } from '../../../../../../hooks'; import { apiHooks } from 'hooks';
import * as module from './hooks'; import * as module from './hooks';

View File

@@ -1,8 +1,8 @@
import { MockUseState } from '@src/testUtils'; import { MockUseState } from 'testUtils';
import { apiHooks } from '@src/hooks'; import { apiHooks } from 'hooks';
import * as hooks from './hooks'; import * as hooks from './hooks';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
apiHooks: { apiHooks: {
useCreateCreditRequest: jest.fn(), useCreateCreditRequest: jest.fn(),
}, },

View File

@@ -1,59 +1,59 @@
import { defineMessages } from '@openedx/frontend-base'; import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({ const messages = defineMessages({
approved: { approved: {
id: 'learner-dash.courseCard.banners.credit.approved', id: 'learner-dash.courseCard.banners.credit.approved',
description: 'Message shown when credit request has been approved', description: '',
defaultMessage: '{congratulations} {providerName} has approved your request for course credit. To see your course credit, visit the {linkToProviderSite} website.', defaultMessage: '{congratulations} {providerName} has approved your request for course credit. To see your course credit, visit the {linkToProviderSite} website.',
}, },
congratulations: { congratulations: {
id: 'learner-dash.courseCard.banners.credit.congratulations', id: 'learner-dash.courseCard.banners.credit.congratulations',
description: 'Congratulatory message for credit approval', description: '',
defaultMessage: 'Congratulations!', defaultMessage: 'Congratulations!',
}, },
eligible: { eligible: {
id: 'learner-dash.courseCard.banners.credit.eligible', id: 'learner-dash.courseCard.banners.credit.eligible',
description: 'Message shown when user is eligible to purchase course credit', description: '',
defaultMessage: 'You have completed this course and are eligible to purchase course credit. Select {getCredit} to get started.', defaultMessage: 'You have completed this course and are eligible to purchase course credit. Select {getCredit} to get started.',
}, },
eligibleFromProvider: { eligibleFromProvider: {
id: 'learner-dash.courseCard.banners.credit.eligibleFromProvider', id: 'learner-dash.courseCard.banners.credit.eligibleFromProvider',
description: 'Message shown when user is eligible for credit from a specific provider', description: '',
defaultMessage: 'You are now eligible for credit from {providerName}. Congratulations!', defaultMessage: 'You are now eligible for credit from {providerName}. Congratulations!',
}, },
getCredit: { getCredit: {
id: 'learner-dash.courseCard.banners.credit.getCredit', id: 'learner-dash.courseCard.banners.credit.getCredit',
description: 'Button text for initiating the credit process', description: '',
defaultMessage: 'Get Credit', defaultMessage: 'Get Credit',
}, },
mustRequest: { mustRequest: {
id: 'learner-dash.courseCard.banners.credit.mustRequest', id: 'learner-dash.courseCard.banners.credit.mustRequest',
description: 'Message shown after payment to instruct user to request credit', description: '',
defaultMessage: 'Thank you for your payment. To receive course credit, you must request credit at the {linkToProviderSite} website. Select {requestCredit} to get started', defaultMessage: 'Thank you for your payment. To receive course credit, you must request credit at the {linkToProviderSite} website. Select {requestCredit} to get started',
}, },
received: { received: {
id: 'learner-dash.courseCard.banners.credit.received', id: 'learner-dash.courseCard.banners.credit.received',
description: 'Message shown when credit request has been received', description: '',
defaultMessage: '{providerName} has received your course credit request. We will update you when credit processing is complete.', defaultMessage: '{providerName} has received your course credit request. We will update you when credit processing is complete.',
}, },
rejected: { rejected: {
id: 'learner-dash.courseCard.banners.credit.rejected', id: 'learner-dash.courseCard.banners.credit.rejected',
description: 'Message shown when credit request has been rejected', description: '',
defaultMessage: '{providerName} did not approve your request for course credit. For more information, contact {linkToProviderSite} directly.', defaultMessage: '{providerName} did not approve your request for course credit. For more information, contact {linkToProviderSite} directly.',
}, },
requestCredit: { requestCredit: {
id: 'learner-dash.courseCard.banners.credit.requestCredit', id: 'learner-dash.courseCard.banners.credit.requestCredit',
description: 'Button text for requesting credit', description: '',
defaultMessage: 'Request Credit', defaultMessage: 'Request Credit',
}, },
viewCredit: { viewCredit: {
id: 'learner-dash.courseCard.banners.credit.viewCredit', id: 'learner-dash.courseCard.banners.credit.viewCredit',
description: 'Button text for viewing credit details', description: '',
defaultMessage: 'View Credit', defaultMessage: 'View Credit',
}, },
viewDetails: { viewDetails: {
id: 'learner-dash.courseCard.banners.credit.viewDetails', id: 'learner-dash.courseCard.banners.credit.viewDetails',
description: 'Button text for viewing credit request details', description: '',
defaultMessage: 'View Details', defaultMessage: 'View Details',
}, },
}); });

View File

@@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, MailtoLink } from '@openedx/paragon'; import { Button, MailtoLink } from '@openedx/paragon';
import { utilHooks, reduxHooks } from '../../../../hooks'; import { utilHooks, reduxHooks } from 'hooks';
import Banner from '../../../../components/Banner';
import Banner from 'components/Banner';
import messages from './messages'; import messages from './messages';
export const EntitlementBanner = ({ cardId }) => { export const EntitlementBanner = ({ cardId }) => {

View File

@@ -1,12 +1,12 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from '@src/testUtils'; import { formatMessage } from 'testUtils';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import EntitlementBanner from './EntitlementBanner'; import EntitlementBanner from './EntitlementBanner';
import messages from './messages'; import messages from './messages';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
utilHooks: { utilHooks: {
useFormatDate: () => date => date, useFormatDate: () => date => date,
}, },

View File

@@ -2,10 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Program } from '@openedx/paragon/icons'; import { Program } from '@openedx/paragon/icons';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '../../../../../hooks'; import { reduxHooks } from 'hooks';
import Banner from '../../../../../components/Banner'; import Banner from 'components/Banner';
import ProgramList from './ProgramsList'; import ProgramList from './ProgramsList';
import messages from './messages'; import messages from './messages';

View File

@@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import RelatedProgramsBanner from '.'; import RelatedProgramsBanner from '.';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardRelatedProgramsData: jest.fn(), useCardRelatedProgramsData: jest.fn(),
}, },

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@openedx/frontend-base'; import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({ const messages = defineMessages({
relatedPrograms: { relatedPrograms: {

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { reduxHooks } from '../../../../hooks'; import { reduxHooks } from 'hooks';
import CourseBannerSlot from '../../../../slots/CourseBannerSlot';
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
import CertificateBanner from './CertificateBanner'; import CertificateBanner from './CertificateBanner';
import CreditBanner from './CreditBanner'; import CreditBanner from './CreditBanner';
import EntitlementBanner from './EntitlementBanner'; import EntitlementBanner from './EntitlementBanner';

View File

@@ -1,8 +1,8 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { MemoryRouter } from 'react-router-dom';
import { reduxHooks } from 'hooks';
import { reduxHooks } from '@src/hooks';
import CourseCardBanners from '.'; import CourseCardBanners from '.';
jest.mock('./CourseBanner', () => jest.fn(() => <div>CourseBanner</div>)); jest.mock('./CourseBanner', () => jest.fn(() => <div>CourseBanner</div>));
@@ -19,7 +19,7 @@ const mockedComponents = [
'RelatedProgramsBanner', 'RelatedProgramsBanner',
]; ];
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardEnrollmentData: jest.fn(() => ({ isEnrolled: true })), useCardEnrollmentData: jest.fn(() => ({ isEnrolled: true })),
}, },
@@ -30,14 +30,7 @@ describe('CourseCardBanners', () => {
cardId: 'test-card-id', cardId: 'test-card-id',
}; };
it('renders default CourseCardBanners', () => { it('renders default CourseCardBanners', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: true }); render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
render(
<MemoryRouter>
<IntlProvider locale="en">
<CourseCardBanners {...props} />
</IntlProvider>
</MemoryRouter>
);
mockedComponents.map((componentName) => { mockedComponents.map((componentName) => {
const mockedComponent = screen.getByText(componentName); const mockedComponent = screen.getByText(componentName);
return expect(mockedComponent).toBeInTheDocument(); return expect(mockedComponent).toBeInTheDocument();
@@ -45,13 +38,7 @@ describe('CourseCardBanners', () => {
}); });
it('render with isEnrolled false', () => { it('render with isEnrolled false', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false }); reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
render( render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
<MemoryRouter>
<IntlProvider locale="en">
<CourseCardBanners {...props} />
</IntlProvider>
</MemoryRouter>
);
const mockedComponentsIfNotEnrolled = mockedComponents.slice(-2); const mockedComponentsIfNotEnrolled = mockedComponents.slice(-2);
mockedComponentsIfNotEnrolled.map((componentName) => { mockedComponentsIfNotEnrolled.map((componentName) => {
const mockedComponent = screen.getByText(componentName); const mockedComponent = screen.getByText(componentName);

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@openedx/frontend-base'; import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({ const messages = defineMessages({
auditAccessExpired: { auditAccessExpired: {

View File

@@ -1,5 +1,5 @@
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from '../../../../hooks'; import { utilHooks, reduxHooks } from 'hooks';
import * as hooks from './hooks'; import * as hooks from './hooks';
import messages from './messages'; import messages from './messages';
@@ -10,10 +10,8 @@ export const useAccessMessage = ({ cardId }) => {
const courseRun = reduxHooks.useCardCourseRunData(cardId); const courseRun = reduxHooks.useCardCourseRunData(cardId);
const formatDate = utilHooks.useFormatDate(); const formatDate = utilHooks.useFormatDate();
if (!courseRun.isStarted) { if (!courseRun.isStarted) {
if (!courseRun.startDate && !courseRun.advertisedStart) { if (!courseRun.startDate && !courseRun.advertisedStart) { return null; }
return null; const startDate = courseRun.advertisedStart ? courseRun.advertisedStart : formatDate(courseRun.startDate);
}
const startDate = courseRun.advertisedStart ?? formatDate(courseRun.startDate);
return formatMessage(messages.courseStarts, { startDate }); return formatMessage(messages.courseStarts, { startDate });
} }
if (enrollment.isEnrolled) { if (enrollment.isEnrolled) {
@@ -29,9 +27,7 @@ export const useAccessMessage = ({ cardId }) => {
{ accessExpirationDate: formatDate(accessExpirationDate) }, { accessExpirationDate: formatDate(accessExpirationDate) },
); );
} }
if (!endDate) { if (!endDate) { return null; }
return null;
}
return formatMessage( return formatMessage(
isArchived ? messages.courseEnded : messages.courseEnds, isArchived ? messages.courseEnded : messages.courseEnds,
{ endDate: formatDate(endDate) }, { endDate: formatDate(endDate) },
@@ -53,7 +49,7 @@ export const useCardDetailsData = ({ cardId }) => {
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId); const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
return { return {
providerName: providerName ?? formatMessage(messages.unknownProviderName), providerName: providerName || formatMessage(messages.unknownProviderName),
accessMessage: hooks.useAccessMessage({ cardId }), accessMessage: hooks.useAccessMessage({ cardId }),
isEntitlement, isEntitlement,
isFulfilled, isFulfilled,

View File

@@ -1,11 +1,12 @@
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { keyStore } from 'utils';
import { utilHooks, reduxHooks } from 'hooks';
import { keyStore } from '@src/utils';
import { utilHooks, reduxHooks } from '@src/hooks';
import * as hooks from './hooks'; import * as hooks from './hooks';
import messages from './messages'; import messages from './messages';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
utilHooks: { utilHooks: {
useFormatDate: jest.fn(), useFormatDate: jest.fn(),
}, },
@@ -19,10 +20,10 @@ jest.mock('@src/hooks', () => ({
}, },
})); }));
jest.mock('@openedx/frontend-base', () => { jest.mock('@edx/frontend-platform/i18n', () => {
const { formatMessage } = jest.requireActual('@src/testUtils'); const { formatMessage } = jest.requireActual('testUtils');
return { return {
...jest.requireActual('@openedx/frontend-base'), ...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({ useIntl: () => ({
formatMessage, formatMessage,
}), }),
@@ -45,8 +46,9 @@ describe('CourseCardDetails hooks', () => {
}); });
describe('useCardDetailsData', () => { describe('useCardDetailsData', () => {
const providerName = 'my-provider-name'; const providerData = {
const providerData = {}; name: 'my-provider-name',
};
const entitlementData = { const entitlementData = {
isEntitlement: false, isEntitlement: false,
disableViewCourse: false, disableViewCourse: false,
@@ -76,10 +78,8 @@ describe('CourseCardDetails hooks', () => {
expect(out.accessMessage).toEqual(mockAccessMessage({ cardId })); expect(out.accessMessage).toEqual(mockAccessMessage({ cardId }));
}); });
it('forwards provider name if it exists, else formatted unknown provider name', () => { it('forwards provider name if it exists, else formatted unknown provider name', () => {
runHook({ provider: { name: providerName } }); expect(out.providerName).toEqual(providerData.name);
expect(out.providerName).toEqual(providerName); runHook({ provider: { name: '' } });
runHook({ provider: {} });
expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName)); expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName));
}); });
it('forward changeOrLeaveSessionMessage', () => { it('forward changeOrLeaveSessionMessage', () => {

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@openedx/frontend-base'; import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({ const messages = defineMessages({
accessExpired: { accessExpired: {

View File

@@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Badge } from '@openedx/paragon'; import { Badge } from '@openedx/paragon';
import track from '../../../tracking'; import track from 'tracking';
import { reduxHooks } from '../../../hooks'; import { reduxHooks } from 'hooks';
import verifiedRibbon from '../../../assets/verified-ribbon.png'; import verifiedRibbon from 'assets/verified-ribbon.png';
import useActionDisabledState from './hooks'; import useActionDisabledState from './hooks';
import messages from '../messages'; import messages from '../messages';

View File

@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from '@src/testUtils'; import { formatMessage } from 'testUtils';
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import useActionDisabledState from './hooks'; import useActionDisabledState from './hooks';
import { CourseCardImage } from './CourseCardImage'; import { CourseCardImage } from './CourseCardImage';
import messages from '../messages'; import messages from '../messages';
@@ -9,7 +9,7 @@ import messages from '../messages';
const homeUrl = 'https://example.com'; const homeUrl = 'https://example.com';
const bannerImgSrc = 'banner-img-src.jpg'; const bannerImgSrc = 'banner-img-src.jpg';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCourseData: jest.fn(() => ({ bannerImgSrc })), useCardCourseData: jest.fn(() => ({ bannerImgSrc })),
useCardCourseRunData: jest.fn(() => ({ homeUrl })), useCardCourseRunData: jest.fn(() => ({ homeUrl })),

View File

@@ -1,13 +1,12 @@
import { useContext } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as ReactShare from 'react-share'; import * as ReactShare from 'react-share';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@openedx/paragon'; import { Dropdown } from '@openedx/paragon';
import MasqueradeUserContext from '../../../../data/contexts/MasqueradeUserContext'; import track from 'tracking';
import track from '../../../../tracking'; import { reduxHooks } from 'hooks';
import { reduxHooks } from '../../../../hooks';
import messages from './messages'; import messages from './messages';
@@ -21,7 +20,7 @@ export const SocialShareMenu = ({ cardId, emailSettings }) => {
const { courseName } = reduxHooks.useCardCourseData(cardId); const { courseName } = reduxHooks.useCardCourseData(cardId);
const { isEmailEnabled, isExecEd2UCourse } = reduxHooks.useCardEnrollmentData(cardId); const { isEmailEnabled, isExecEd2UCourse } = reduxHooks.useCardEnrollmentData(cardId);
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId); const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
const { isMasquerading } = useContext(MasqueradeUserContext); const { isMasquerading } = reduxHooks.useMasqueradeData();
const handleTwitterShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'twitter'); const handleTwitterShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'twitter');
const handleFacebookShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'facebook'); const handleFacebookShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'facebook');

View File

@@ -1,20 +1,22 @@
import { IntlProvider } from '@openedx/frontend-base';
import { render, screen } from '@testing-library/react';
import { when } from 'jest-when'; import { when } from 'jest-when';
import track from '@src/tracking';
import { reduxHooks } from '@src/hooks'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext'; import { render, screen } from '@testing-library/react';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import { useEmailSettings } from './hooks'; import { useEmailSettings } from './hooks';
import SocialShareMenu from './SocialShareMenu'; import SocialShareMenu from './SocialShareMenu';
import messages from './messages'; import messages from './messages';
jest.mock('@src/tracking', () => ({ jest.mock('tracking', () => ({
socialShare: 'test-social-share-key', socialShare: 'test-social-share-key',
})); }));
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useMasqueradeData: jest.fn(),
useCardCourseData: jest.fn(), useCardCourseData: jest.fn(),
useCardEnrollmentData: jest.fn(), useCardEnrollmentData: jest.fn(),
useCardSocialSettingsData: jest.fn(), useCardSocialSettingsData: jest.fn(),
@@ -63,6 +65,7 @@ const mockHooks = (returnVals = {}) => {
{ isCardHook: true }, { isCardHook: true },
); );
mockHook(reduxHooks.useCardCourseData, { courseName }, { isCardHook: true }); mockHook(reduxHooks.useCardCourseData, { courseName }, { isCardHook: true });
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
mockHook( mockHook(
reduxHooks.useCardSocialSettingsData, reduxHooks.useCardSocialSettingsData,
{ {
@@ -73,13 +76,7 @@ const mockHooks = (returnVals = {}) => {
); );
}; };
const renderComponent = (isMasquerading = false) => render( const renderComponent = () => render(<IntlProvider locale="en"><SocialShareMenu {...props} /></IntlProvider>);
<IntlProvider locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<SocialShareMenu {...props} />
</MasqueradeUserContext.Provider>
</IntlProvider>,
);
describe('SocialShareMenu', () => { describe('SocialShareMenu', () => {
describe('behavior', () => { describe('behavior', () => {
@@ -94,6 +91,7 @@ describe('SocialShareMenu', () => {
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId); when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
when(reduxHooks.useCardCourseData).expectCalledWith(props.cardId); when(reduxHooks.useCardCourseData).expectCalledWith(props.cardId);
when(reduxHooks.useCardSocialSettingsData).expectCalledWith(props.cardId); when(reduxHooks.useCardSocialSettingsData).expectCalledWith(props.cardId);
when(reduxHooks.useMasqueradeData).expectCalledWith();
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter'); when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter');
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook'); when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook');
}); });
@@ -118,9 +116,7 @@ describe('SocialShareMenu', () => {
if (isMasquerading) { if (isMasquerading) {
it('is disabled', () => { it('is disabled', () => {
const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage }); const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeInTheDocument();
expect(emailSettingsButton).toHaveAttribute('aria-disabled', 'true'); expect(emailSettingsButton).toHaveAttribute('aria-disabled', 'true');
expect(emailSettingsButton).toHaveClass('disabled');
}); });
} else { } else {
it('is enabled', () => { it('is enabled', () => {
@@ -171,8 +167,8 @@ describe('SocialShareMenu', () => {
}); });
describe('masquerading', () => { describe('masquerading', () => {
beforeEach(() => { beforeEach(() => {
mockHooks({ isEmailEnabled: true }); mockHooks({ isEmailEnabled: true, isMasquerading: true });
renderComponent(true); renderComponent();
}); });
testEmailSettingsDropdown(true); testEmailSettingsDropdown(true);
}); });

View File

@@ -1,8 +1,7 @@
import track from 'tracking';
import { reduxHooks } from 'hooks';
import { useState } from 'react'; import { useState } from 'react';
import { StrictDict } from 'utils';
import { reduxHooks } from '../../../../hooks';
import track from '../../../../tracking';
import { StrictDict } from '../../../../utils';
export const state = StrictDict({ export const state = StrictDict({
isUnenrollConfirmVisible: (val) => useState(val), // eslint-disable-line isUnenrollConfirmVisible: (val) => useState(val), // eslint-disable-line
@@ -33,9 +32,7 @@ export const useHandleToggleDropdown = (cardId) => {
cardId, cardId,
); );
return (isOpen) => { return (isOpen) => {
if (isOpen) { if (isOpen) { trackCourseEvent(); }
trackCourseEvent();
}
}; };
}; };

View File

@@ -1,10 +1,10 @@
import { reduxHooks } from '@src/hooks'; import { reduxHooks } from 'hooks';
import track from '@src/tracking'; import track from 'tracking';
import { MockUseState } from '@src/testUtils'; import { MockUseState } from 'testUtils';
import * as hooks from './hooks'; import * as hooks from './hooks';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useCardCertificateData: jest.fn(), useCardCertificateData: jest.fn(),
useCardEnrollmentData: jest.fn(), useCardEnrollmentData: jest.fn(),
@@ -68,9 +68,7 @@ describe('CourseCardMenu hooks', () => {
}); });
describe('useHandleToggleDropdown', () => { describe('useHandleToggleDropdown', () => {
beforeEach(() => { beforeEach(() => { out = hooks.useHandleToggleDropdown(cardId); });
out = hooks.useHandleToggleDropdown(cardId);
});
describe('behavior', () => { describe('behavior', () => {
it('initializes course event tracker with event name and card ID', () => { it('initializes course event tracker with event name and card ID', () => {
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith( expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(

View File

@@ -1,15 +1,12 @@
import { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@openedx/frontend-base'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown, Icon, IconButton } from '@openedx/paragon'; import { Dropdown, Icon, IconButton } from '@openedx/paragon';
import { MoreVert } from '@openedx/paragon/icons'; import { MoreVert } from '@openedx/paragon/icons';
import MasqueradeUserContext from '../../../../data/contexts/MasqueradeUserContext'; import EmailSettingsModal from 'containers/EmailSettingsModal';
import EmailSettingsModal from '../../../../containers/EmailSettingsModal'; import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import UnenrollConfirmModal from '../../../../containers/UnenrollConfirmModal'; import { reduxHooks } from 'hooks';
import { reduxHooks } from '../../../../hooks';
import SocialShareMenu from './SocialShareMenu'; import SocialShareMenu from './SocialShareMenu';
import { import {
useEmailSettings, useEmailSettings,
@@ -31,7 +28,7 @@ export const CourseCardMenu = ({ cardId }) => {
const unenrollModal = useUnenrollData(); const unenrollModal = useUnenrollData();
const handleToggleDropdown = useHandleToggleDropdown(cardId); const handleToggleDropdown = useHandleToggleDropdown(cardId);
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId); const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
const { isMasquerading } = useContext(MasqueradeUserContext); const { isMasquerading } = reduxHooks.useMasqueradeData();
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId); const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
if (!shouldShowDropdown) { if (!shouldShowDropdown) {

View File

@@ -1,21 +1,23 @@
import { when } from 'jest-when'; import { when } from 'jest-when';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@openedx/frontend-base'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from '@src/hooks';
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext'; import { reduxHooks } from 'hooks';
import * as hooks from './hooks'; import * as hooks from './hooks';
import CourseCardMenu from '.'; import CourseCardMenu from '.';
import messages from './messages'; import messages from './messages';
jest.mock('@src/hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useMasqueradeData: jest.fn(),
useCardEnrollmentData: jest.fn(), useCardEnrollmentData: jest.fn(),
}, },
})); }));
jest.mock('./SocialShareMenu', () => jest.fn(() => <div>SocialShareMenu</div>)); jest.mock('./SocialShareMenu', () => jest.fn(() => <div>SocialShareMenu</div>));
jest.mock('@src/containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>)); jest.mock('containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
jest.mock('@src/containers/UnenrollConfirmModal', () => jest.fn(() => <div>UnenrollConfirmModal</div>)); jest.mock('containers/UnenrollConfirmModal', () => jest.fn(() => <div>UnenrollConfirmModal</div>));
jest.mock('./hooks', () => ({ jest.mock('./hooks', () => ({
useEmailSettings: jest.fn(), useEmailSettings: jest.fn(),
useUnenrollData: jest.fn(), useUnenrollData: jest.fn(),
@@ -67,6 +69,7 @@ const mockHooks = (returnVals = {}) => {
}, },
{ isCardHook: true }, { isCardHook: true },
); );
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
mockHook( mockHook(
reduxHooks.useCardEnrollmentData, reduxHooks.useCardEnrollmentData,
{ isEmailEnabled: !!returnVals.isEmailEnabled }, { isEmailEnabled: !!returnVals.isEmailEnabled },
@@ -74,13 +77,7 @@ const mockHooks = (returnVals = {}) => {
); );
}; };
const renderComponent = (isMasquerading = false) => render( const renderComponent = () => render(<IntlProvider locale="en"><CourseCardMenu {...props} /></IntlProvider>);
<IntlProvider locale="en">
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
<CourseCardMenu {...props} />
</MasqueradeUserContext.Provider>
</IntlProvider>
);
describe('CourseCardMenu', () => { describe('CourseCardMenu', () => {
describe('hooks', () => { describe('hooks', () => {
@@ -95,6 +92,7 @@ describe('CourseCardMenu', () => {
when(hooks.useOptionVisibility).expectCalledWith(props.cardId); when(hooks.useOptionVisibility).expectCalledWith(props.cardId);
}); });
it('initializes redux hook data ', () => { it('initializes redux hook data ', () => {
when(reduxHooks.useMasqueradeData).expectCalledWith();
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId); when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
}); });
}); });
@@ -155,13 +153,14 @@ describe('CourseCardMenu', () => {
}); });
describe('masquerading', () => { describe('masquerading', () => {
it('renders but unenroll is disabled', async () => { it('renders but unenroll is disabled', async () => {
mockHooks({ ...hookProps }); mockHooks({ ...hookProps, isMasquerading: true });
renderComponent(true); renderComponent();
const user = userEvent.setup(); const user = userEvent.setup();
const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage }); const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeInTheDocument(); expect(dropdown).toBeInTheDocument();
await user.click(dropdown); await user.click(dropdown);
const unenrollOption = screen.getByRole('button', { name: messages.unenroll.defaultMessage }); const unenrollOption = screen.getByRole('button', { name: messages.unenroll.defaultMessage });
expect(unenrollOption).toBeInTheDocument(); expect(unenrollOption).toBeInTheDocument();
expect(unenrollOption).toHaveAttribute('aria-disabled', 'true'); expect(unenrollOption).toHaveAttribute('aria-disabled', 'true');

View File

@@ -1,4 +1,4 @@
import { defineMessages } from '@openedx/frontend-base'; import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({ const messages = defineMessages({
unenroll: { unenroll: {

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