Compare commits

..

5 Commits

Author SHA1 Message Date
nsprenkle
94dd9e1cdd test: update snapshots 2023-03-07 18:10:13 -05:00
nsprenkle
9f8249ef50 fix: update menu item link for variant header 2023-03-07 18:09:35 -05:00
nsprenkle
dab11db684 test: update snapshots 2023-03-07 17:29:52 -05:00
nsprenkle
315ca34121 fix: add environment variables for new URLs 2023-03-07 17:21:03 -05:00
nsprenkle
496a1c7de5 fix: update menu item link for account and profile 2023-03-07 17:14:18 -05:00
346 changed files with 44373 additions and 15799 deletions

9
.env
View File

@@ -2,7 +2,6 @@ NODE_ENV='production'
NODE_PATH=./src
BASE_URL=''
LMS_BASE_URL=''
ECOMMERCE_BASE_URL=''
LOGIN_URL=''
LOGOUT_URL=''
CSRF_TOKEN_API_PATH=''
@@ -36,9 +35,5 @@ ZENDESK_KEY=''
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
ACCOUNT_MICROFRONTEND_URL=''
PROFILE_MICROFRONTEND_URL=''

View File

@@ -2,12 +2,12 @@ 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
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.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'
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL=''
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'
@@ -42,9 +42,6 @@ ZENDESK_KEY=''
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false
// Microfrontends
ACCOUNT_MICROFRONTEND_URL='http://localhost:1997/'
PROFILE_MICROFRONTEND_URL='http://localhost:1995/u/'

View File

@@ -2,12 +2,12 @@ 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
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.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'
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL=''
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'
@@ -41,9 +41,6 @@ ZENDESK_KEY='test-zendesk-key'
HOTJAR_APP_ID='hot-jar-app-id'
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=true
ENABLE_PROGRAMS=false
// Microfrontends
ACCOUNT_MICROFRONTEND_URL='http://localhost:1997/'
PROFILE_MICROFRONTEND_URL='http://localhost:1995/u/'

View File

@@ -1,11 +1,10 @@
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/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': ['*'] } }],
},
});

9
.github/CODEOWNERS vendored
View File

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

View File

@@ -1,7 +0,0 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

33
.github/renovate.json vendored
View File

@@ -1,33 +0,0 @@
{
"extends": [
"config:base",
"schedule:weekly",
":automergeLinters",
":automergeMinor",
":automergeTesters",
":enableVulnerabilityAlerts",
":rebaseStalePrs",
":semanticCommits",
":updateNotScheduled"
],
"packageRules": [
{
"matchDepTypes": [
"devDependencies"
],
"matchUpdateTypes": [
"lockFileMaintenance",
"minor",
"patch",
"pin"
],
"automerge": true
},
{
"matchPackagePatterns": ["@edx", "@openedx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
],
"timezone": "America/New_York"
}

View File

@@ -10,17 +10,16 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
matrix:
node: [18, 20]
node: [16]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Setup Nodejs
uses: actions/setup-node@v4
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
@@ -40,10 +39,7 @@ jobs:
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
uses: codecov/codecov-action@v3
- name: Send failure notification
if: ${{ failure() }}
@@ -53,11 +49,9 @@ jobs:
server_port: 465
username: ${{ secrets.EDX_SMTP_USERNAME }}
password: ${{ secrets.EDX_SMTP_PASSWORD }}
subject: CI workflow failed in ${{github.repository}}
to: masters-grades@edx.org,aperture@2u-internal.opsgenie.net
subject: Upgrade python requirements workflow failed in ${{github.repository}}
to: masters-grades@edx.org
from: github-actions <github-actions@edx.org>
nodemailerlog: true
nodemailerdebug: true
body: CI workflow in ${{github.repository}} failed!
body: Upgrade python requirements workflow in ${{github.repository}} failed!
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
}}"

View File

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

View File

@@ -7,20 +7,17 @@ on:
jobs:
release:
name: Release
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VER }}
node-version: 12
- name: Install dependencies
run: npm ci

2
.gitignore vendored
View File

@@ -1,6 +1,5 @@
.DS_Store
.eslintcache
env.config.*
node_modules
npm-debug.log
coverage
@@ -26,4 +25,3 @@ module.config.js
### transifex ###
src/i18n/transifex_input.json
temp
src/i18n/messages

1
.nvmrc
View File

@@ -1 +0,0 @@
20

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-learner-dashboard]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -2,13 +2,17 @@ npm-install-%: ## install specified % npm package
npm install $* --save-dev
git add package.json
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_resource = frontend-app-learner-dashboard
transifex_langs = "ar,fr,es_419,zh_CN"
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-formatjs
transifex_temp = ./temp/babel-plugin-react-intl
NPM_TESTS=build i18n_extract lint test
@@ -40,17 +44,20 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull $(ATLAS_OPTIONS) \
translations/frontend-platform/src/i18n/messages:frontend-platform \
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
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments
# Pushing comments to Transifex...
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
$(intl_imports) frontend-platform paragon frontend-component-footer frontend-app-learner-dashboard
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
# This target is used by CI.
validate-no-uncommitted-package-lock-changes:

26
README.md Normal file
View File

@@ -0,0 +1,26 @@
# 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.
## Quickstart
To start the MFE and enable the feature in LMS:
1. Start the MFE with `npm run start`. Take a note of the path/port (defaults to `http://localhost:1996`).
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.
## Contributing
A core goal of this app is to provide a clean experimentation interface. To promote this end, we have provided a silo'ed code directory at `src/widgets` in which contributors should add their custom widget components. In order to ensure our ability to maintain the code stability of the app, the code for these widgets should be strictly contained within the bounds of that directory.
Once written, the widgets can be configured into one of our widget containers at `src/containers/WidgetContainers`. This can include conditional logic, as well as Optimizely triggers. It is important to note that our integration tests will isolate and ignore these containers, and thus testing your widget is the response of the creator/maintainer of the widget itself.
Some guidelines for writing widgets:
* Code for the widget should be strictly confined to the `src/widgets` directory.
* You can load data from the redux store, but should not add or modify fields in that structure.
* Network events should be managed in component hooks, though can use our `data/constants/requests:requestStates` for ease of tracking the request states.
## Resources
* [Learner Home project info at the Open edX Wiki](https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3575906333/Learner+Home)

View File

@@ -1,107 +0,0 @@
|license-badge| |status-badge| |ci-badge| |codecov-badge|
.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-app-learner-dashboard.svg
:target: https://github.com/openedx/frontend-app-learner-dashboard/blob/master/LICENSE
:alt: License
.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
:alt: Maintained
.. |ci-badge| image:: https://github.com/openedx/frontend-app-learner-dashboard/actions/workflows/ci.yml/badge.svg
:target: https://github.com/openedx/frontend-app-learner-dashboard/actions/workflows/ci.yml
:alt: Continuous Integration
.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-app-learner-dashboard/coverage.svg?branch=master
:target: https://app.codecov.io/github/openedx/frontend-app-learner-dashboard?branch=master
:alt: Codecov
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.
Quickstart
----------
To start the MFE and enable the feature in LMS:
1. Start the MFE with ``npm run start``. Take a note of the path/port (defaults to ``http://localhost:1996``).
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
-------
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
------------
A core goal of this app is to provide a clean experimentation interface. To promote this end, we have provided a
silo'ed code directory at ``src/widgets`` in which contributors should add their custom widget components. In order to
ensure our ability to maintain the code stability of the app, the code for these widgets should be strictly contained
within the bounds of that directory.
Once written, the widgets can be configured into one of our widget containers at ``src/containers/WidgetContainers``.
This can include conditional logic, as well as Optimizely triggers. It is important to note that our integration tests
will isolate and ignore these containers, and thus testing your widget is the response of the creator/maintainer of the
widget itself.
Some guidelines for writing widgets:
* Code for the widget should be strictly confined to the ``src/widgets`` directory.
* You can load data from the redux store, but should not add or modify fields in that structure.
* Network events should be managed in component hooks, though can use our ``data/constants/requests:requestStates`` for
ease of tracking the request states.
License
-------
The code in this repository is licensed under the AGPLv3 unless otherwise noted.
Please see the `license`_ for more info.
.. _license: https://github.com/openedx/frontend-app-learner-dashboard/blob/master/LICENSE
Getting Help
------------
If you're having trouble, we have discussion forums at https://discuss.openedx.org where you can connect with others in
the community.
Our real-time conversations are on Slack. You can request a `Slack invitation`_, then join our
`community Slack workspace`_. Because this is a frontend repository, the best place to discuss it would be in the
`#wg-frontend channel`_.
For anything non-trivial, the best path is to open an issue in this repository with as many details about the issue you
are facing as you can provide.
https://github.com/openedx/frontend-app-learner-dashboard/issues
For more information about these options, see the `Getting Help`_ page.
.. _Slack invitation: https://openedx.org/slack
.. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/community/connect
Resources
---------
Additional info about the Learner Home MFE project can be found on the `Open edX Wiki`_.
.. _Open edX Wiki: https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3575906333/Learner+Home
The Open edX Code of Conduct
----------------------------
All community members are expected to follow the `Open edX Code of Conduct`_.
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
Reporting Security Issues
-------------------------
Please do not report security issues in public. Please email security@openedx.org.

View File

@@ -1,24 +0,0 @@
# This file records information about this repo. Its use is described in OEP-55:
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'frontend-app-learner-dashboard'
description: 'The Microfrontend for the course listing interface allowing learners to view and act upon enrollments.'
links:
- url: 'https://github.com/openedx/frontend-app-learner-dashboard/blob/master/README.rst'
title: 'Documentation'
icon: 'Article'
annotations:
# (Optional) Annotation keys and values can be whatever you want.
# We use it in Open edX repos to have a comma-separated list of GitHub user
# names that might be interested in changes to the architecture of this
# component.
openedx.org/arch-interest-groups: ""
# This can be multiple comma-separated projects.
openedx.org/add-to-projects: "openedx:23"
spec:
type: 'service'
lifecycle: 'production'
owner: 2U-aperture
# (Optional) An array of different components or resources.

View File

@@ -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',
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',
ZENDESK_KEY: '',
HOTJAR_APP_ID: '',
HOTJAR_VERSION: 6,
HOTJAR_DEBUG: '',
NEW_RELIC_APP_ID: '',
NEW_RELIC_LICENSE_KEY: '',
ACCOUNT_SETTINGS_URL: 'http://localhost:1997',
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
ENABLE_NOTICES: '',
CAREER_LINK_URL: '',
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
};

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', {
setupFilesAfterEnv: [
@@ -6,6 +6,9 @@ module.exports = createConfig('jest', {
'<rootDir>/src/setupTest.jsx',
],
modulePaths: ['<rootDir>/src/'],
snapshotSerializers: [
'enzyme-to-json/serializer',
],
coveragePathIgnorePatterns: [
'src/segment.js',
'src/postcss.config.js',

49427
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,9 @@
"type": "git",
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
},
"browserslist": [
"extends @edx/browserslist-config"
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"semantic-release": "semantic-release",
@@ -19,7 +16,6 @@
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
"quality": "npm run lint-fix && npm run test",
"watch-tests": "jest --watch",
"snapshot": "fedx-scripts jest --updateSnapshot",
"prepare": "husky install"
},
"author": "edX",
@@ -29,71 +25,70 @@
"access": "public"
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-enterprise-hotjar": "3.0.0",
"@edx/frontend-platform": "8.1.2",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "3.0.0",
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-component-footer": "^11.4.1",
"@edx/frontend-enterprise-hotjar": "^1.2.0",
"@edx/frontend-platform": "^2.6.2",
"@edx/paragon": "20.19.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.2.0",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.2.2",
"@fortawesome/react-fontawesome": "^0.1.15",
"@redux-beacon/segment": "^1.1.0",
"@redux-devtools/extension": "3.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.21.4",
"classnames": "^2.3.1",
"core-js": "3.38.1",
"core-js": "3.16.2",
"dompurify": "^2.3.1",
"email-prop-type": "^3.0.1",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",
"file-saver": "^2.0.5",
"filesize": "^8.0.6",
"font-awesome": "4.7.0",
"history": "5.3.0",
"history": "5.0.1",
"html-react-parser": "^1.3.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-when": "^3.6.0",
"jest": "^26.6.3",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"prop-types": "15.7.2",
"query-string": "7.0.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-helmet": "^6.1.0",
"react-intl": "6.8.0",
"react-pdf": "^7.0.0",
"react-intl": "^5.20.9",
"react-pdf": "^5.5.0",
"react-redux": "^7.2.4",
"react-router-dom": "6.27.0",
"react-router-dom": "5.3.3",
"react-share": "^4.4.0",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux": "4.1.1",
"redux-beacon": "^2.1.0",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-thunk": "2.4.2",
"regenerator-runtime": "^0.14.0",
"redux-thunk": "2.3.0",
"regenerator-runtime": "^0.13.9",
"reselect": "^4.0.0",
"universal-cookie": "^4.0.4",
"util": "^0.12.4",
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@edx/browserslist-config": "^1.3.0",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "14.1.5",
"@edx/frontend-build": "11.0.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"copy-webpack-plugin": "^12.0.0",
"husky": "^9.0.0",
"axios-mock-adapter": "^1.20.0",
"enzyme-adapter-react-16": "^1.15.6",
"fetch-mock": "^9.11.0",
"husky": "^7.0.0",
"identity-obj-proxy": "^3.0.0",
"jest-expect-message": "^1.1.3",
"react-dev-utils": "^12.0.0",
"react-test-renderer": "^17.0.2",
"jest-expect-message": "^1.0.2",
"react-dev-utils": "^11.0.4",
"react-test-renderer": "^16.14.0",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.4",
"semantic-release": "^20.1.3"
"semantic-release": "^17.4.5"
}
}

View File

@@ -1,8 +1,10 @@
<!doctype html>
<html lang="en-us" dir="ltr">
<head>
<title>Learner Dashboard | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
</head>
<body>
<div id="root"></div>

View File

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

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -6,8 +7,8 @@ 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 '@openedx/frontend-slot-footer';
import { Alert } from '@openedx/paragon';
import Footer from '@edx/frontend-component-footer';
import { Alert } from '@edx/paragon';
import { RequestKeys } from 'data/constants/requests';
import store from 'data/store';
@@ -17,16 +18,15 @@ import {
} from 'data/redux';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import ZendeskFab from 'components/ZendeskFab';
import track from 'tracking';
import fakeData from 'data/services/lms/fakeData/courses';
import LearnerDashboardHeaderVariant from './containers/LearnerDashboardHeaderVariant';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
import './App.scss';
export const App = () => {
@@ -41,7 +41,7 @@ export const App = () => {
const loadData = reduxHooks.useLoadData();
React.useEffect(() => {
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') {
window.loadEmptyData = () => {
loadData({ ...fakeData.globalData, courses: [] });
};
@@ -59,12 +59,12 @@ export const App = () => {
window.actions = actions;
window.track = track;
}
if (getConfig().HOTJAR_APP_ID) {
if (process.env.HOTJAR_APP_ID) {
try {
initializeHotjar({
hotjarId: getConfig().HOTJAR_APP_ID,
hotjarVersion: getConfig().HOTJAR_VERSION,
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
hotjarId: process.env.HOTJAR_APP_ID,
hotjarVersion: process.env.HOTJAR_VERSION,
hotjarDebug: !!process.env.HOTJAR_DEBUG,
});
} catch (error) {
logError(error);
@@ -72,28 +72,24 @@ export const App = () => {
}
}, [authenticatedUser, loadData]);
return (
<>
<Router>
<Helmet>
<title>{formatMessage(messages.pageTitle)}</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
{hasNetworkFailure
? (
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (
<Dashboard />
)}
</main>
</AppWrapper>
<FooterSlot />
<LearnerDashboardHeaderVariant />
<main>
{hasNetworkFailure
? (
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
) : (<Dashboard />)}
</main>
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
<ZendeskFab />
</div>
</>
</Router>
);
};

View File

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

View File

@@ -1,24 +1,26 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Helmet } from 'react-helmet';
import { shallow } from '@edx/react-unit-test-utils';
import { ErrorPage } from '@edx/frontend-platform/react';
import { BrowserRouter as Router } from 'react-router-dom';
import Footer from '@edx/frontend-component-footer';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { Alert } from '@edx/paragon';
import { RequestKeys } from 'data/constants/requests';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import LearnerDashboardHeaderVariant from 'containers/LearnerDashboardHeaderVariant';
import { App } from './App';
import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
jest.mock('@edx/frontend-component-footer', () => 'Footer');
jest.mock('containers/Dashboard', () => 'Dashboard');
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
jest.mock('containers/LearnerDashboardHeaderVariant', () => 'LearnerDashboardHeaderVariant');
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
actions: 'redux.actions',
@@ -33,123 +35,76 @@ jest.mock('hooks', () => ({
}));
jest.mock('data/store', () => 'data/store');
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({})),
}));
const loadData = jest.fn();
reduxHooks.useLoadData.mockReturnValue(loadData);
const logo = 'fakeLogo.png';
let el;
const supportEmail = 'test-support-url';
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
describe('App router component', () => {
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
const { formatMessage } = useIntl();
describe('component', () => {
const runBasicTests = () => {
test('snapshot', () => { expect(el.snapshot).toMatchSnapshot(); });
test('snapshot', () => { expect(el).toMatchSnapshot(); });
it('displays title in helmet component', () => {
const control = el.instance
.findByType(Helmet)[0]
.findByType('title')[0];
expect(control.children[0].el).toEqual(formatMessage(messages.pageTitle));
expect(el.find(Helmet).find('title').text()).toEqual(useIntl().formatMessage(messages.pageTitle));
});
it('displays learner dashboard header', () => {
expect(el.instance.findByType(LearnerDashboardHeader).length).toEqual(1);
expect(el.find(LearnerDashboardHeaderVariant).length).toEqual(1);
});
it('wraps the header and main components in an AppWrapper widget container', () => {
const container = el.instance.findByType(AppWrapper)[0];
expect(container.children[0].type).toEqual('LearnerDashboardHeader');
expect(container.children[1].type).toEqual('main');
it('wraps the page in a browser router', () => {
expect(el.find(Router)).toMatchObject(el);
});
test('Footer logo drawn from env variable', () => {
expect(el.find(Footer).props().logo).toEqual(logo);
});
};
describe('no network failure', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({});
el = shallow(<App />);
});
runBasicTests();
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
});
});
describe('no network failure with optimizely url', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
el = shallow(<App />);
});
runBasicTests();
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
});
});
describe('no network failure with optimizely project id', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
el = shallow(<App />);
});
runBasicTests();
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
expect(el.find('main')).toMatchObject(shallow(
<main><Dashboard /></main>,
));
});
});
describe('initialize failure', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
getConfig.mockReturnValue({});
el = shallow(<App />);
});
runBasicTests();
it('loads error page', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const alert = main.children[0];
expect(alert.type).toEqual('Alert');
expect(alert.children.length).toEqual(1);
const errorPage = alert.children[0];
expect(errorPage.type).toEqual('ErrorPage');
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
expect(el.find('main')).toEqual(shallow(
<main>
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
</main>,
));
});
});
describe('refresh failure', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
getConfig.mockReturnValue({});
el = shallow(<App />);
});
runBasicTests();
it('loads error page', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const alert = main.children[0];
expect(alert.type).toEqual('Alert');
expect(alert.children.length).toEqual(1);
const errorPage = alert.children[0];
expect(errorPage.type).toEqual('ErrorPage');
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
expect(el.find('main')).toEqual(shallow(
<main>
<Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert>
</main>,
));
});
});
});

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App router component component initialize failure snapshot 1`] = `
<Fragment>
<BrowserRouter>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -9,31 +9,28 @@ exports[`App router component component initialize failure snapshot 1`] = `
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
</AppWrapper>
<FooterSlot />
<LearnerDashboardHeaderVariant />
<main>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
</div>
</Fragment>
</BrowserRouter>
`;
exports[`App router component component no network failure snapshot 1`] = `
<Fragment>
<BrowserRouter>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -41,77 +38,22 @@ exports[`App router component component no network failure snapshot 1`] = `
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
</div>
</Fragment>
`;
exports[`App router component component no network failure with optimizely project id snapshot 1`] = `
<Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
<LearnerDashboardHeaderVariant />
<main>
<Dashboard />
</main>
<Footer
logo="fakeLogo.png"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
`;
exports[`App router component component no network failure with optimizely url snapshot 1`] = `
<Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
</div>
</Fragment>
</BrowserRouter>
`;
exports[`App router component component refresh failure snapshot 1`] = `
<Fragment>
<BrowserRouter>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
@@ -119,25 +61,22 @@ exports[`App router component component refresh failure snapshot 1`] = `
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
</AppWrapper>
<FooterSlot />
<LearnerDashboardHeaderVariant />
<main>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
</div>
</Fragment>
</BrowserRouter>
`;

View File

@@ -1,40 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
<ErrorPage
message="test-error-message"
/>
<IntlProvider
locale="en"
>
<ErrorPage
message="test-error-message"
/>
</IntlProvider>
`;
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<AppProvider
store={
{
"redux": "store",
}
}
wrapWithRouter={true}
<IntlProvider
locale="en"
>
<NoticesWrapper>
<Routes>
<Route
element={
<PageWrap>
<App />
</PageWrap>
}
<AppProvider
store={
Object {
"redux": "store",
}
}
>
<Switch>
<PageRoute
path="/"
>
<App />
</PageRoute>
<Redirect
to="/"
/>
<Route
element={
<Navigate
replace={true}
to="/"
/>
}
path="*"
/>
</Routes>
</NoticesWrapper>
</AppProvider>
</Switch>
</AppProvider>
</IntlProvider>
`;

View File

@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@openedx/paragon';
import { Info } from '@openedx/paragon/icons';
import { Alert } from '@edx/paragon';
import { Info } from '@edx/paragon/icons';
export const Banner = ({
children, variant, icon, className,

View File

@@ -1,6 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { Alert } from '@openedx/paragon';
import { Alert } from '@edx/paragon';
import Banner from './Banner';
@@ -11,17 +11,17 @@ describe('Banner', () => {
describe('snapshot', () => {
test('renders default banner', () => {
const wrapper = shallow(<Banner {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
test('renders with variants', () => {
const wrapper = shallow(<Banner {...props} variant="success" />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
expect(wrapper.instance.findByType(Alert)[0].props.variant).toEqual('success');
expect(wrapper.find(Alert).prop('variant')).toEqual('success');
});
test('renders with custom class', () => {
const wrapper = shallow(<Banner {...props} className="custom-class" />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { MailtoLink } from '@edx/paragon';
export const EmailLink = ({ address }) => {
if (!address) {
return null;
}
return (
<MailtoLink to={address}>{address}</MailtoLink>
);
};
EmailLink.defaultProps = { address: null };
EmailLink.propTypes = { address: PropTypes.string };
export default EmailLink;

View File

@@ -0,0 +1,16 @@
import { shallow } from 'enzyme';
import EmailLink from './EmailLink';
describe('EmailLink', () => {
it('renders null when no address is provided', () => {
const wrapper = shallow(<EmailLink />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toEqual(true);
});
it('renders a MailtoLink when an address is provided', () => {
const wrapper = shallow(<EmailLink address="test@email.com" />);
expect(wrapper.find('MailtoLink').length).toEqual(1);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,26 +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 error404Message = 'This probably happened because the notices plugin is not installed on platform.';
export const getNotices = ({ onLoad }) => {
const authenticatedUser = getAuthenticatedUser();
const handleError = async (e) => {
// Error probably means that notices is not installed, which is fine.
const { customAttributes: { httpErrorStatus } } = e;
if (httpErrorStatus === 404) {
logInfo(`${e}. ${error404Message}`);
} else {
logError(e);
}
};
if (authenticatedUser) {
return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
}
return null;
};
export default { getNotices };

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +0,0 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import useNoticesWrapperData from './hooks';
import NoticesWrapper from '.';
jest.mock('./hooks', () => jest.fn());
const hookProps = { isRedirected: false };
useNoticesWrapperData.mockReturnValue(hookProps);
let el;
const children = [<b key={1}>some</b>, <i key={2}>children</i>];
describe('NoticesWrapper component', () => {
describe('behavior', () => {
it('initializes hooks', () => {
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
expect(useNoticesWrapperData).toHaveBeenCalledWith();
});
});
describe('output', () => {
it('does not show children if redirected', () => {
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
expect(el.instance.children.length).toEqual(0);
});
it('shows children if not redirected', () => {
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
expect(el.instance.children.length).toEqual(2);
expect(el.instance.children[0].type).toEqual(shallow(children[0]).type);
expect(el.instance.props).toEqual(shallow(children[0]).props);
expect(el.instance.children[1].type).toEqual(shallow(children[1]).type);
expect(el.instance.props).toEqual(shallow(children[1]).props);
});
});
});

View File

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

View File

@@ -16,9 +16,6 @@ const ZendeskFab = () => {
},
chat: {
suppress: false,
departments: {
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
},
},
contactForm: {
ticketForms: [

View File

@@ -1,4 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import ZendeskFab from '.';
@@ -7,6 +7,6 @@ jest.mock('react-zendesk', () => 'Zendesk');
describe('ZendeskFab', () => {
test('snapshot', () => {
const wrapper = shallow(<ZendeskFab />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,6 +1,6 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
const messages = defineMessages({
export const messages = StrictDict({
supportTitle: {
id: 'zendesk.supportTitle',
description: 'Title for the support button',

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmailLink renders a MailtoLink when an address is provided 1`] = `
<MailtoLink
to="test@email.com"
>
test@email.com
</MailtoLink>
`;
exports[`EmailLink renders null when no address is provided 1`] = `""`;

View File

@@ -1,7 +1,6 @@
const configuration = {
// BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
// LOGIN_URL: process.env.LOGIN_URL,
// LOGOUT_URL: process.env.LOGOUT_URL,
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
@@ -14,12 +13,9 @@ const configuration = {
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
ZENDESK_KEY: process.env.ZENDESK_KEY,
SUPPORT_URL: process.env.SUPPORT_URL || null,
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
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',
// Microfrontends
ACCOUNT_MICROFRONTEND_URL: process.env.ACCOUNT_MICROFRONTEND_URL,
PROFILE_MICROFRONTEND_URL: process.env.PROFILE_MICROFRONTEND_URL,
};
const features = {};

View File

@@ -1,4 +1,4 @@
@import "@openedx/paragon/scss/core/core";
@import "@edx/paragon/scss/core/core";
.course-card {
.card {

View File

@@ -1,4 +1,4 @@
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { useWindowSize, breakpoints } from '@edx/paragon';
export const useIsCollapsed = () => {
const { width } = useWindowSize();

View File

@@ -1,4 +1,4 @@
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { useWindowSize, breakpoints } from '@edx/paragon';
import useIsCollapsed from './hooks';
describe('useIsCollapsed', () => {

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Button } from '@openedx/paragon';
import { Button } from '@edx/paragon';
import useIsCollapsed from './hooks';

View File

@@ -1,4 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import ActionButton from '.';
@@ -14,12 +14,12 @@ describe('ActionButton', () => {
test('is collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<ActionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
test('is not collapsed', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<ActionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -5,24 +5,22 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton';
import messages from './messages';
export const BeginCourseButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
const { disableBeginCourse } = useActionDisabledState(cardId);
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const handleClick = reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
cardId,
homeUrl + execEdTrackingParam,
homeUrl,
);
return (
<ActionButton
disabled={disableBeginCourse}
disabled={isMasquerading || !hasAccess}
as="a"
href="#"
onClick={handleClick}

View File

@@ -1,8 +1,8 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { htmlProps } from 'data/constants/htmlKeys';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import useActionDisabledState from '../hooks';
import BeginCourseButton from './BeginCourseButton';
jest.mock('tracking', () => ({
@@ -13,22 +13,19 @@ jest.mock('tracking', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardExecEdTrackingParam: jest.fn(),
useTrackCourseEvent: jest.fn(),
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'home-url' })),
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
useTrackCourseEvent: jest.fn(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper;
const homeUrl = 'home-url';
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
reduxHooks.useTrackCourseEvent.mockImplementation(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
);
const { homeUrl } = reduxHooks.useCardCourseRunData();
describe('BeginCourseButton', () => {
const props = {
@@ -37,49 +34,37 @@ describe('BeginCourseButton', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('snapshot', () => {
test('renders default button when learner has access to the course', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
props.cardId,
homeUrl,
));
});
});
describe('behavior', () => {
it('initializes course run data with cardId', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
});
it('loads exec education path param', () => {
it('initializes enrollment data with cardId', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
});
it('loads disabled states for begin action from action hooks', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
});
});
describe('snapshot', () => {
describe('disabled', () => {
beforeEach(() => {
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
describe('disabled states', () => {
test('learner does not have access', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
wrapper = shallow(<BeginCourseButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be disabled', () => {
expect(wrapper.instance.props.disabled).toEqual(true);
});
});
describe('enabled', () => {
beforeEach(() => {
test('masquerading', () => {
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be enabled', () => {
expect(wrapper.instance.props.disabled).toEqual(false);
});
it('should track enter course clicked event on click, with exec ed param', () => {
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
props.cardId,
homeUrl + execEdPath(props.cardId),
));
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
});
});

View File

@@ -5,24 +5,22 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton';
import messages from './messages';
export const ResumeButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
const { disableResumeCourse } = useActionDisabledState(cardId);
const { hasAccess, isAudit, isAuditAccessExpired } = reduxHooks.useCardEnrollmentData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { formatMessage } = useIntl();
const handleClick = reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
cardId,
resumeUrl + execEdTrackingParam,
resumeUrl,
);
return (
<ActionButton
disabled={disableResumeCourse}
disabled={isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired)}
as="a"
href="#"
onClick={handleClick}

View File

@@ -1,83 +1,82 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { htmlProps } from 'data/constants/htmlKeys';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import useActionDisabledState from '../hooks';
import ResumeButton from './ResumeButton';
jest.mock('tracking', () => ({
course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
},
}));
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardExecEdTrackingParam: jest.fn(),
useTrackCourseEvent: jest.fn(),
useCardCourseRunData: jest.fn(() => ({ resumeUrl: 'resumeUrl' })),
useCardEnrollmentData: jest.fn(() => ({
hasAccess: true,
isAudit: true,
isAuditAccessExpired: false,
})),
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
useTrackCourseEvent: (eventName, cardId, url) => jest
.fn()
.mockName(`useTrackCourseEvent('${eventName}', '${cardId}', '${url}')`),
},
}));
jest.mock('tracking', () => ({
course: {
enterCourseClicked: 'enterCourseClicked',
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
const resumeUrl = 'resume-url';
reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
reduxHooks.useTrackCourseEvent.mockImplementation(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
);
let wrapper;
const { resumeUrl } = reduxHooks.useCardCourseRunData();
describe('ResumeButton', () => {
const props = {
cardId: 'cardId',
};
describe('behavior', () => {
it('initializes course run data with cardId', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
});
it('loads exec education path param', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
});
it('loads disabled states for resume action from action hooks', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
describe('snapshot', () => {
test('renders default button when learner has access to the course', () => {
const wrapper = shallow(<ResumeButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.prop(htmlProps.onClick).getMockName()).toContain(
'useTrackCourseEvent',
track.course.enterCourseClicked,
props.cardId,
resumeUrl,
);
});
});
describe('snapshot', () => {
describe('disabled', () => {
beforeEach(() => {
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be disabled', () => {
expect(wrapper.instance.props.disabled).toEqual(true);
});
describe('behavior', () => {
it('initializes course run data based on cardId', () => {
shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
});
describe('enabled', () => {
beforeEach(() => {
wrapper = shallow(<ResumeButton {...props} />);
it('initializes course enrollment data based on cardId', () => {
shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
});
describe('disabled states', () => {
test('masquerading', () => {
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
const wrapper = shallow(<ResumeButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
test('learner does not have access', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
hasAccess: false,
isAudit: true,
isAuditAccessExpired: false,
});
const wrapper = shallow(<ResumeButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
it('should be enabled', () => {
expect(wrapper.instance.props.disabled).toEqual(false);
});
it('should track enter course clicked event on click, with exec ed param', () => {
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
props.cardId,
resumeUrl + execEdPath(props.cardId),
));
test('audit access expired', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
hasAccess: true,
isAudit: true,
isAuditAccessExpired: true,
});
const wrapper = shallow(<ResumeButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
});
});

View File

@@ -4,17 +4,18 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton';
import messages from './messages';
export const SelectSessionButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const { disableSelectSession } = useActionDisabledState(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
const { canChange, hasSessions } = reduxHooks.useCardEntitlementData(cardId);
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
return (
<ActionButton
disabled={disableSelectSession}
disabled={isMasquerading || !hasAccess || (!canChange || !hasSessions)}
onClick={openSessionModal}
>
{formatMessage(messages.selectSession)}

View File

@@ -1,34 +1,67 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import { htmlProps } from 'data/constants/htmlKeys';
import SelectSessionButton from './SelectSessionButton';
jest.mock('hooks', () => ({
reduxHooks: {
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
useCardEntitlementData: jest.fn(() => ({ canChange: true, hasSessions: true })),
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
useUpdateSelectSessionModalCallback: () => jest.fn().mockName('mockOpenSessionModal'),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false })));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper;
describe('SelectSessionButton', () => {
const props = { cardId: 'cardId' };
it('default render', () => {
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(false);
expect(wrapper.instance.props.onClick.getMockName()).toEqual(
reduxHooks.useUpdateSelectSessionModalCallback().getMockName(),
);
describe('snapshot', () => {
test('renders default button', () => {
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
it('renders disabled button when user does not have access to the course', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
it('renders disabled button if masquerading', () => {
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
test('disabled states', () => {
useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
describe('behavior', () => {
it('default render', () => {
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.prop(htmlProps.onClick).getMockName())
.toEqual(reduxHooks.useUpdateSelectSessionModalCallback().getMockName());
});
describe('disabled states', () => {
test('learner does not have access', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('learner cannot change sessions', () => {
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ canChange: false, hasSessions: true });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('entitlement does not have available sessions', () => {
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ canChange: true, hasSessions: false });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('user is masquerading', () => {
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
});
});
});

View File

@@ -1,12 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Locked } from '@openedx/paragon/icons';
import { Locked } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton';
import messages from './messages';
@@ -15,14 +14,15 @@ export const UpgradeButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { disableUpgradeCourse } = useActionDisabledState(cardId);
const { canUpgrade } = reduxHooks.useCardEnrollmentData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
track.course.upgradeClicked,
cardId,
upgradeUrl,
);
const isEnabled = (!isMasquerading && canUpgrade);
const enabledProps = {
as: 'a',
href: upgradeUrl,
@@ -32,8 +32,8 @@ export const UpgradeButton = ({ cardId }) => {
<ActionButton
iconBefore={Locked}
variant="outline-primary"
disabled={disableUpgradeCourse}
{...!disableUpgradeCourse && enabledProps}
disabled={!isEnabled}
{...isEnabled && enabledProps}
>
{formatMessage(messages.upgrade)}
</ActionButton>

View File

@@ -1,8 +1,8 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import { htmlProps } from 'data/constants/htmlKeys';
import UpgradeButton from './UpgradeButton';
jest.mock('tracking', () => ({
@@ -13,13 +13,15 @@ jest.mock('tracking', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })),
useTrackCourseEvent: jest.fn(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableUpgradeCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
describe('UpgradeButton', () => {
@@ -31,19 +33,25 @@ describe('UpgradeButton', () => {
describe('snapshot', () => {
test('can upgrade', () => {
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(false);
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
track.course.upgradeClicked,
props.cardId,
upgradeUrl,
));
});
test('cannot upgrade', () => {
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false });
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('masquerading', () => {
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
});
});

View File

@@ -5,23 +5,23 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton';
import messages from './messages';
export const ViewCourseButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { disableViewCourse } = useActionDisabledState(cardId);
const { hasAccess, isAudit, isAuditAccessExpired } = reduxHooks.useCardEnrollmentData(cardId);
const handleClick = reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
cardId,
homeUrl,
);
// disabled on no access or (is audit track but audit access was expired)
const disabledViewCourseButton = !hasAccess || (isAudit && isAuditAccessExpired);
return (
<ActionButton
disabled={disableViewCourse}
disabled={disabledViewCourseButton}
as="a"
href="#"
onClick={handleClick}

View File

@@ -1,8 +1,8 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import track from 'tracking';
import { htmlProps } from 'data/constants/htmlKeys';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ViewCourseButton from './ViewCourseButton';
jest.mock('tracking', () => ({
@@ -13,33 +13,73 @@ jest.mock('tracking', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardEntitlementData: jest.fn(),
useTrackCourseEvent: jest.fn(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper;
const defaultProps = { cardId: 'cardId' };
const homeUrl = 'homeUrl';
const createWrapper = ({
hasAccess = false,
isAudit = false,
isAuditAccessExpired = false,
isEntitlement = false,
isExpired = false,
propsOveride = {},
}) => {
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess, isAudit, isAuditAccessExpired });
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
return shallow(<ViewCourseButton {...defaultProps} {...propsOveride} />);
};
describe('ViewCourseButton', () => {
test('learner can view course', () => {
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
defaultProps.cardId,
homeUrl,
));
expect(wrapper.instance.props.disabled).toEqual(false);
describe('learner has access to course', () => {
beforeEach(() => {
wrapper = createWrapper({ hasAccess: true });
});
test('snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
test('links to home URL', () => {
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
defaultProps.cardId,
homeUrl,
));
});
test('link is enabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
});
test('link is disabled when audit access is expired', () => {
wrapper = createWrapper({ hasAccess: true, isAudit: true, isAuditAccessExpired: true });
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
});
test('learner cannot view course', () => {
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
describe('learner does not have access to course', () => {
beforeEach(() => {
wrapper = createWrapper({ hasAccess: false });
});
test('snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
test('links to home URL', () => {
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
defaultProps.cardId,
homeUrl,
));
});
test('link is disabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
});
});

View File

@@ -1,35 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
},
}
}
>
Begin Course
</ActionButton>
`;
exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
exports[`BeginCourseButton snapshot renders default button when learner has access to the course 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
"upgradeUrl": "home-url",
},
}
}

View File

@@ -1,38 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResumeButton snapshot disabled snapshot 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
},
}
}
>
Resume
</ActionButton>
`;
exports[`ResumeButton snapshot enabled snapshot 1`] = `
exports[`ResumeButton snapshot renders default button when learner has access to the course 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
},
}
}
onClick={[MockFunction useTrackCourseEvent('enterCourseClicked', 'cardId', 'resumeUrl')]}
>
Resume
</ActionButton>

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectSessionButton default render 1`] = `
exports[`SelectSessionButton snapshot renders default button 1`] = `
<ActionButton
disabled={false}
onClick={[MockFunction mockOpenSessionModal]}
@@ -9,7 +9,16 @@ exports[`SelectSessionButton default render 1`] = `
</ActionButton>
`;
exports[`SelectSessionButton disabled states 1`] = `
exports[`SelectSessionButton snapshot renders disabled button if masquerading 1`] = `
<ActionButton
disabled={true}
onClick={[MockFunction mockOpenSessionModal]}
>
Select Session
</ActionButton>
`;
exports[`SelectSessionButton snapshot renders disabled button when user does not have access to the course 1`] = `
<ActionButton
disabled={true}
onClick={[MockFunction mockOpenSessionModal]}

View File

@@ -7,8 +7,8 @@ exports[`UpgradeButton snapshot can upgrade 1`] = `
href="upgradeUrl"
iconBefore={[MockFunction icons.Locked]}
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.trackUpgradeClicked],
"upgradeUrl": "upgradeUrl",
@@ -30,3 +30,13 @@ exports[`UpgradeButton snapshot cannot upgrade 1`] = `
Upgrade
</ActionButton>
`;
exports[`UpgradeButton snapshot masquerading 1`] = `
<ActionButton
disabled={true}
iconBefore={[MockFunction icons.Locked]}
variant="outline-primary"
>
Upgrade
</ActionButton>
`;

View File

@@ -1,32 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ViewCourseButton learner can view course 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",
},
}
}
>
View Course
</ActionButton>
`;
exports[`ViewCourseButton learner cannot view course 1`] = `
exports[`ViewCourseButton learner does not have access to course snapshot 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
{
"trackCourseEvent": {
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",
},
}
}
>
View Course
</ActionButton>
`;
exports[`ViewCourseButton learner has access to course snapshot 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",

View File

@@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardActions snapshot show begin course button when verified and not entitlement and has started 1`] = `
<ActionRow
data-test-id="CourseCardActions"
>
<BeginCourseButton
cardId="cardId"
/>
</ActionRow>
`;
exports[`CourseCardActions snapshot show resume button when verified and not entitlement and has started 1`] = `
<ActionRow
data-test-id="CourseCardActions"
>
<ResumeButton
cardId="cardId"
/>
</ActionRow>
`;
exports[`CourseCardActions snapshot show select session button when not verified and entitlement 1`] = `
<ActionRow
data-test-id="CourseCardActions"
>
<SelectSessionButton
cardId="cardId"
/>
</ActionRow>
`;
exports[`CourseCardActions snapshot show upgrade button when not verified and not entitlement 1`] = `
<ActionRow
data-test-id="CourseCardActions"
>
<UpgradeButton
cardId="cardId"
/>
<BeginCourseButton
cardId="cardId"
/>
</ActionRow>
`;
exports[`CourseCardActions snapshot show view course button when not verified and entitlement and fulfilled 1`] = `
<ActionRow
data-test-id="CourseCardActions"
>
<ViewCourseButton
cardId="cardId"
/>
</ActionRow>
`;

View File

@@ -1,11 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ActionRow } from '@openedx/paragon';
import { ActionRow } from '@edx/paragon';
import { reduxHooks } from 'hooks';
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
import UpgradeButton from './UpgradeButton';
import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton';
import ResumeButton from './ResumeButton';
@@ -13,25 +13,21 @@ import ViewCourseButton from './ViewCourseButton';
export const CourseCardActions = ({ cardId }) => {
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
const {
hasStarted,
} = reduxHooks.useCardEnrollmentData(cardId);
const { isVerified, hasStarted } = reduxHooks.useCardEnrollmentData(cardId);
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
let PrimaryButton;
if (isEntitlement) {
PrimaryButton = isFulfilled ? ViewCourseButton : SelectSessionButton;
} else if (isArchived) {
PrimaryButton = ViewCourseButton;
} else {
PrimaryButton = hasStarted ? ResumeButton : BeginCourseButton;
}
return (
<ActionRow data-test-id="CourseCardActions">
<CourseCardActionSlot cardId={cardId} />
{isEntitlement && (isFulfilled
? <ViewCourseButton cardId={cardId} />
: <SelectSessionButton cardId={cardId} />
)}
{(isArchived && !isEntitlement) && (
<ViewCourseButton cardId={cardId} />
)}
{!(isArchived || isEntitlement) && (hasStarted
? <ResumeButton cardId={cardId} />
: <BeginCourseButton cardId={cardId} />
)}
{!(isEntitlement || isVerified) && <UpgradeButton cardId={cardId} />}
<PrimaryButton cardId={cardId} />
</ActionRow>
);
};

View File

@@ -1,14 +1,7 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { reduxHooks } from 'hooks';
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
import UpgradeButton from './UpgradeButton';
import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton';
import ResumeButton from './ResumeButton';
import ViewCourseButton from './ViewCourseButton';
import CourseCardActions from '.';
jest.mock('hooks', () => ({
@@ -16,103 +9,96 @@ jest.mock('hooks', () => ({
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardEntitlementData: jest.fn(),
useMasqueradeData: jest.fn(),
},
}));
jest.mock('plugin-slots/CourseCardActionSlot', () => 'CustomActionButton');
jest.mock('./UpgradeButton', () => 'UpgradeButton');
jest.mock('./SelectSessionButton', () => 'SelectSessionButton');
jest.mock('./ViewCourseButton', () => 'ViewCourseButton');
jest.mock('./BeginCourseButton', () => 'BeginCourseButton');
jest.mock('./ResumeButton', () => 'ResumeButton');
const cardId = 'test-card-id';
const props = { cardId };
let el;
describe('CourseCardActions', () => {
const mockHooks = ({
isEntitlement = false,
isExecEd2UCourse = false,
isFulfilled = false,
isArchived = false,
isVerified = false,
hasStarted = false,
isMasquerading = false,
} = {}) => {
const props = {
cardId: 'cardId',
};
const createWrapper = ({
isEntitlement, isFulfilled, isArchived, isVerified, hasStarted,
}) => {
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted });
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isVerified, hasStarted });
return shallow(<CourseCardActions {...props} />);
};
const render = () => {
el = shallow(<CourseCardActions {...props} />);
};
describe('behavior', () => {
it('initializes redux hooks', () => {
mockHooks();
render();
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
describe('snapshot', () => {
test('show upgrade button when not verified and not entitlement', () => {
const wrapper = createWrapper({
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
});
expect(wrapper).toMatchSnapshot();
});
test('show select session button when not verified and entitlement', () => {
const wrapper = createWrapper({
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
});
expect(wrapper).toMatchSnapshot();
});
test('show begin course button when verified and not entitlement and has started', () => {
const wrapper = createWrapper({
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: false,
});
expect(wrapper).toMatchSnapshot();
});
test('show resume button when verified and not entitlement and has started', () => {
const wrapper = createWrapper({
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: true,
});
expect(wrapper).toMatchSnapshot();
});
test('show view course button when not verified and entitlement and fulfilled', () => {
const wrapper = createWrapper({
isEntitlement: true, isFulfilled: true, isArchived: false, isVerified: false, hasStarted: false,
});
expect(wrapper).toMatchSnapshot();
});
});
describe('output', () => {
describe('Exec Ed course', () => {
it('does not render upgrade button', () => {
mockHooks({ isExecEd2UCourse: true });
render();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
describe('behavior', () => {
it('show upgrade button when not verified and not entitlement', () => {
const wrapper = createWrapper({
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
});
expect(wrapper.find('UpgradeButton')).toHaveLength(1);
});
describe('entitlement course', () => {
it('does not render upgrade button', () => {
mockHooks({ isEntitlement: true });
render();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
});
it('renders ViewCourseButton if fulfilled', () => {
mockHooks({ isEntitlement: true, isFulfilled: true });
render();
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
});
it('renders SelectSessionButton if not fulfilled', () => {
mockHooks({ isEntitlement: true });
render();
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId);
it('show select session button when not verified and entitlement', () => {
const wrapper = createWrapper({
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
});
expect(wrapper.find('SelectSessionButton')).toHaveLength(1);
});
describe('verified course', () => {
it('does not render upgrade button', () => {
mockHooks({ isVerified: true });
render();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
it('show begin course button when verified and not entitlement and has started', () => {
const wrapper = createWrapper({
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: false,
});
expect(wrapper.find('BeginCourseButton')).toHaveLength(1);
});
describe('not entitlement, verified, or exec ed', () => {
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
mockHooks({ isArchived: true });
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
it('show resume button when verified and not entitlement and has started', () => {
const wrapper = createWrapper({
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: true,
});
describe('unstarted courses', () => {
it('renders UpgradeButton and BeginCourseButton', () => {
mockHooks();
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId);
});
expect(wrapper.find('ResumeButton')).toHaveLength(1);
});
it('show view course button when not verified and entitlement and fulfilled', () => {
const wrapper = createWrapper({
isEntitlement: true, isFulfilled: true, isArchived: false, isVerified: false, hasStarted: false,
});
describe('active courses (started, and not archived)', () => {
it('renders UpgradeButton and ResumeButton', () => {
mockHooks({ hasStarted: true });
render();
expect(el.instance.findByType(CourseCardActionSlot)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(ResumeButton)[0].props.cardId).toEqual(cardId);
});
expect(wrapper.find('ViewCourseButton')).toHaveLength(1);
});
it('show view course button when not verified and entitlement and fulfilled and archived', () => {
const wrapper = createWrapper({
isEntitlement: true, isFulfilled: true, isArchived: true, isVerified: false, hasStarted: false,
});
expect(wrapper.find('ViewCourseButton')).toHaveLength(1);
});
});
});

View File

@@ -1,6 +1,6 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
const messages = defineMessages({
export const messages = StrictDict({
upgrade: {
id: 'learner-dash.courseCard.actions.upgrade',
description: 'Course card upgrade button text',

View File

@@ -2,8 +2,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { MailtoLink, Hyperlink } from '@openedx/paragon';
import { CheckCircle } from '@openedx/paragon/icons';
import { MailtoLink, Hyperlink } from '@edx/paragon';
import { CheckCircle } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from 'hooks';
@@ -26,28 +26,16 @@ export const CertificateBanner = ({ cardId }) => {
const { formatMessage } = useIntl();
const formatDate = useFormatDate();
const emailLink = address => <MailtoLink to={address}>{address}</MailtoLink>;
const emailLink = address => address && <MailtoLink to={address}>{address}</MailtoLink>;
if (certificate.isRestricted) {
return (
<Banner variant="danger">
{ supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
{formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) })}
{isVerified && ' '}
{isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))}
</Banner>
);
}
if (certificate.isDownloadable) {
return (
<Banner variant="success" icon={CheckCircle}>
{formatMessage(messages.certReady)}
{certificate.certPreviewUrl && (
<>
{' '}
<Hyperlink isInline destination={certificate.certPreviewUrl}>
{formatMessage(messages.viewCertificate)}
</Hyperlink>
</>
{isVerified && formatMessage(
messages.certRefundContactBilling,
{ billingEmail: emailLink(billingEmail) },
)}
</Banner>
);
@@ -75,6 +63,17 @@ export const CertificateBanner = ({ cardId }) => {
</Banner>
);
}
if (certificate.isDownloadable) {
return (
<Banner variant="success" icon={CheckCircle}>
{formatMessage(messages.certReady)}
{' '}
<Hyperlink isInline destination={certificate.certPreviewUrl}>
{formatMessage(messages.viewCertificate)}
</Hyperlink>
</Banner>
);
}
if (certificate.isEarnedButUnavailable) {
return (
<Banner>

View File

@@ -1,4 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { reduxHooks } from 'hooks';
import CertificateBanner from './CertificateBanner';
@@ -21,6 +21,10 @@ jest.mock('components/Banner', () => 'Banner');
describe('CertificateBanner', () => {
const props = { cardId: 'cardId' };
reduxHooks.usePlatformSettingsData.mockReturnValue({
supportEmail: 'suport@email',
billingEmail: 'billing@email',
});
reduxHooks.useCardCourseRunData.mockReturnValue({
minPassingGrade: 0.8,
progressUrl: 'progressUrl',
@@ -38,22 +42,18 @@ describe('CertificateBanner', () => {
};
const defaultCourseRun = { isArchived: false };
const defaultGrade = { isPassing: false };
const defaultPlatformSettings = {};
const createWrapper = ({
certificate = {},
enrollment = {},
grade = {},
courseRun = {},
platformSettings = {},
}) => {
reduxHooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
reduxHooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings });
return shallow(<CertificateBanner {...props} />);
};
/** TODO: Update tests to validate snapshots **/
describe('snapshot', () => {
test('is restricted', () => {
const wrapper = createWrapper({
@@ -61,29 +61,7 @@ describe('CertificateBanner', () => {
isRestricted: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted with support email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail: 'suport@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted with billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
test('is restricted and verified', () => {
const wrapper = createWrapper({
@@ -94,64 +72,7 @@ describe('CertificateBanner', () => {
isVerified: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with support email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with support and billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: true },
certificate: { isDownloadable: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: false },
certificate: { isDownloadable: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
test('not passing and audit', () => {
const wrapper = createWrapper({
@@ -159,17 +80,28 @@ describe('CertificateBanner', () => {
isAudit: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
test('not passing and has finished', () => {
const wrapper = createWrapper({
courseRun: { isArchived: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
test('not passing and not audit and not finished', () => {
const wrapper = createWrapper({});
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
test('is passing and is downloadable', () => {
const wrapper = createWrapper({
grade: {
isPassing: true,
},
certificate: {
isDownloadable: true,
},
});
expect(wrapper).toMatchSnapshot();
});
test('is passing and is earned but unavailable', () => {
const wrapper = createWrapper({
@@ -180,7 +112,7 @@ describe('CertificateBanner', () => {
isEarnedButUnavailable: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
test('is passing and not downloadable render empty', () => {
const wrapper = createWrapper({
@@ -188,7 +120,7 @@ describe('CertificateBanner', () => {
isPassing: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
});
describe('behavior', () => {
@@ -197,12 +129,8 @@ describe('CertificateBanner', () => {
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
const bannerMessage = wrapper.instance.findByType('format-message-function').map(el => el.props.message.defaultMessage).join('\n');
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
expect(bannerMessage).toEqual(messages.certRestricted.defaultMessage);
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
});
@@ -214,12 +142,8 @@ describe('CertificateBanner', () => {
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
const bannerMessage = wrapper.instance.findByType('format-message-function').map(el => el.props.message.defaultMessage).join('\n');
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
expect(bannerMessage).toContain(messages.certRefundContactBilling.defaultMessage);
});

View File

@@ -1,7 +1,7 @@
/* eslint-disable max-len */
import React from 'react';
import PropTypes from 'prop-types';
import { Hyperlink } from '@openedx/paragon';
import { Hyperlink } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from 'hooks';
@@ -36,9 +36,11 @@ export const CourseBanner = ({ cardId }) => {
<Banner>
{formatMessage(messages.auditAccessExpired)}
{' '}
<Hyperlink isInline destination="">
{formatMessage(messages.findAnotherCourse)}
</Hyperlink>
{
<Hyperlink isInline destination="">
{formatMessage(messages.findAnotherCourse)}
</Hyperlink>
}
</Banner>
))}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Hyperlink } from '@openedx/paragon';
import { shallow } from 'enzyme';
import { Hyperlink } from '@edx/paragon';
import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils';
@@ -70,11 +70,11 @@ describe('CourseBanner', () => {
render({ enrollment: { isAuditAccessExpired: true, canUpgrade: true } });
});
test('snapshot: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.instance.children[0].children[2].el).toContain(messages.upgradeToAccess.defaultMessage);
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.text()).toContain(messages.upgradeToAccess.defaultMessage);
});
});
describe('audit access expired, cannot upgrade', () => {
@@ -82,11 +82,11 @@ describe('CourseBanner', () => {
render({ enrollment: { isAuditAccessExpired: true } });
});
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.instance.findByType(Hyperlink)[0].children[0].el).toEqual(messages.findAnotherCourse.defaultMessage);
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.find(Hyperlink).text()).toEqual(messages.findAnotherCourse.defaultMessage);
});
});
describe('course run active and cannot upgrade', () => {
@@ -94,31 +94,31 @@ describe('CourseBanner', () => {
render({ courseRun: { isActive: true } });
});
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('messages: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.upgradeDeadlinePassed.defaultMessage);
const link = el.instance.findByType(Hyperlink);
expect(link[0].children[0].el).toEqual(messages.exploreCourseDetails.defaultMessage);
expect(link[0].props.destination).toEqual(courseRunData.marketingUrl);
expect(el.text()).toContain(messages.upgradeDeadlinePassed.defaultMessage);
const link = el.find(Hyperlink);
expect(link.text()).toEqual(messages.exploreCourseDetails.defaultMessage);
expect(link.props().destination).toEqual(courseRunData.marketingUrl);
});
});
test('no display if audit access not expired and (course is not active or can upgrade)', () => {
render();
// isEmptyRender() isn't true because the minimal is <Fragment />
expect(el.instance.children).toEqual([]);
expect(el.html()).toEqual('');
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
expect(el.instance.children).toEqual([]);
expect(el.html()).toEqual('');
});
describe('unmet prerequisites', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
});
test('snapshot: unmetPrerequisites', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('messages: prerequisitesNotMet', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.prerequisitesNotMet.defaultMessage);
expect(el.text()).toContain(messages.prerequisitesNotMet.defaultMessage);
});
});
describe('too early', () => {
@@ -126,17 +126,17 @@ describe('CourseBanner', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } }, courseRun: { startDate: null } });
});
test('snapshot', () => expect(el.snapshot).toMatchSnapshot());
test('messages', () => expect(el.instance.children).toEqual([]));
test('snapshot', () => expect(el).toMatchSnapshot());
test('messages', () => expect(el.text()).toEqual(''));
});
describe('has start date', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } } });
});
test('snapshot', () => expect(el.snapshot).toMatchSnapshot());
test('snapshot', () => expect(el).toMatchSnapshot());
test('messages: courseHasNotStarted', () => {
expect(el.instance.children[0].children[0].el).toContain(
expect(el.text()).toContain(
formatMessage(messages.courseHasNotStarted, { startDate: courseRunData.startDate }),
);
});
@@ -147,7 +147,7 @@ describe('CourseBanner', () => {
render({ enrollment: { coursewareAccess: { isStaff: true } } });
});
test('snapshot: isStaff', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
});
test('snapshot: stacking banners', () => {
@@ -160,6 +160,6 @@ describe('CourseBanner', () => {
},
},
});
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
});

View File

@@ -6,23 +6,20 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
>
<p
className="credit-error-msg"
data-testid="credit-error-msg"
>
<format-message-function
message={
{
Object {
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
"description": "",
"id": "learner-dash.courseCard.banners.credit.error",
}
}
values={
{
"supportEmailLink": <MailtoLink
to="test-support-email"
>
test-support-email
</MailtoLink>,
Object {
"supportEmailLink": <EmailLink
address="test-support-email"
/>,
}
}
/>
@@ -33,22 +30,6 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
</Banner>
`;
exports[`CreditBanner component render with error state with no email snapshot 1`] = `
<Banner
variant="danger"
>
<p
className="credit-error-msg"
data-testid="credit-error-msg"
>
An error occurred with this transaction.
</p>
<ContentComponent
cardId="test-card-id"
/>
</Banner>
`;
exports[`CreditBanner component render with no error state snapshot 1`] = `
<Banner>
<ContentComponent

View File

@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import Banner from 'components/Banner';
import EmailLink from 'components/EmailLink';
import { MailtoLink } from '@openedx/paragon';
import hooks from './hooks';
import messages from './messages';
@@ -15,14 +15,13 @@ export const CreditBanner = ({ cardId }) => {
if (hookData === null) {
return null;
}
const { ContentComponent, error, supportEmail } = hookData;
const supportEmailLink = (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>);
const supportEmailLink = (<EmailLink address={supportEmail} />);
return (
<Banner {...(error && { variant: 'danger' })}>
{error && (
<p className="credit-error-msg" data-testid="credit-error-msg">
{supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)}
<p className="credit-error-msg">
{formatMessage(messages.error, { supportEmailLink })}
</p>
)}
<ContentComponent cardId={cardId} />

View File

@@ -1,14 +1,16 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { MailtoLink } from '@openedx/paragon';
import EmailLink from 'components/EmailLink';
import hooks from './hooks';
import messages from './messages';
import CreditBanner from '.';
jest.mock('components/Banner', () => 'Banner');
jest.mock('components/EmailLink', () => 'EmailLink');
jest.mock('./hooks', () => ({
useCreditBannerData: jest.fn(),
@@ -44,37 +46,22 @@ describe('CreditBanner component', () => {
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
it('passes danger variant to Banner parent', () => {
expect(el.instance.findByType('Banner')[0].props.variant).toEqual('danger');
expect(el.find('Banner').props().variant).toEqual('danger');
});
it('includes credit-error-msg with support email link', () => {
expect(el.instance.findByTestId('credit-error-msg')[0].children[0].el).toEqual(shallow(formatMessage(messages.error, {
supportEmailLink: (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>),
})));
expect(el.find('.credit-error-msg').containsMatchingElement(
formatMessage(messages.error, {
supportEmailLink: (<EmailLink address={supportEmail} />),
}),
)).toEqual(true);
});
it('loads ContentComponent with cardId', () => {
expect(el.instance.findByType('ContentComponent')[0].props.cardId).toEqual(cardId);
expect(el.find('ContentComponent').props().cardId).toEqual(cardId);
});
});
describe('with error state with no email', () => {
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue({
error: true,
ContentComponent,
});
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('includes credit-error-msg without support email link', () => {
expect(el.instance.findByTestId('credit-error-msg')[0].children[0].el).toEqual(formatMessage(messages.errorNoEmail));
});
});
describe('with no error state', () => {
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue({
@@ -85,10 +72,10 @@ describe('CreditBanner component', () => {
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
it('loads ContentComponent with cardId', () => {
expect(el.instance.findByType('ContentComponent')[0].props.cardId).toEqual(cardId);
expect(el.find('ContentComponent').props().cardId).toEqual(cardId);
});
});
});

View File

@@ -1,16 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
const messages = defineMessages({
export const messages = StrictDict({
error: {
id: 'learner-dash.courseCard.banners.credit.error',
description: '',
defaultMessage: 'An error occurred with this transaction. For help, contact {supportEmailLink}.',
},
errorNoEmail: {
id: 'learner-dash.courseCard.banners.credit.errorNoEmail',
description: '',
defaultMessage: 'An error occurred with this transaction.',
},
});
export default messages;

View File

@@ -11,11 +11,10 @@ import messages from './messages';
export const ApprovedContent = ({ cardId }) => {
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { formatMessage } = useIntl();
return (
<CreditContent
action={{ href, message: formatMessage(messages.viewCredit), disabled: isMasquerading }}
action={{ href, message: formatMessage(messages.viewCredit) }}
message={formatMessage(
messages.approved,
{

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
@@ -10,7 +10,6 @@ import ApprovedContent from './ApprovedContent';
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
useMasqueradeData: jest.fn(),
},
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
@@ -23,7 +22,6 @@ const credit = {
providerName: 'test-credit-provider-name',
};
reduxHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
describe('ApprovedContent component', () => {
beforeEach(() => {
@@ -38,19 +36,16 @@ describe('ApprovedContent component', () => {
describe('rendered CreditContent component', () => {
let component;
beforeAll(() => {
component = el.instance.findByType('CreditContent');
component = el.find('CreditContent');
});
test('action.href from credit.providerStatusUrl', () => {
expect(component[0].props.action.href).toEqual(credit.providerStatusUrl);
expect(component.props().action.href).toEqual(credit.providerStatusUrl);
});
test('action.message is formatted viewCredit message', () => {
expect(component[0].props.action.message).toEqual(formatMessage(messages.viewCredit));
});
test('action.disabled is false', () => {
expect(component[0].props.action.disabled).toEqual(false);
expect(component.props().action.message).toEqual(formatMessage(messages.viewCredit));
});
test('message is formatted approved message', () => {
expect(component[0].props.message).toEqual(formatMessage(
expect(component.props().message).toEqual(formatMessage(
messages.approved,
{
congratulations: (<b>{formatMessage(messages.congratulations)}</b>),

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils';
@@ -36,7 +36,7 @@ const render = () => {
el = shallow(<EligibleContent cardId={cardId} />);
};
const loadComponent = () => {
component = el.instance.findByType('CreditContent');
component = el.find('CreditContent');
};
describe('EligibleContent component', () => {
beforeEach(() => {
@@ -56,24 +56,24 @@ describe('EligibleContent component', () => {
loadComponent();
});
test('action.onClick sends credit purchase track event', () => {
expect(component[0].props.action.onClick).toEqual(
expect(component.props().action.onClick).toEqual(
track.credit.purchase(courseId),
);
});
test('action.message is formatted getCredit message', () => {
expect(component[0].props.action.message).toEqual(formatMessage(messages.getCredit));
expect(component.props().action.message).toEqual(formatMessage(messages.getCredit));
});
test('message is formatted eligible message if no provider', () => {
reduxHooks.useCardCreditData.mockReturnValueOnce({});
render();
loadComponent();
expect(component[0].props.message).toEqual(formatMessage(
expect(component.props().message).toEqual(formatMessage(
messages.eligible,
{ getCredit: (<b>{formatMessage(messages.getCredit)}</b>) },
));
});
test('message is formatted eligible message if provider', () => {
expect(component[0].props.message).toEqual(
expect(component.props().message).toEqual(
formatMessage(messages.eligibleFromProvider, { providerName: credit.providerName }),
);
});

View File

@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink';
import hooks from './hooks';
@@ -13,13 +12,11 @@ import messages from './messages';
export const MustRequestContent = ({ cardId }) => {
const { formatMessage } = useIntl();
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
return (
<CreditContent
action={{
message: formatMessage(messages.requestCredit),
onClick: createCreditRequest,
disabled: isMasquerading,
}}
message={formatMessage(messages.mustRequest, {
linkToProviderSite: (<ProviderLink cardId={cardId} />),

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import messages from './messages';
import hooks from './hooks';
import ProviderLink from './components/ProviderLink';
@@ -12,9 +11,6 @@ import MustRequestContent from './MustRequestContent';
jest.mock('./hooks', () => ({
useCreditRequestData: jest.fn(),
}));
jest.mock('hooks', () => ({
reduxHooks: { useMasqueradeData: jest.fn() },
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
@@ -24,11 +20,7 @@ let component;
const cardId = 'test-card-id';
const requestData = { test: 'requestData' };
const createCreditRequest = jest.fn().mockName('createCreditRequest');
hooks.useCreditRequestData.mockReturnValue({
requestData,
createCreditRequest,
});
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
hooks.useCreditRequestData.mockReturnValue({ requestData, createCreditRequest });
const render = () => {
el = shallow(<MustRequestContent cardId={cardId} />);
@@ -45,29 +37,24 @@ describe('MustRequestContent component', () => {
describe('render', () => {
describe('rendered CreditContent component', () => {
beforeEach(() => {
component = el.instance.findByType('CreditContent');
component = el.find('CreditContent');
});
test('action.onClick calls createCreditRequest from useCreditRequestData hook', () => {
expect(component[0].props.action.onClick).toEqual(createCreditRequest);
expect(component.props().action.onClick).toEqual(createCreditRequest);
});
test('action.message is formatted requestCredit message', () => {
expect(component[0].props.action.message).toEqual(
formatMessage(messages.requestCredit),
);
});
test('action.disabled is false', () => {
expect(component[0].props.action.disabled).toEqual(false);
expect(component.props().action.message).toEqual(formatMessage(messages.requestCredit));
});
test('message is formatted mustRequest message', () => {
expect(component[0].props.message).toEqual(
expect(component.props().message).toEqual(
formatMessage(messages.mustRequest, {
linkToProviderSite: <ProviderLink cardId={cardId} />,
requestCredit: <b>{formatMessage(messages.requestCredit)}</b>,
linkToProviderSite: (<ProviderLink cardId={cardId} />),
requestCredit: (<b>{formatMessage(messages.requestCredit)}</b>),
}),
);
});
test('requestData drawn from useCreditRequestData hook', () => {
expect(component[0].props.requestData).toEqual(requestData);
expect(component.props().requestData).toEqual(requestData);
});
});
});

View File

@@ -9,14 +9,12 @@ import messages from './messages';
export const PendingContent = ({ cardId }) => {
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { formatMessage } = useIntl();
return (
<CreditContent
action={{
href,
message: formatMessage(messages.viewDetails),
disabled: isMasquerading,
}}
message={formatMessage(messages.received, { providerName })}
/>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
@@ -7,9 +7,7 @@ import { reduxHooks } from 'hooks';
import messages from './messages';
import PendingContent from './PendingContent';
jest.mock('hooks', () => ({
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
}));
jest.mock('hooks', () => ({ reduxHooks: { useCardCreditData: jest.fn() } }));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
@@ -19,11 +17,7 @@ let component;
const cardId = 'test-card-id';
const providerName = 'test-credit-provider-name';
const providerStatusUrl = 'test-credit-provider-status-url';
reduxHooks.useCardCreditData.mockReturnValue({
providerName,
providerStatusUrl,
});
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
reduxHooks.useCardCreditData.mockReturnValue({ providerName, providerStatusUrl });
const render = () => {
el = shallow(<PendingContent cardId={cardId} />);
@@ -40,21 +34,16 @@ describe('PendingContent component', () => {
describe('render', () => {
describe('rendered CreditContent component', () => {
beforeEach(() => {
component = el.instance.findByType('CreditContent');
component = el.find('CreditContent');
});
test('action.href will go to provider status site', () => {
expect(component[0].props.action.href).toEqual(providerStatusUrl);
expect(component.props().action.href).toEqual(providerStatusUrl);
});
test('action.message is formatted requestCredit message', () => {
expect(component[0].props.action.message).toEqual(
formatMessage(messages.viewDetails),
);
});
test('action.disabled is false', () => {
expect(component[0].props.action.disabled).toEqual(false);
expect(component.props().action.message).toEqual(formatMessage(messages.viewDetails));
});
test('message is formatted pending message', () => {
expect(component[0].props.message).toEqual(
expect(component.props().message).toEqual(
formatMessage(messages.received, { providerName }),
);
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
@@ -25,7 +25,7 @@ reduxHooks.useCardCreditData.mockReturnValue(credit);
let el;
let component;
const render = () => { el = shallow(<RejectedContent cardId={cardId} />); };
const loadComponent = () => { component = el.instance.findByType('CreditContent'); };
const loadComponent = () => { component = el.find('CreditContent'); };
describe('RejectedContent component', () => {
beforeEach(render);
@@ -38,10 +38,10 @@ describe('RejectedContent component', () => {
describe('rendered CreditContent component', () => {
beforeAll(loadComponent);
test('no action is passed', () => {
expect(component[0].props.action).toEqual(undefined);
expect(component.props().action).toEqual(undefined);
});
test('message is formatted rejected message', () => {
expect(component[0].props.message).toEqual(formatMessage(
expect(component.props().message).toEqual(formatMessage(
messages.rejected,
{
linkToProviderSite: <ProviderLink cardId={cardId} />,

View File

@@ -1,27 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ActionRow, Button } from '@openedx/paragon';
import { ActionRow, Button } from '@edx/paragon';
import CreditRequestForm from './CreditRequestForm';
export const CreditContent = ({ action, message, requestData }) => (
<>
<div className="message-copy credit-msg" data-testid="credit-msg">
<div className="message-copy credit-msg">
{message}
</div>
{action && (
<ActionRow className="mt-4">
<Button
as="a"
disabled={!!action.disabled}
// make sure href is not undefined. Paragon won't disable the button if href is undefined.
href={action.href || '#'}
href={action.href}
rel="noopener"
target="_blank"
variant="outline-primary"
className="border-gray-400"
onClick={action.onClick}
data-testid="action-row-btn"
>
{action.message}
</Button>
@@ -39,7 +36,6 @@ CreditContent.propTypes = {
href: PropTypes.string,
onClick: PropTypes.func,
message: PropTypes.string,
disabled: PropTypes.bool,
}),
message: PropTypes.node.isRequired,
requestData: PropTypes.shape({

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import CreditContent from './CreditContent';
@@ -8,7 +8,6 @@ const action = {
href: 'test-action-href',
onClick: jest.fn().mockName('test-action-onClick'),
message: 'test-action-message',
disabled: false,
};
const message = 'test-message';
@@ -22,24 +21,19 @@ describe('CreditContent component', () => {
el = shallow(<CreditContent {...props} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
it('loads href, onClick, and message into action row button', () => {
const buttonEl = el.instance.findByTestId('action-row-btn')[0];
expect(buttonEl.props.href).toEqual(action.href);
expect(buttonEl.props.onClick).toEqual(action.onClick);
expect(buttonEl.props.disabled).toEqual(action.disabled);
expect(buttonEl.children[0].el).toEqual(action.message);
const buttonEl = el.find('ActionRow Button');
expect(buttonEl.props().href).toEqual(action.href);
expect(buttonEl.props().onClick).toEqual(action.onClick);
expect(buttonEl.text()).toEqual(action.message);
});
it('loads message into credit-msg div', () => {
expect(el.instance.findByTestId('credit-msg')[0].children[0].el).toEqual(message);
expect(el.find('div.credit-msg').text()).toEqual(message);
});
it('loads CreditRequestForm with passed requestData', () => {
expect(el.instance.findByType('CreditRequestForm')[0].props.requestData).toEqual(requestData);
});
test('disables action button when action.disabled is true', () => {
el = shallow(<CreditContent {...props} action={{ ...action, disabled: true }} />);
expect(el.instance.findByTestId('action-row-btn')[0].props.disabled).toEqual(true);
expect(el.find('CreditRequestForm').props().requestData).toEqual(requestData);
});
});
describe('without action', () => {
@@ -47,13 +41,13 @@ describe('CreditContent component', () => {
el = shallow(<CreditContent {...{ message, requestData }} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
it('loads message into credit-msg div', () => {
expect(el.instance.findByTestId('credit-msg')[0].children[0].el).toEqual(message);
expect(el.find('div.credit-msg').text()).toEqual(message);
});
it('loads CreditRequestForm with passed requestData', () => {
expect(el.instance.findByType('CreditRequestForm')[0].props.requestData).toEqual(requestData);
expect(el.find('CreditRequestForm').props().requestData).toEqual(requestData);
});
});
});

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Form, FormControl } from '@openedx/paragon';
import { Button, Form, FormControl } from '@edx/paragon';
import useCreditRequestFormData from './hooks';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { keyStore } from 'utils';
@@ -46,19 +46,19 @@ describe('CreditRequestForm component', () => {
shallowRender(requestData);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
it('loads Form with requestData url', () => {
expect(el.instance.findByType('Form')[0].props.action).toEqual(requestData.url);
expect(el.find('Form').props().action).toEqual(requestData.url);
});
it('loads a textarea form control for each requestData parameter', () => {
const controls = el.instance.findByType('FormControl');
expect(controls[0].props.name).toEqual(paramKeys.key1);
expect(controls[0].props.value).toEqual(requestData.parameters.key1);
expect(controls[1].props.name).toEqual(paramKeys.key2);
expect(controls[1].props.value).toEqual(requestData.parameters.key2);
expect(controls[2].props.name).toEqual(paramKeys.key3);
expect(controls[2].props.value).toEqual(requestData.parameters.key3);
const controls = el.find('FormControl');
expect(controls.at(0).props().name).toEqual(paramKeys.key1);
expect(controls.at(0).props().value).toEqual(requestData.parameters.key1);
expect(controls.at(1).props().name).toEqual(paramKeys.key2);
expect(controls.at(1).props().value).toEqual(requestData.parameters.key2);
expect(controls.at(2).props().name).toEqual(paramKeys.key3);
expect(controls.at(2).props().value).toEqual(requestData.parameters.key3);
});
});
});

View File

@@ -4,7 +4,7 @@ import { render } from '@testing-library/react';
import useCreditRequestFormData from './hooks';
import CreditRequestForm from '.';
jest.unmock('@openedx/paragon');
jest.unmock('@edx/paragon');
jest.unmock('react');
jest.mock('./hooks', () => ({

View File

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { reduxHooks } from 'hooks';
@@ -30,13 +30,13 @@ describe('ProviderLink component', () => {
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
it('passes credit.providerStatusUrl to the hyperlink href', () => {
expect(el.instance.findByType('Hyperlink')[0].props.href).toEqual(credit.providerStatusUrl);
expect(el.find('Hyperlink').props().href).toEqual(credit.providerStatusUrl);
});
it('passes providerName for the link message', () => {
expect(el.instance.findByType('Hyperlink')[0].children[0].el).toEqual(credit.providerName);
expect(el.find('Hyperlink').text()).toEqual(credit.providerName);
});
});
});

View File

@@ -4,7 +4,6 @@ exports[`CreditContent component render with action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>
@@ -14,8 +13,6 @@ exports[`CreditContent component render with action snapshot 1`] = `
<Button
as="a"
className="border-gray-400"
data-testid="action-row-btn"
disabled={false}
href="test-action-href"
onClick={[MockFunction test-action-onClick]}
rel="noopener"
@@ -27,8 +24,8 @@ exports[`CreditContent component render with action snapshot 1`] = `
</ActionRow>
<CreditRequestForm
requestData={
{
"parameters": {
Object {
"parameters": Object {
"key1": "val1",
},
"url": "test-request-data-url",
@@ -42,14 +39,13 @@ exports[`CreditContent component render without action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>
<CreditRequestForm
requestData={
{
"parameters": {
Object {
"parameters": Object {
"key1": "val1",
},
"url": "test-request-data-url",

View File

@@ -14,10 +14,7 @@ export const useCreditRequestData = (cardId) => {
const createCreditApiRequest = apiHooks.useCreateCreditRequest(cardId);
const createCreditRequest = (e) => {
e.preventDefault();
createCreditApiRequest()
.then((request) => {
setRequestData(request.data);
});
createCreditApiRequest().then(setRequestData);
};
return { requestData, createCreditRequest };
};

View File

@@ -11,7 +11,7 @@ jest.mock('hooks', () => ({
const state = new MockUseState(hooks);
const cardId = 'test-card-id';
const requestData = { data: 'request data' };
const requestData = { test: 'request data' };
const creditRequest = jest.fn().mockReturnValue(Promise.resolve(requestData));
apiHooks.useCreateCreditRequest.mockReturnValue(creditRequest);
const event = { preventDefault: jest.fn() };
@@ -48,7 +48,7 @@ describe('Credit Banner view hooks', () => {
it('calls api.createCreditRequest and sets requestData with the response', async () => {
await out.createCreditRequest(event);
expect(creditRequest).toHaveBeenCalledWith();
expect(state.setState.creditRequestData).toHaveBeenCalledWith(requestData.data);
expect(state.setState.creditRequestData).toHaveBeenCalledWith(requestData);
});
});
});

View File

@@ -1,6 +1,6 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
const messages = defineMessages({
export const messages = StrictDict({
approved: {
id: 'learner-dash.courseCard.banners.credit.approved',
description: '',

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, MailtoLink } from '@openedx/paragon';
import { Button, MailtoLink } from '@edx/paragon';
import { utilHooks, reduxHooks } from 'hooks';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { reduxHooks } from 'hooks';
import EntitlementBanner from './EntitlementBanner';
@@ -50,11 +50,11 @@ describe('EntitlementBanner', () => {
});
test('snapshot: no sessions available', () => {
render({ entitlement: { isFulfilled: false, hasSessions: false } });
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('snapshot: expiration warning', () => {
render({ entitlement: { showExpirationWarning: true } });
expect(el.snapshot).toMatchSnapshot();
expect(el).toMatchSnapshot();
});
test('no display if sessions available and not displaying warning', () => {
render();

View File

@@ -1,4 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils';
import { shallow } from 'enzyme';
import { ProgramsList } from './ProgramsList';
@@ -16,8 +16,8 @@ describe('ProgramsList', () => {
it('renders correctly', () => {
const wrapper = shallow(<ProgramsList programs={programs} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
expect(wrapper.instance.findByType('li').length).toEqual(programs.length);
expect(wrapper.find('li').length).toEqual(programs.length);
});
});

View File

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

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Program } from '@openedx/paragon/icons';
import { Program } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
@@ -15,20 +15,16 @@ export const RelatedProgramsBanner = ({ cardId }) => {
const programData = reduxHooks.useCardRelatedProgramsData(cardId);
if (!programData?.length) {
return null;
}
return (
<Banner
icon={Program}
className="bg-white border-top border-bottom mb-0 related-programs-banner"
>
<span className="font-weight-bolder">
{formatMessage(messages.relatedPrograms)}
</span>
<ProgramList programs={programData.list} />
</Banner>
programData?.length > 0 && (
<Banner
icon={Program}
className="bg-white border-top border-bottom mb-0 related-programs-banner"
>
<span className="font-weight-bolder">{formatMessage(messages.relatedPrograms)}</span>
<ProgramList programs={programData.list} />
</Banner>
)
);
};
RelatedProgramsBanner.propTypes = {

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