Compare commits
34 Commits
2u/main
...
frontend-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d63d9b7b4f | ||
|
|
0632525600 | ||
|
|
70576cf373 | ||
|
|
35283dfdd3 | ||
|
|
852444af42 | ||
|
|
4f117dfc96 | ||
|
|
21cb51861d | ||
|
|
48eeff44fe | ||
|
|
648be5f579 | ||
|
|
0d96fff5f1 | ||
|
|
2c295e8ecc | ||
|
|
117b518f0d | ||
|
|
31581eb2c3 | ||
|
|
a6c5f878ba | ||
|
|
f1d18c45e6 | ||
|
|
7f604ba786 | ||
|
|
3adbbbd3be | ||
|
|
2509d1b450 | ||
|
|
dd70abd61c | ||
|
|
6f4bf0a13e | ||
|
|
6bed0308bd | ||
|
|
057b925589 | ||
|
|
6202f7bb54 | ||
|
|
d0c27f4377 | ||
|
|
b2c6ec2dc9 | ||
|
|
8175d7e2f6 | ||
|
|
d0051d0a7d | ||
|
|
c621d581bd | ||
|
|
268ccc864d | ||
|
|
2045854099 | ||
|
|
9b439d7d74 | ||
|
|
b8f4d49a55 | ||
|
|
1d29810f6c | ||
|
|
89559a4987 |
@@ -1,10 +0,0 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
LICENSE
|
||||
.babelrc
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.npmignore
|
||||
commitlint.config.js
|
||||
46
.env
46
.env
@@ -1,46 +0,0 @@
|
||||
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={}
|
||||
@@ -1,52 +0,0 @@
|
||||
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
50
.env.test
@@ -1,50 +0,0 @@
|
||||
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={}
|
||||
@@ -1,5 +0,0 @@
|
||||
coverage/*
|
||||
dist/
|
||||
node_modules/
|
||||
src/postcss.config.js
|
||||
src/segment.js
|
||||
22
.eslintrc.js
22
.eslintrc.js
@@ -1,22 +0,0 @@
|
||||
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
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
*.snap linguist-generated=false
|
||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @openedx/2U-aperture
|
||||
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
@@ -29,9 +29,6 @@ jobs:
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Type check
|
||||
run: npm run types
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
32
.gitignore
vendored
32
.gitignore
vendored
@@ -1,29 +1,15 @@
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
env.config.*
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
|
||||
dist/
|
||||
public/samples/
|
||||
|
||||
### pyenv ###
|
||||
.python-version
|
||||
|
||||
### Emacs ###
|
||||
*~
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
### Development environments ###
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Local package dependencies
|
||||
module.config.js
|
||||
dist/
|
||||
/*.tgz
|
||||
|
||||
### transifex ###
|
||||
### i18n ###
|
||||
src/i18n/transifex_input.json
|
||||
temp
|
||||
src/i18n/messages
|
||||
|
||||
### Editors ###
|
||||
.DS_Store
|
||||
*~
|
||||
/temp
|
||||
/.vscode
|
||||
|
||||
16
.npmignore
16
.npmignore
@@ -1,12 +1,6 @@
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
Makefile
|
||||
npm-debug.log
|
||||
|
||||
config
|
||||
coverage
|
||||
__mocks__
|
||||
node_modules
|
||||
public
|
||||
*.test.js
|
||||
*.test.jsx
|
||||
*.test.ts
|
||||
*.test.tsx
|
||||
|
||||
18
Makefile
18
Makefile
@@ -24,6 +24,19 @@ test.npm.%: validate-no-uncommitted-package-lock-changes
|
||||
requirements: ## install ci requirements
|
||||
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:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
rm -rf $(transifex_temp)
|
||||
@@ -45,12 +58,11 @@ pull_translations:
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/frontend-base/src/i18n/messages:frontend-base \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
|
||||
|
||||
$(intl_imports) frontend-platform paragon frontend-component-footer frontend-app-learner-dashboard
|
||||
$(intl_imports) frontend-base paragon frontend-app-learner-dashboard
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
19
README.rst
19
README.rst
@@ -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
|
||||
(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
|
||||
"widget" containers to provide upsell and discovery widgets as sidebar/footer components.
|
||||
"widget" containers to provide upsell and discovery widgets as sidebar components.
|
||||
|
||||
Quickstart
|
||||
----------
|
||||
@@ -30,21 +30,10 @@ 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
|
||||
already, and then redirected to your home page.
|
||||
|
||||
Plugins
|
||||
Widgets
|
||||
-------
|
||||
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||
|
||||
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.
|
||||
This MFE can be customized with widgets. The parts of this MFE that can be customized in that manner are documented
|
||||
`here </src/slots>`_.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
10
app.d.ts
vendored
Normal file
10
app.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/// <reference types="@openedx/frontend-base" />
|
||||
|
||||
declare module 'site.config' {
|
||||
export default SiteConfig;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
3
babel.config.js
Normal file
3
babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const { createConfig } = require('@openedx/frontend-base/tools');
|
||||
|
||||
module.exports = createConfig('babel');
|
||||
22
eslint.config.js
Normal file
22
eslint.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// @ts-check
|
||||
|
||||
const { createLintConfig } = require('@openedx/frontend-base/tools');
|
||||
|
||||
module.exports = createLintConfig(
|
||||
{
|
||||
files: [
|
||||
'src/**/*',
|
||||
'site.config.*',
|
||||
],
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'coverage/*',
|
||||
'dist/*',
|
||||
'documentation/*',
|
||||
'node_modules/*',
|
||||
'**/__mocks__/*',
|
||||
'**/__snapshots__/*',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
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,
|
||||
};
|
||||
@@ -1,18 +1,22 @@
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-base/tools');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
module.exports = createConfig('test', {
|
||||
setupFilesAfterEnv: [
|
||||
'jest-expect-message',
|
||||
'<rootDir>/src/setupTest.jsx',
|
||||
],
|
||||
modulePaths: ['<rootDir>/src/'],
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
'testUtils', // don't unit test jest mocking tools
|
||||
'src/data/services/lms/fakeData', // don't unit test mock data
|
||||
'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,
|
||||
testEnvironment: 'jsdom',
|
||||
});
|
||||
|
||||
10679
package-lock.json
generated
10679
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
86
package.json
Executable file → Normal file
86
package.json
Executable file → Normal file
@@ -1,79 +1,69 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-learner-dashboard",
|
||||
"version": "0.0.1",
|
||||
"name": "@openedx/frontend-app-learner-dashboard",
|
||||
"version": "1.0.0-alpha.6",
|
||||
"description": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./app.scss": "./dist/app.scss"
|
||||
},
|
||||
"files": [
|
||||
"/dist"
|
||||
],
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"sideEffects": [
|
||||
"*.css",
|
||||
"*.scss"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"dev": "PUBLIC_PATH=/learner-dashboard/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"quality": "npm run lint-fix && npm run test",
|
||||
"watch-tests": "jest --watch",
|
||||
"types": "tsc --noEmit"
|
||||
"build": "make build",
|
||||
"clean": "make clean",
|
||||
"dev": "PORT=1996 PUBLIC_PATH=/learner-dashboard openedx dev",
|
||||
"i18n_extract": "openedx formatjs extract",
|
||||
"lint": "openedx lint .",
|
||||
"lint:fix": "openedx lint --fix .",
|
||||
"prepack": "npm run build",
|
||||
"test": "openedx test --coverage --passWithNoTests"
|
||||
},
|
||||
"author": "edX",
|
||||
"author": "Open edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "",
|
||||
"homepage": "https://github.com/openedx/frontend-app-learner-dashboard#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/openedx/frontend-app-learner-dashboard/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^23.4.5",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.45.1",
|
||||
"filesize": "^10.0.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"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",
|
||||
"redux": "4.2.1",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"reselect": "^4.0.0",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"util": "^0.12.4"
|
||||
"reselect": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.3.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@edx/browserslist-config": "^1.5.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
@@ -81,6 +71,20 @@
|
||||
"jest-when": "^3.6.0",
|
||||
"react-dev-utils": "^12.0.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us" dir="ltr">
|
||||
<head>
|
||||
<title>Learner Dashboard Development Site></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
40
site.config.dev.tsx
Normal file
40
site.config.dev.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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;
|
||||
27
site.config.test.tsx
Normal file
27
site.config.test.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
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
100
src/App.jsx
@@ -1,100 +0,0 @@
|
||||
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
66
src/App.scss
@@ -1,66 +0,0 @@
|
||||
// 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
135
src/App.test.jsx
@@ -1,135 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
20
src/Main.jsx
Normal file
20
src/Main.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
1
src/__mocks__/file.js
Normal file
1
src/__mocks__/file.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = 'FileMock';
|
||||
1
src/__mocks__/svg.js
Normal file
1
src/__mocks__/svg.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = 'SvgURL';
|
||||
38
src/app.scss
Executable file
38
src/app.scss
Executable file
@@ -0,0 +1,38 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/app.ts
Normal file
23
src/app.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
@@ -1,15 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 765 B |
@@ -1,25 +0,0 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
|
||||
|
||||
export const getNotices = ({ onLoad, notFoundMessage }) => {
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
|
||||
const handleError = async (e) => {
|
||||
// Error probably means that notices is not installed, which is fine.
|
||||
const { customAttributes: { httpErrorStatus } } = e;
|
||||
if (httpErrorStatus === 404) {
|
||||
logInfo(`${e}. ${notFoundMessage}`);
|
||||
} else {
|
||||
logError(e);
|
||||
}
|
||||
};
|
||||
if (authenticatedUser) {
|
||||
return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default { getNotices };
|
||||
@@ -1,65 +0,0 @@
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({
|
||||
LMS_BASE_URL: 'test-lms-url',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
getAuthenticatedUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
logError: jest.fn(),
|
||||
logInfo: jest.fn(),
|
||||
}));
|
||||
|
||||
const testData = 'test-data';
|
||||
const successfulGet = () => Promise.resolve(testData);
|
||||
const error404 = { customAttributes: { httpErrorStatus: 404 }, test: 'error' };
|
||||
const error404Get = () => Promise.reject(error404);
|
||||
const error500 = { customAttributes: { httpErrorStatus: 500 }, test: 'error' };
|
||||
const error500Get = () => Promise.reject(error500);
|
||||
|
||||
const get = jest.fn().mockImplementation(successfulGet);
|
||||
getAuthenticatedHttpClient.mockReturnValue({ get });
|
||||
const authenticatedUser = { fake: 'user' };
|
||||
getAuthenticatedUser.mockReturnValue(authenticatedUser);
|
||||
|
||||
const onLoad = jest.fn();
|
||||
describe('getNotices api method', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('not authenticated', () => {
|
||||
it('does not fetch anything', () => {
|
||||
getAuthenticatedUser.mockReturnValueOnce(null);
|
||||
api.getNotices({ onLoad });
|
||||
expect(get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('authenticated', () => {
|
||||
it('fetches noticesUrl with onLoad behavior', async () => {
|
||||
await api.getNotices({ onLoad });
|
||||
expect(get).toHaveBeenCalledWith(api.noticesUrl, {});
|
||||
expect(onLoad).toHaveBeenCalledWith(testData);
|
||||
});
|
||||
it('calls logInfo if fetch fails with 404', async () => {
|
||||
get.mockImplementation(error404Get);
|
||||
await api.getNotices({ onLoad });
|
||||
expect(logInfo).toHaveBeenCalledWith(`${error404}. ${api.error404Message}`);
|
||||
});
|
||||
it('calls logError if fetch fails with non-404 error', async () => {
|
||||
get.mockImplementation(error500Get);
|
||||
await api.getNotices({ onLoad });
|
||||
expect(logError).toHaveBeenCalledWith(error500);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@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;
|
||||
@@ -1,99 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
|
||||
/**
|
||||
* This component uses the platform-plugin-notices plugin to function.
|
||||
* If the user has an unacknowledged notice, they will be rerouted off
|
||||
* course home and onto a full-screen notice page. If the plugin is not
|
||||
* installed, or there are no notices, we just passthrough this component.
|
||||
*/
|
||||
const NoticesWrapper = ({ children }) => {
|
||||
const { isRedirected } = useNoticesWrapperData();
|
||||
return (
|
||||
<div>
|
||||
{isRedirected === true ? null : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoticesWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default NoticesWrapper;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
import NoticesWrapper from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = { isRedirected: false };
|
||||
|
||||
const children = [<b key={1}>some</b>, <i key={2}>children</i>];
|
||||
describe('NoticesWrapper component', () => {
|
||||
beforeEach(() => {
|
||||
useNoticesWrapperData.mockClear();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
useNoticesWrapperData.mockReturnValue(hookProps);
|
||||
render(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(useNoticesWrapperData).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('does not show children if redirected', () => {
|
||||
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
|
||||
render(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(screen.queryByText('some')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('children')).not.toBeInTheDocument();
|
||||
});
|
||||
it('shows children if not redirected', () => {
|
||||
useNoticesWrapperData.mockReturnValue(hookProps);
|
||||
render(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(screen.getByText('some')).toBeInTheDocument();
|
||||
expect(screen.getByText('children')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
error404Message: {
|
||||
id: 'learner-dash.notices.error404Message',
|
||||
defaultMessage: 'This probably happened because the notices plugin is not installed on platform.',
|
||||
description: 'Error message when notices API returns 404',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,28 +0,0 @@
|
||||
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 };
|
||||
1
src/constants.ts
Normal file
1
src/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const appId = 'org.openedx.frontend.app.learnerDashboard';
|
||||
@@ -1,13 +0,0 @@
|
||||
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;
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import track from '../../../../tracking';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import track from '@src/tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
jest.mock('@src/tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import track from '../../../../tracking';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import track from '@src/tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ResumeButton from './ResumeButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
jest.mock('@src/tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useUpdateSelectSessionModalCallback: jest.fn(),
|
||||
},
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import track from '../../../../tracking';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from '@src/tracking';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
jest.mock('@src/tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
|
||||
@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
|
||||
|
||||
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 BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
|
||||
import CourseCardActions from '.';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
@@ -12,7 +12,7 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('plugin-slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
|
||||
jest.mock('@src/slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
|
||||
jest.mock('./SelectSessionButton', () => jest.fn(() => <div>SelectSessionButton</div>));
|
||||
jest.mock('./ViewCourseButton', () => jest.fn(() => <div>ViewCourseButton</div>));
|
||||
jest.mock('./BeginCourseButton', () => jest.fn(() => <div>BeginCourseButton</div>));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
beginCourse: {
|
||||
|
||||
@@ -4,10 +4,10 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { MailtoLink, Hyperlink } from '@openedx/paragon';
|
||||
import { CheckCircle } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
import { utilHooks, reduxHooks } from '../../../../hooks';
|
||||
import Banner from '../../../../components/Banner';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -31,7 +31,7 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
if (certificate.isRestricted) {
|
||||
return (
|
||||
<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 && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))}
|
||||
</Banner>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import CertificateBanner from './CertificateBanner';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
utilHooks: {
|
||||
useFormatDate: jest.fn(() => date => date),
|
||||
},
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { utilHooks, reduxHooks } from '../../../../hooks';
|
||||
import Banner from '../../../../components/Banner';
|
||||
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
import messages from './messages';
|
||||
|
||||
export const CourseBanner = ({ cardId }) => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import { formatMessage } from '@src/testUtils';
|
||||
import { CourseBanner } from './CourseBanner';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
utilHooks: {
|
||||
useFormatDate: () => date => date,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { StrictDict } from '../../../../../utils';
|
||||
import { reduxHooks } from '../../../../../hooks';
|
||||
|
||||
import ApprovedContent from './views/ApprovedContent';
|
||||
import EligibleContent from './views/EligibleContent';
|
||||
@@ -17,7 +16,9 @@ export const statusComponents = StrictDict({
|
||||
export const useCreditBannerData = (cardId) => {
|
||||
const credit = reduxHooks.useCardCreditData(cardId);
|
||||
const { supportEmail } = reduxHooks.usePlatformSettingsData();
|
||||
if (!credit.isEligible) { return null; }
|
||||
if (!credit.isEligible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { error, purchased, requestStatus } = credit;
|
||||
let ContentComponent = EligibleContent;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { keyStore } from 'utils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { keyStore } from '@src/utils';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
|
||||
import ApprovedContent from './views/ApprovedContent';
|
||||
import EligibleContent from './views/EligibleContent';
|
||||
@@ -9,7 +9,7 @@ import RejectedContent from './views/RejectedContent';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
import Banner from '../../../../../components/Banner';
|
||||
|
||||
import { MailtoLink } from '@openedx/paragon';
|
||||
import hooks from './hooks';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import hooks from './hooks';
|
||||
import { CreditBanner } from '.';
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
error: {
|
||||
id: 'learner-dash.courseCard.banners.credit.error',
|
||||
description: '',
|
||||
description: 'Error message for credit transaction with support email link',
|
||||
defaultMessage: 'An error occurred with this transaction. For help, contact {supportEmailLink}.',
|
||||
},
|
||||
errorNoEmail: {
|
||||
id: 'learner-dash.courseCard.banners.credit.errorNoEmail',
|
||||
description: '',
|
||||
description: 'Error message for credit transaction without support email',
|
||||
defaultMessage: 'An error occurred with this transaction.',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useContext } from 'react';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';
|
||||
import { reduxHooks } from '../../../../../../hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
|
||||
@@ -11,7 +11,7 @@ import messages from './messages';
|
||||
|
||||
export const ApprovedContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import { formatMessage } from '@src/testUtils';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
|
||||
import messages from './messages';
|
||||
import ApprovedContent from './ApprovedContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -18,12 +18,19 @@ const credit = {
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
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('hooks', () => {
|
||||
it('initializes credit data with cardId', () => {
|
||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||
renderWithMasquerading();
|
||||
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
@@ -31,7 +38,7 @@ describe('ApprovedContent component', () => {
|
||||
describe('rendered CreditContent component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||
renderWithMasquerading();
|
||||
});
|
||||
it('action.message is formatted viewCredit message', () => {
|
||||
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
|
||||
@@ -56,8 +63,7 @@ describe('ApprovedContent component', () => {
|
||||
});
|
||||
describe('when masquerading', () => {
|
||||
beforeEach(() => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||
renderWithMasquerading(true);
|
||||
});
|
||||
|
||||
it('disables the action button', () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from '../../../../../../hooks';
|
||||
import track from '../../../../../../tracking';
|
||||
|
||||
import CreditContent from './components/CreditContent';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import track from '@src/tracking';
|
||||
|
||||
import messages from './messages';
|
||||
import EligibleContent from './EligibleContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
jest.mock('@src/tracking', () => ({
|
||||
credit: {
|
||||
purchase: jest.fn(),
|
||||
},
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useContext } from 'react';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import hooks from './hooks';
|
||||
@@ -13,7 +12,7 @@ import messages from './messages';
|
||||
export const MustRequestContent = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
|
||||
import messages from './messages';
|
||||
import hooks from './hooks';
|
||||
import MustRequestContent from './MustRequestContent';
|
||||
@@ -11,9 +12,8 @@ jest.mock('./hooks', () => ({
|
||||
useCreditRequestData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardCreditData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
@@ -31,9 +31,11 @@ const providerName = 'test-credit-provider-name';
|
||||
const providerStatusUrl = 'test-credit-provider-status-url';
|
||||
const createCreditRequest = jest.fn().mockName('createCreditRequest');
|
||||
|
||||
const renderMustRequestContent = () => render(
|
||||
const renderMustRequestContent = (isMasquerading = false) => render(
|
||||
<IntlProvider locale="en" messages={messages}>
|
||||
<MustRequestContent cardId={cardId} />
|
||||
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
|
||||
<MustRequestContent cardId={cardId} />
|
||||
</MasqueradeUserContext.Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -44,7 +46,6 @@ describe('MustRequestContent component', () => {
|
||||
requestData,
|
||||
createCreditRequest,
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
reduxHooks.useCardCreditData.mockReturnValue({
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
@@ -90,14 +91,13 @@ describe('MustRequestContent component', () => {
|
||||
|
||||
describe('when masquerading', () => {
|
||||
beforeEach(() => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
||||
renderMustRequestContent();
|
||||
renderMustRequestContent(true);
|
||||
});
|
||||
|
||||
it('disables the request credit button', () => {
|
||||
const button = screen.getByRole('button', { name: /request credit/i });
|
||||
expect(button).toHaveClass('disabled');
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(button).toHaveClass('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useContext } from 'react';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import MasqueradeUserContext from '../../../../../../data/contexts/MasqueradeUserContext';
|
||||
import { reduxHooks } from '../../../../../../hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import messages from './messages';
|
||||
|
||||
export const PendingContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
|
||||
|
||||
import messages from './messages';
|
||||
import PendingContent from './PendingContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: { useCardCreditData: jest.fn() },
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
@@ -17,11 +18,12 @@ reduxHooks.useCardCreditData.mockReturnValue({
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
const renderPendingContent = () => render(
|
||||
const renderPendingContent = (isMasquerading = false) => render(
|
||||
<IntlProvider messages={{}} locale="en">
|
||||
<PendingContent cardId={cardId} />
|
||||
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
|
||||
<PendingContent cardId={cardId} />
|
||||
</MasqueradeUserContext.Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
describe('PendingContent component', () => {
|
||||
@@ -56,9 +58,9 @@ describe('PendingContent component', () => {
|
||||
});
|
||||
describe('when masqueradeData is true', () => {
|
||||
it('disables the view details button', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: true });
|
||||
renderPendingContent();
|
||||
renderPendingContent(true);
|
||||
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(button).toHaveClass('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../../../../../hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import RejectedContent from './RejectedContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
import { keyStore } from '@src/utils';
|
||||
|
||||
import useCreditRequestFormData from './hooks';
|
||||
import CreditRequestForm from '.';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '../../../../../../../hooks';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
|
||||
export const ProviderLink = ({ cardId }) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
|
||||
import ProviderLink from './ProviderLink';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { apiHooks } from 'hooks';
|
||||
import { StrictDict } from '../../../../../../utils';
|
||||
import { apiHooks } from '../../../../../../hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { apiHooks } from 'hooks';
|
||||
import { MockUseState } from '@src/testUtils';
|
||||
import { apiHooks } from '@src/hooks';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
apiHooks: {
|
||||
useCreateCreditRequest: jest.fn(),
|
||||
},
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
approved: {
|
||||
id: 'learner-dash.courseCard.banners.credit.approved',
|
||||
description: '',
|
||||
description: 'Message shown when credit request has been approved',
|
||||
defaultMessage: '{congratulations} {providerName} has approved your request for course credit. To see your course credit, visit the {linkToProviderSite} website.',
|
||||
},
|
||||
congratulations: {
|
||||
id: 'learner-dash.courseCard.banners.credit.congratulations',
|
||||
description: '',
|
||||
description: 'Congratulatory message for credit approval',
|
||||
defaultMessage: 'Congratulations!',
|
||||
},
|
||||
eligible: {
|
||||
id: 'learner-dash.courseCard.banners.credit.eligible',
|
||||
description: '',
|
||||
description: 'Message shown when user is eligible to purchase course credit',
|
||||
defaultMessage: 'You have completed this course and are eligible to purchase course credit. Select {getCredit} to get started.',
|
||||
},
|
||||
eligibleFromProvider: {
|
||||
id: 'learner-dash.courseCard.banners.credit.eligibleFromProvider',
|
||||
description: '',
|
||||
description: 'Message shown when user is eligible for credit from a specific provider',
|
||||
defaultMessage: 'You are now eligible for credit from {providerName}. Congratulations!',
|
||||
},
|
||||
getCredit: {
|
||||
id: 'learner-dash.courseCard.banners.credit.getCredit',
|
||||
description: '',
|
||||
description: 'Button text for initiating the credit process',
|
||||
defaultMessage: 'Get Credit',
|
||||
},
|
||||
mustRequest: {
|
||||
id: 'learner-dash.courseCard.banners.credit.mustRequest',
|
||||
description: '',
|
||||
description: 'Message shown after payment to instruct user to request credit',
|
||||
defaultMessage: 'Thank you for your payment. To receive course credit, you must request credit at the {linkToProviderSite} website. Select {requestCredit} to get started',
|
||||
},
|
||||
received: {
|
||||
id: 'learner-dash.courseCard.banners.credit.received',
|
||||
description: '',
|
||||
description: 'Message shown when credit request has been received',
|
||||
defaultMessage: '{providerName} has received your course credit request. We will update you when credit processing is complete.',
|
||||
},
|
||||
rejected: {
|
||||
id: 'learner-dash.courseCard.banners.credit.rejected',
|
||||
description: '',
|
||||
description: 'Message shown when credit request has been rejected',
|
||||
defaultMessage: '{providerName} did not approve your request for course credit. For more information, contact {linkToProviderSite} directly.',
|
||||
},
|
||||
requestCredit: {
|
||||
id: 'learner-dash.courseCard.banners.credit.requestCredit',
|
||||
description: '',
|
||||
description: 'Button text for requesting credit',
|
||||
defaultMessage: 'Request Credit',
|
||||
},
|
||||
viewCredit: {
|
||||
id: 'learner-dash.courseCard.banners.credit.viewCredit',
|
||||
description: '',
|
||||
description: 'Button text for viewing credit details',
|
||||
defaultMessage: 'View Credit',
|
||||
},
|
||||
viewDetails: {
|
||||
id: 'learner-dash.courseCard.banners.credit.viewDetails',
|
||||
description: '',
|
||||
description: 'Button text for viewing credit request details',
|
||||
defaultMessage: 'View Details',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
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';
|
||||
|
||||
export const EntitlementBanner = ({ cardId }) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import { formatMessage } from '@src/testUtils';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
utilHooks: {
|
||||
useFormatDate: () => date => date,
|
||||
},
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Program } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
import { reduxHooks } from '../../../../../hooks';
|
||||
import Banner from '../../../../../components/Banner';
|
||||
|
||||
import ProgramList from './ProgramsList';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import RelatedProgramsBanner from '.';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardRelatedProgramsData: jest.fn(),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
relatedPrograms: {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
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 CreditBanner from './CreditBanner';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import CourseCardBanners from '.';
|
||||
|
||||
jest.mock('./CourseBanner', () => jest.fn(() => <div>CourseBanner</div>));
|
||||
@@ -19,7 +19,7 @@ const mockedComponents = [
|
||||
'RelatedProgramsBanner',
|
||||
];
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardEnrollmentData: jest.fn(() => ({ isEnrolled: true })),
|
||||
},
|
||||
@@ -30,7 +30,14 @@ describe('CourseCardBanners', () => {
|
||||
cardId: 'test-card-id',
|
||||
};
|
||||
it('renders default CourseCardBanners', () => {
|
||||
render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: true });
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<IntlProvider locale="en">
|
||||
<CourseCardBanners {...props} />
|
||||
</IntlProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
mockedComponents.map((componentName) => {
|
||||
const mockedComponent = screen.getByText(componentName);
|
||||
return expect(mockedComponent).toBeInTheDocument();
|
||||
@@ -38,7 +45,13 @@ describe('CourseCardBanners', () => {
|
||||
});
|
||||
it('render with isEnrolled false', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
|
||||
render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<IntlProvider locale="en">
|
||||
<CourseCardBanners {...props} />
|
||||
</IntlProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
const mockedComponentsIfNotEnrolled = mockedComponents.slice(-2);
|
||||
mockedComponentsIfNotEnrolled.map((componentName) => {
|
||||
const mockedComponent = screen.getByText(componentName);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
auditAccessExpired: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { utilHooks, reduxHooks } from '../../../../hooks';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
@@ -10,8 +10,10 @@ export const useAccessMessage = ({ cardId }) => {
|
||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
||||
const formatDate = utilHooks.useFormatDate();
|
||||
if (!courseRun.isStarted) {
|
||||
if (!courseRun.startDate && !courseRun.advertisedStart) { return null; }
|
||||
const startDate = courseRun.advertisedStart ? courseRun.advertisedStart : formatDate(courseRun.startDate);
|
||||
if (!courseRun.startDate && !courseRun.advertisedStart) {
|
||||
return null;
|
||||
}
|
||||
const startDate = courseRun.advertisedStart ?? formatDate(courseRun.startDate);
|
||||
return formatMessage(messages.courseStarts, { startDate });
|
||||
}
|
||||
if (enrollment.isEnrolled) {
|
||||
@@ -27,7 +29,9 @@ export const useAccessMessage = ({ cardId }) => {
|
||||
{ accessExpirationDate: formatDate(accessExpirationDate) },
|
||||
);
|
||||
}
|
||||
if (!endDate) { return null; }
|
||||
if (!endDate) {
|
||||
return null;
|
||||
}
|
||||
return formatMessage(
|
||||
isArchived ? messages.courseEnded : messages.courseEnds,
|
||||
{ endDate: formatDate(endDate) },
|
||||
@@ -49,7 +53,7 @@ export const useCardDetailsData = ({ cardId }) => {
|
||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
|
||||
return {
|
||||
providerName: providerName || formatMessage(messages.unknownProviderName),
|
||||
providerName: providerName ?? formatMessage(messages.unknownProviderName),
|
||||
accessMessage: hooks.useAccessMessage({ cardId }),
|
||||
isEntitlement,
|
||||
isFulfilled,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { keyStore } from '@src/utils';
|
||||
import { utilHooks, reduxHooks } from '@src/hooks';
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
utilHooks: {
|
||||
useFormatDate: jest.fn(),
|
||||
},
|
||||
@@ -20,10 +19,10 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => {
|
||||
const { formatMessage } = jest.requireActual('testUtils');
|
||||
jest.mock('@openedx/frontend-base', () => {
|
||||
const { formatMessage } = jest.requireActual('@src/testUtils');
|
||||
return {
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
...jest.requireActual('@openedx/frontend-base'),
|
||||
useIntl: () => ({
|
||||
formatMessage,
|
||||
}),
|
||||
@@ -46,9 +45,8 @@ describe('CourseCardDetails hooks', () => {
|
||||
});
|
||||
|
||||
describe('useCardDetailsData', () => {
|
||||
const providerData = {
|
||||
name: 'my-provider-name',
|
||||
};
|
||||
const providerName = 'my-provider-name';
|
||||
const providerData = {};
|
||||
const entitlementData = {
|
||||
isEntitlement: false,
|
||||
disableViewCourse: false,
|
||||
@@ -78,8 +76,10 @@ describe('CourseCardDetails hooks', () => {
|
||||
expect(out.accessMessage).toEqual(mockAccessMessage({ cardId }));
|
||||
});
|
||||
it('forwards provider name if it exists, else formatted unknown provider name', () => {
|
||||
expect(out.providerName).toEqual(providerData.name);
|
||||
runHook({ provider: { name: '' } });
|
||||
runHook({ provider: { name: providerName } });
|
||||
expect(out.providerName).toEqual(providerName);
|
||||
|
||||
runHook({ provider: {} });
|
||||
expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName));
|
||||
});
|
||||
it('forward changeOrLeaveSessionMessage', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
accessExpired: {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
|
||||
import { Badge } from '@openedx/paragon';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import verifiedRibbon from 'assets/verified-ribbon.png';
|
||||
import track from '../../../tracking';
|
||||
import { reduxHooks } from '../../../hooks';
|
||||
import verifiedRibbon from '../../../assets/verified-ribbon.png';
|
||||
import useActionDisabledState from './hooks';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import { formatMessage } from '@src/testUtils';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import useActionDisabledState from './hooks';
|
||||
import { CourseCardImage } from './CourseCardImage';
|
||||
import messages from '../messages';
|
||||
@@ -9,7 +9,7 @@ import messages from '../messages';
|
||||
const homeUrl = 'https://example.com';
|
||||
const bannerImgSrc = 'banner-img-src.jpg';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseData: jest.fn(() => ({ bannerImgSrc })),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as ReactShare from 'react-share';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { Dropdown } from '@openedx/paragon';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import MasqueradeUserContext from '../../../../data/contexts/MasqueradeUserContext';
|
||||
import track from '../../../../tracking';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -20,7 +21,7 @@ export const SocialShareMenu = ({ cardId, emailSettings }) => {
|
||||
const { courseName } = reduxHooks.useCardCourseData(cardId);
|
||||
const { isEmailEnabled, isExecEd2UCourse } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { twitter, facebook } = reduxHooks.useCardSocialSettingsData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
|
||||
const handleTwitterShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'twitter');
|
||||
const handleFacebookShare = reduxHooks.useTrackCourseEvent(track.socialShare, cardId, 'facebook');
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { when } from 'jest-when';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { when } from 'jest-when';
|
||||
import track from '@src/tracking';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
|
||||
|
||||
import { useEmailSettings } from './hooks';
|
||||
import SocialShareMenu from './SocialShareMenu';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
jest.mock('@src/tracking', () => ({
|
||||
socialShare: 'test-social-share-key',
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardSocialSettingsData: jest.fn(),
|
||||
@@ -65,7 +63,6 @@ const mockHooks = (returnVals = {}) => {
|
||||
{ isCardHook: true },
|
||||
);
|
||||
mockHook(reduxHooks.useCardCourseData, { courseName }, { isCardHook: true });
|
||||
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
|
||||
mockHook(
|
||||
reduxHooks.useCardSocialSettingsData,
|
||||
{
|
||||
@@ -76,7 +73,13 @@ const mockHooks = (returnVals = {}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderComponent = () => render(<IntlProvider locale="en"><SocialShareMenu {...props} /></IntlProvider>);
|
||||
const renderComponent = (isMasquerading = false) => render(
|
||||
<IntlProvider locale="en">
|
||||
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
|
||||
<SocialShareMenu {...props} />
|
||||
</MasqueradeUserContext.Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
describe('SocialShareMenu', () => {
|
||||
describe('behavior', () => {
|
||||
@@ -91,7 +94,6 @@ describe('SocialShareMenu', () => {
|
||||
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useCardCourseData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useCardSocialSettingsData).expectCalledWith(props.cardId);
|
||||
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
||||
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'twitter');
|
||||
when(reduxHooks.useTrackCourseEvent).expectCalledWith(track.socialShare, props.cardId, 'facebook');
|
||||
});
|
||||
@@ -116,7 +118,9 @@ describe('SocialShareMenu', () => {
|
||||
if (isMasquerading) {
|
||||
it('is disabled', () => {
|
||||
const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
|
||||
expect(emailSettingsButton).toBeInTheDocument();
|
||||
expect(emailSettingsButton).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(emailSettingsButton).toHaveClass('disabled');
|
||||
});
|
||||
} else {
|
||||
it('is enabled', () => {
|
||||
@@ -167,8 +171,8 @@ describe('SocialShareMenu', () => {
|
||||
});
|
||||
describe('masquerading', () => {
|
||||
beforeEach(() => {
|
||||
mockHooks({ isEmailEnabled: true, isMasquerading: true });
|
||||
renderComponent();
|
||||
mockHooks({ isEmailEnabled: true });
|
||||
renderComponent(true);
|
||||
});
|
||||
testEmailSettingsDropdown(true);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { useState } from 'react';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
import track from '../../../../tracking';
|
||||
import { StrictDict } from '../../../../utils';
|
||||
|
||||
export const state = StrictDict({
|
||||
isUnenrollConfirmVisible: (val) => useState(val), // eslint-disable-line
|
||||
@@ -32,7 +33,9 @@ export const useHandleToggleDropdown = (cardId) => {
|
||||
cardId,
|
||||
);
|
||||
return (isOpen) => {
|
||||
if (isOpen) { trackCourseEvent(); }
|
||||
if (isOpen) {
|
||||
trackCourseEvent();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import { MockUseState } from 'testUtils';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import track from '@src/tracking';
|
||||
import { MockUseState } from '@src/testUtils';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCertificateData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
@@ -68,7 +68,9 @@ describe('CourseCardMenu hooks', () => {
|
||||
});
|
||||
|
||||
describe('useHandleToggleDropdown', () => {
|
||||
beforeEach(() => { out = hooks.useHandleToggleDropdown(cardId); });
|
||||
beforeEach(() => {
|
||||
out = hooks.useHandleToggleDropdown(cardId);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes course event tracker with event name and card ID', () => {
|
||||
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { Dropdown, Icon, IconButton } from '@openedx/paragon';
|
||||
import { MoreVert } from '@openedx/paragon/icons';
|
||||
|
||||
import EmailSettingsModal from 'containers/EmailSettingsModal';
|
||||
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import MasqueradeUserContext from '../../../../data/contexts/MasqueradeUserContext';
|
||||
import EmailSettingsModal from '../../../../containers/EmailSettingsModal';
|
||||
import UnenrollConfirmModal from '../../../../containers/UnenrollConfirmModal';
|
||||
import { reduxHooks } from '../../../../hooks';
|
||||
|
||||
import SocialShareMenu from './SocialShareMenu';
|
||||
import {
|
||||
useEmailSettings,
|
||||
@@ -28,7 +31,7 @@ export const CourseCardMenu = ({ cardId }) => {
|
||||
const unenrollModal = useUnenrollData();
|
||||
const handleToggleDropdown = useHandleToggleDropdown(cardId);
|
||||
const { shouldShowUnenrollItem, shouldShowDropdown } = useOptionVisibility(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { isMasquerading } = useContext(MasqueradeUserContext);
|
||||
const { isEmailEnabled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
|
||||
if (!shouldShowDropdown) {
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { when } from 'jest-when';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import { reduxHooks } from '@src/hooks';
|
||||
import MasqueradeUserContext from '@src/data/contexts/MasqueradeUserContext';
|
||||
import * as hooks from './hooks';
|
||||
import CourseCardMenu from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
jest.mock('@src/hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./SocialShareMenu', () => jest.fn(() => <div>SocialShareMenu</div>));
|
||||
jest.mock('containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
|
||||
jest.mock('containers/UnenrollConfirmModal', () => jest.fn(() => <div>UnenrollConfirmModal</div>));
|
||||
jest.mock('@src/containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
|
||||
jest.mock('@src/containers/UnenrollConfirmModal', () => jest.fn(() => <div>UnenrollConfirmModal</div>));
|
||||
jest.mock('./hooks', () => ({
|
||||
useEmailSettings: jest.fn(),
|
||||
useUnenrollData: jest.fn(),
|
||||
@@ -69,7 +67,6 @@ const mockHooks = (returnVals = {}) => {
|
||||
},
|
||||
{ isCardHook: true },
|
||||
);
|
||||
mockHook(reduxHooks.useMasqueradeData, { isMasquerading: !!returnVals.isMasquerading });
|
||||
mockHook(
|
||||
reduxHooks.useCardEnrollmentData,
|
||||
{ isEmailEnabled: !!returnVals.isEmailEnabled },
|
||||
@@ -77,7 +74,13 @@ const mockHooks = (returnVals = {}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderComponent = () => render(<IntlProvider locale="en"><CourseCardMenu {...props} /></IntlProvider>);
|
||||
const renderComponent = (isMasquerading = false) => render(
|
||||
<IntlProvider locale="en">
|
||||
<MasqueradeUserContext.Provider value={{ isMasquerading }}>
|
||||
<CourseCardMenu {...props} />
|
||||
</MasqueradeUserContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('CourseCardMenu', () => {
|
||||
describe('hooks', () => {
|
||||
@@ -92,7 +95,6 @@ describe('CourseCardMenu', () => {
|
||||
when(hooks.useOptionVisibility).expectCalledWith(props.cardId);
|
||||
});
|
||||
it('initializes redux hook data ', () => {
|
||||
when(reduxHooks.useMasqueradeData).expectCalledWith();
|
||||
when(reduxHooks.useCardEnrollmentData).expectCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
@@ -153,14 +155,13 @@ describe('CourseCardMenu', () => {
|
||||
});
|
||||
describe('masquerading', () => {
|
||||
it('renders but unenroll is disabled', async () => {
|
||||
mockHooks({ ...hookProps, isMasquerading: true });
|
||||
renderComponent();
|
||||
mockHooks({ ...hookProps });
|
||||
renderComponent(true);
|
||||
|
||||
const user = userEvent.setup();
|
||||
const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
await user.click(dropdown);
|
||||
|
||||
const unenrollOption = screen.getByRole('button', { name: messages.unenroll.defaultMessage });
|
||||
expect(unenrollOption).toBeInTheDocument();
|
||||
expect(unenrollOption).toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
|
||||
const messages = defineMessages({
|
||||
unenroll: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user