Compare commits

...

93 Commits

Author SHA1 Message Date
Peter Kulko
49d64ff5c6 fix: fixed module button href (#508) 2024-12-12 10:30:30 +02:00
Peter Kulko
fe658c1796 fix: added LMS_BASE_URL for brand logo (#498) 2024-11-20 13:19:08 +02:00
Max Sokolski
82dbc27aba fix: removed Program link from the header (#497) 2024-11-18 16:59:27 +02:00
Peter Kulko
25d3f831a2 fix: removed program tab in the header 2024-11-14 23:35:40 +02:00
Adolfo R. Brandes
bc68a8c674 fix: Remove edX-specific reference
Remove edx-Specific reference from email confirmation banner.
2024-06-10 11:35:01 -03:00
Adolfo R. Brandes
15f9969993 feat: use frontend-plugin-framework to provide a FooterSlot (#356)
Co-authored-by: Brian Smith <bsmith@axim.org>
2024-06-06 15:05:35 -03:00
renovate[bot]
4c9008d141 fix(deps): update dependency react-pdf to v7 [security] (#340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-09 12:30:33 -04:00
renovate[bot]
81c44f1317 fix(deps): update dependency axios to ^0.28.0 2024-05-06 08:12:41 +00:00
renovate[bot]
4d00fe924b fix(deps): update dependency @openedx/paragon to v22.3.2 2024-05-06 04:19:13 +00:00
Maxwell Frank
4b8f8798c2 Revert "feat: set up dashboard to deploy js configs" (#337) 2024-05-02 15:34:37 -04:00
Adolfo R. Brandes
d3779adfde Revert "fix: hiding certificate banner for exec ed" (#328) 2024-04-30 12:47:26 -03:00
Deborah Kaplan
cf38f348b1 Merge branch 'master' into revert-326 2024-04-29 10:22:59 -07:00
renovate[bot]
07730596ca fix(deps): update dependency @fortawesome/react-fontawesome to ^0.2.0 2024-04-29 16:31:39 +00:00
renovate[bot]
233ea047e6 fix(deps): update dependency @edx/frontend-component-footer to v13.1.0 2024-04-29 14:34:10 +00:00
renovate[bot]
ba2000581a chore(deps): update dependency node to 18.20 2024-04-29 10:00:25 +00:00
renovate[bot]
ac304ce66d chore(deps): update dependency @openedx/frontend-build to v13.1.4 2024-04-29 07:16:50 +00:00
renovate[bot]
be7d274479 fix(deps): update dependency whatwg-fetch to v3.6.20 2024-04-29 05:27:23 +00:00
renovate[bot]
1dd71f3aec fix(deps): update dependency @reduxjs/toolkit to v1.9.7 2024-04-29 04:20:52 +00:00
Adolfo R. Brandes
a5730daa14 Revert "fix: hiding certificate banner for exec ed"
This reverts commit d90339f5f5.

For the record, Axim Engineering no longer accepts organization-specific
code to land in master.
2024-04-26 13:24:20 -03:00
Kira Miller
24a97445a3 fix: hiding certificate banner for exec ed (#326) 2024-04-25 16:34:48 -06:00
Kira Miller
d90339f5f5 fix: hiding certificate banner for exec ed 2024-04-25 22:19:37 +00:00
renovate[bot]
150f28c374 fix(deps): update dependency @edx/frontend-platform to v7.1.4 2024-04-22 10:43:53 +00:00
renovate[bot]
fb145054a6 fix(deps): update dependency @edx/frontend-component-footer to v13.0.5 2024-04-22 06:34:06 +00:00
Maxwell Frank
96050cb3f3 chore(deps): upgrade to paragon v22 (#320) 2024-04-19 10:20:51 -04:00
Deborah Kaplan
272870f769 chore: add logging to the github action (#318) 2024-04-16 10:59:34 -04:00
Jason Wesson
f98e5994c4 fix: add postinstall script for patch-package (#316) 2024-04-15 12:20:26 -06:00
Jason Wesson
57b6e57fc0 fix: add postinstall script for patch-packageç 2024-04-15 18:14:33 +00:00
renovate[bot]
a0795b5ae0 fix(deps): update dependency @edx/brand to v1.2.3 2024-04-15 05:52:50 +00:00
Justin Hynes
5cd8b005b9 feat!: Removes rendering of the RecommendationsPanel by default in Open edX (#311) 2024-04-05 14:30:08 -04:00
Deborah Kaplan
11bf6f2554 feat: separate no courses and course list into slots
*  this creates a plug-in slot with an ID for each of no courses and course list

FIXES: APER-3304
2024-04-04 17:22:26 -04:00
Maxwell Frank
b8e08d8a8f Refactor course list and empty state (#308)
Co-authored-by: Maxwell Frank <mfrank@2u.com>
2024-04-03 13:41:20 -04:00
Jason Wesson
e045932e5f feat: set up dashboard to deploy js configs (#304) 2024-03-28 12:02:09 -06:00
Jason Wesson
57d3b5a276 refactor: remove over-write of environment variables in configuration.js 2024-03-28 17:27:58 +00:00
Jason Wesson
14a935556b Merge branch 'master' into jwesson/set-up-dashboard-to-deploy-js-configs 2024-03-28 11:05:43 -06:00
Maxwell Frank
731fbe2e2e refactor: course filters (#303)
Co-authored-by: Maxwell Frank <mfrank@2u.com>
2024-03-28 12:50:59 -04:00
Jason Wesson
ef4b7ecb5d feat: merge JS config variables in initialized config 2024-03-28 16:43:59 +00:00
Jason Wesson
ac8ede4b4f feat: add frontend-plugin-framework library to mfe (#299) 2024-03-26 10:31:00 -06:00
Jason Wesson
32822747ee chore: bump plugin framework version 2024-03-26 16:19:24 +00:00
Jason Wesson
523d531a38 Merge branch 'master' into jwesson/fpf-in-ld 2024-03-19 09:51:01 -06:00
Jason Wesson
86176ce34e revert: remove gitignore change 2024-03-18 21:20:52 +00:00
Jason Wesson
8d6204a4a6 fix: mock variables for jest test 2024-03-18 21:19:21 +00:00
Jason Wesson
ec7069b02d Merge branch 'master' into jwesson/set-up-dashboard-to-deploy-js-configs 2024-03-18 21:04:35 +00:00
Jason Wesson
78b30d8777 feat: add js config compatibility to mfe prod build 2024-03-18 20:55:06 +00:00
Samir Sabri
b01960e6a1 feat!: remove Transifex calls for OEP-58 | FC-0012 (#227) 2024-03-18 16:54:04 -04:00
Jason Wesson
a7f2cec12b feat: add frontend-plugin-framework library to mfe 2024-03-18 19:49:41 +00:00
Justin Hynes
5083645eff refactor: remove unused cookies.js util file (#296) 2024-03-04 11:14:27 -05:00
Justin Hynes
e167df0082 docs: Add codecov/status/license/ci badge to README (#295) 2024-03-04 08:09:46 -05:00
Adolfo R. Brandes
ce9dd938b6 refactor: replace @edx/paragon and @edx/frontend-build (#266) 2024-02-29 09:44:46 -03:00
Bilal Qamar
53abfb03e7 Merge branch 'master' of https://github.com/openedx/frontend-app-learner-dashboard into mashal-m/replace-edx/paragon-frontend-build 2024-02-29 11:05:44 +05:00
Fateme Khodayari
e8660b941f fix: translation for header of course filter form (course status) (#289) 2024-02-26 16:02:08 -05:00
Attiya Ishaque
99815e8b60 refactor: remove painted door recommendations experiment (#292) 2024-02-23 13:04:29 +05:00
Attiya Ishaque
3e116bf047 refactor: remove painted door experiment code 2024-02-23 12:56:27 +05:00
Ghassan Maslamani
0e9d0ea3c2 fix: #287 translation strigs are not exported (#293) 2024-02-22 17:25:08 -05:00
Bilal Qamar
75a4af95c5 Merge branch 'master' into mashal-m/replace-edx/paragon-frontend-build 2024-02-22 15:40:54 +05:00
Deborah Kaplan
152c2a4847 feat: adding catalog-info.yml (#294) 2024-02-21 16:01:17 -05:00
Muhammad Abdullah Waheed
69c085f4cd fix: fixed mock for unit test 2024-02-20 14:12:23 +05:00
Muhammad Abdullah Waheed
769032fc92 fix: updated snapshots 2024-02-15 16:31:01 +05:00
Bilal Qamar
858aff6fba refactor: updated edx packages 2024-02-15 16:07:47 +05:00
Bilal Qamar
e1a6293ebb Merge branch 'master' of https://github.com/openedx/frontend-app-learner-dashboard into mashal-m/replace-edx/paragon-frontend-build 2024-02-15 14:51:25 +05:00
salmannawaz
3268165233 Add renovate configuration file (#283)
Co-authored-by: Maxwell Frank <92897870+MaxFrank13@users.noreply.github.com>
2024-02-09 15:30:06 -05:00
Jason Wesson
ad56b361be fix: URLs get the current value of LMS_BASE_URL from getConfig() #269 (#273) 2024-02-07 13:39:41 -06:00
hinakhadim
c73c0fe30d fix: URLs get the current value of LMS_BASE_URL from getConfig() #269 2024-02-07 10:59:35 +05:00
Deborah Kaplan
3a2d6db65f feat: transition ownership of the learner dash (#281) 2024-02-06 11:49:04 -05:00
Deborah Kaplan
8edc7572e2 feat: transition ownership of the learner dash
*  transition ownership of the component
*  correct ownership label for vanguards

FIXES:
APER-3174
2024-02-06 16:34:20 +00:00
Hina Khadim
bc377b265a fix: Course filter adds duplicate values (#275) 2024-02-05 14:03:06 -05:00
Omar Al-Ithawi
4730cf82e8 feat: tutor-mfe compatiblilty for atlas pull | FC-0012 (#280)
- install atlas
 - remove `--filter` to pull all languages by default
 - use ATLAS_OPTIONS to allow custom `--filter`
 - include frontend-platform in `atlas pull`

Refs: FC-0012 OEP-58
2024-02-02 10:30:26 -05:00
leangseu-edx
513d3fc4eb fix: a few typos (#279) 2024-01-31 10:49:47 -05:00
Syed Ali Abbas Zaidi
69e090e9a6 feat: migrate enzyme to react-unit-test-utils (#276) 2024-01-30 12:31:34 +05:00
mashal-m
79314ead86 refactor: replace @edx/paragon and @edx/frontend-build 2023-12-29 11:19:54 +05:00
Maria Grimaldi
bed2c341b5 refactor: hide switch dashboard option behind flag (#237) 2023-12-07 09:32:19 -05:00
Justin Hynes
9d334350f5 chore: add Aperture team OpsGenie email address as a recipient when CI fails for Learner Dashboard (#255) 2023-12-04 13:07:58 -05:00
Jason Wesson
bd0c0c578c feat: update environment variables to use javascript-based config (#249) 2023-12-04 10:00:44 -08:00
Jason Wesson
edc4afe4dd fix: typo in setUp context 2023-12-04 17:48:30 +00:00
Jason Wesson
e191448bb8 docs: add more context in using env.config.js 2023-12-01 16:20:00 +00:00
Jason Wesson
ec4e9e8c60 docs: add comments around how to set up env.config.js and testing with Jest 2023-11-30 22:54:17 +00:00
Jason Wesson
d22dc31208 Merge branch 'master' into jwesson/update-to-jsbased-config 2023-11-30 14:33:53 -08:00
Jason Wesson
f041d35c27 docs: add example env.config.js file 2023-11-30 22:25:24 +00:00
Jason Wesson
5b15cef74a feat: add env.config.js to gitignore 2023-11-30 22:25:24 +00:00
Deborah Kaplan
a4f14da17a fix: use getConfig not process.env (#232) 2023-11-30 10:51:49 -05:00
Brian Smith
81ce59eab7 fix: properly use OPTIMIZELY_PROJECT_ID and increase test coverage 2023-11-29 12:15:47 -05:00
Deborah Kaplan
d1bf6f9c91 Merge branch 'master' into runtime-config 2023-11-21 12:09:14 -05:00
Justin Hynes
5ca1e9dc1f fix: MailToLink to account for no emails (#230) 2023-11-21 07:54:00 -05:00
Cindy Nguyen
b83f128f81 fix: MailToLink to account for no emails 2023-11-20 10:45:14 -05:00
Brian Smith
c2a20af9b8 fix: replace hardcoded strings and properly define i18n messages 2023-11-16 15:46:25 -03:00
Jason Wesson
8f2ed779ca fix: default context for painted door experiment (#218) 2023-11-14 11:44:42 -08:00
Brian Smith
0cedeb0809 fix: default context for painted door experiment
There are multiple places where attributes of the object provided by `usePaintedDoorExperimentContext()` are assumed to exist. This provides default (null) values for those attributes when creating the context.
2023-11-14 14:26:11 -05:00
Mashal Malik
1a51ac07a2 refactor: updated README file to reflect template changes (#229) 2023-10-31 19:12:14 +05:00
Cindy Nguyen
4b2d65c44c fix: use getConfig not process.env
Co-authored-by: Mena Hassan <mhassan@axim.org>
2023-10-27 15:50:44 -04:00
Jason Wesson
c44db75273 feat: babel-plugin-react-intl to babel-plugin-formatjs migration (#209) 2023-10-23 12:54:17 -07:00
Jason Wesson
a98fd50788 Merge branch 'master' into abdullahwaheed/react-intl-to-formatjs 2023-10-23 12:54:04 -07:00
Illia Shestakov
fd57523b2e fix(deps): replace edx.org brand dependency with openedx brand (#183) 2023-10-23 15:07:47 -03:00
Abdullah Waheed
7375c8f27b fix: upgraded frontend-build to fix security issue 2023-10-10 18:29:42 +05:00
Abdullah Waheed
0cf98c9b78 feat: babel-plugin-react-intl to babel-plugin-formatjs migration 2023-10-04 19:55:38 +05:00
279 changed files with 11561 additions and 6673 deletions

2
.env
View File

@@ -41,4 +41,4 @@ ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true
ENABLE_EDX_PERSONAL_DASHBOARD=false

View File

@@ -8,7 +8,6 @@ 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'
@@ -48,3 +47,4 @@ ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
ENABLE_EDX_PERSONAL_DASHBOARD=false

View File

@@ -8,7 +8,6 @@ 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'
@@ -47,4 +46,4 @@ ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY='SDK Key'
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true
ENABLE_EDX_PERSONAL_DASHBOARD=true

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('eslint', {
rules: {

6
.github/CODEOWNERS vendored
View File

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

33
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,33 @@
{
"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

@@ -50,8 +50,10 @@ jobs:
username: ${{ secrets.EDX_SMTP_USERNAME }}
password: ${{ secrets.EDX_SMTP_PASSWORD }}
subject: CI workflow failed in ${{github.repository}}
to: masters-grades@edx.org
to: masters-grades@edx.org,aperture@2u-internal.opsgenie.net
from: github-actions <github-actions@edx.org>
nodemailerlog: true
nodemailerdebug: true
body: CI workflow in ${{github.repository}} failed!
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
}}"

2
.gitignore vendored
View File

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

2
.nvmrc
View File

@@ -1 +1 @@
18.15
18.20

View File

@@ -1,9 +0,0 @@
[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

@@ -1,8 +1,6 @@
npm-install-%: ## install specified % npm package
npm install $* --save-dev
git add package.json
export TRANSIFEX_RESOURCE = frontend-app-learner-dashboard
transifex_langs = "ar,fr,fr_CA,es_419,pt_BR,zh_CN"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
@@ -10,7 +8,7 @@ i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
transifex_temp = ./temp/babel-plugin-formatjs
NPM_TESTS=build i18n_extract lint test
@@ -42,34 +40,17 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# 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/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \
&& 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
$(intl_imports) paragon frontend-component-footer frontend-app-learner-dashboard
endif
$(intl_imports) frontend-platform paragon frontend-component-footer frontend-app-learner-dashboard
# This target is used by CI.
validate-no-uncommitted-package-lock-changes:

View File

@@ -1,26 +0,0 @@
# 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)

107
README.rst Normal file
View File

@@ -0,0 +1,107 @@
|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.

24
catalog-info.yml Normal file
View File

@@ -0,0 +1,24 @@
# 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.

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

@@ -0,0 +1,74 @@
/*
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: '',
OPTIMIZELY_FULL_STACK_SDK_KEY: '',
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
};

View File

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

12727
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
},
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"semantic-release": "semantic-release",
@@ -26,44 +26,44 @@
"access": "public"
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-component-footer": "^12.2.1",
"@edx/frontend-enterprise-hotjar": "^2.0.0",
"@edx/frontend-platform": "^5.5.4",
"@edx/paragon": "^20.44.0",
"@edx/react-unit-test-utils": "^1.7.0",
"@edx/frontend-enterprise-hotjar": "3.0.0",
"@edx/frontend-platform": "^7.1.4",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "2.0.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15",
"@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.1.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.2.2",
"@optimizely/react-sdk": "^2.9.2",
"@redux-beacon/segment": "^1.1.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.21.4",
"axios": "^0.28.0",
"classnames": "^2.3.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.0.1",
"html-react-parser": "^1.3.0",
"jest": "^26.6.3",
"jest": "^29.7.0",
"jest-when": "^3.6.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "15.7.2",
"prop-types": "^15.7.2",
"query-string": "7.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-intl": "^5.20.9",
"react-pdf": "^5.5.0",
"react-intl": "6.4.7",
"react-pdf": "^7.0.0",
"react-redux": "^7.2.4",
"react-router-dom": "6.15.0",
"react-share": "^4.4.0",
@@ -80,11 +80,10 @@
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@edx/frontend-build": "12.8.27",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "13.1.4",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"axios-mock-adapter": "^1.20.0",
"copy-webpack-plugin": "^11.0.0",
"fetch-mock": "^9.11.0",

View File

@@ -1,19 +1,8 @@
<!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" />
<% if (process.env.OPTIMIZELY_URL) { %>
<script
src="<%= process.env.OPTIMIZELY_URL %>"
></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
></script>
<% } %>
</head>
<body>
<div id="root"></div>

View File

@@ -6,8 +6,8 @@ import { logError } from '@edx/frontend-platform/logging';
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
import Footer from '@edx/frontend-component-footer';
import { Alert } from '@edx/paragon';
import FooterSlot from '@openedx/frontend-slot-footer';
import { Alert } from '@openedx/paragon';
import { RequestKeys } from 'data/constants/requests';
import store from 'data/store';
@@ -27,6 +27,7 @@ import fakeData from 'data/services/lms/fakeData/courses';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
import './App.scss';
@@ -41,8 +42,21 @@ export const App = () => {
const { supportEmail } = reduxHooks.usePlatformSettingsData();
const loadData = reduxHooks.useLoadData();
const optimizelyScript = () => {
if (getConfig().OPTIMIZELY_URL) {
return <script src={getConfig().OPTIMIZELY_URL} />;
} if (getConfig().OPTIMIZELY_PROJECT_ID) {
return (
<script
src={`${getConfig().MARKETING_SITE_BASE_URL}/optimizelyjs/${getConfig().OPTIMIZELY_PROJECT_ID}.js`}
/>
);
}
return null;
};
React.useEffect(() => {
if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') {
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
window.loadEmptyData = () => {
loadData({ ...fakeData.globalData, courses: [] });
};
@@ -60,12 +74,12 @@ export const App = () => {
window.actions = actions;
window.track = track;
}
if (process.env.HOTJAR_APP_ID) {
if (getConfig().HOTJAR_APP_ID) {
try {
initializeHotjar({
hotjarId: process.env.HOTJAR_APP_ID,
hotjarVersion: process.env.HOTJAR_VERSION,
hotjarDebug: !!process.env.HOTJAR_DEBUG,
hotjarId: getConfig().HOTJAR_APP_ID,
hotjarVersion: getConfig().HOTJAR_VERSION,
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
});
} catch (error) {
logError(error);
@@ -76,6 +90,8 @@ export const App = () => {
<>
<Helmet>
<title>{formatMessage(messages.pageTitle)}</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
{optimizelyScript()}
</Helmet>
<div>
<AppWrapper>
@@ -93,7 +109,7 @@ export const App = () => {
)}
</main>
</AppWrapper>
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
<FooterSlot />
<ZendeskFab />
</div>
</>

View File

@@ -1,7 +1,7 @@
// frontend-app-*/src/index.scss
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@edx/paragon/scss/core/core";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
$fa-font-path: "~font-awesome/fonts";

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { Helmet } from 'react-helmet';
import { shallow } from '@edx/react-unit-test-utils';
import Footer from '@edx/frontend-component-footer';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { RequestKeys } from 'data/constants/requests';
import { reduxHooks } from 'hooks';
@@ -14,7 +14,7 @@ import { ExperimentProvider } from 'ExperimentContext';
import { App } from './App';
import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => 'Footer');
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
jest.mock('containers/Dashboard', () => 'Dashboard');
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
@@ -37,17 +37,19 @@ 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 = () => {
@@ -61,9 +63,6 @@ describe('App router component', () => {
it('displays learner dashboard header', () => {
expect(el.instance.findByType(LearnerDashboardHeader).length).toEqual(1);
});
test('Footer logo drawn from env variable', () => {
expect(el.instance.findByType(Footer)[0].props.logo).toEqual(logo);
});
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');
@@ -73,6 +72,43 @@ describe('App router component', () => {
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 expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
).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 expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
).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();
@@ -90,6 +126,7 @@ describe('App router component', () => {
describe('initialize failure', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
getConfig.mockReturnValue({});
el = shallow(<App />);
});
runBasicTests();
@@ -107,6 +144,7 @@ describe('App router component', () => {
describe('refresh failure', () => {
beforeAll(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
getConfig.mockReturnValue({});
el = shallow(<App />);
});
runBasicTests();

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useWindowSize, breakpoints } from '@edx/paragon';
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { StrictDict } from 'utils';
import api from 'widgets/ProductRecommendations/api';
import * as module from './ExperimentContext';

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { useWindowSize } from '@edx/paragon';
import { waitFor, render } from '@testing-library/react';
import { useWindowSize } from '@openedx/paragon';
import api from 'widgets/ProductRecommendations/api';
import { MockUseState } from 'testUtils';
@@ -40,7 +39,7 @@ describe('experiments context', () => {
it('calls useEffect once', () => {
expect(calls.length).toEqual(1);
});
describe('successfull fetch', () => {
describe('successful fetch', () => {
it('sets the country code', async () => {
let resolveFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
@@ -58,7 +57,7 @@ describe('experiments context', () => {
});
});
});
describe('unsuccessfull fetch', () => {
describe('unsuccessful fetch', () => {
it('sets the country code to an empty string', async () => {
let rejectFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
@@ -109,7 +108,7 @@ describe('experiments context', () => {
state.mock();
mount(
render(
<ExperimentProvider>
<TestComponent />
</ExperimentProvider>,

View File

@@ -9,6 +9,10 @@ exports[`App router component component initialize failure snapshot 1`] = `
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<div>
<AppWrapper>
@@ -23,9 +27,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
</Alert>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
@@ -40,6 +42,10 @@ 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>
@@ -50,9 +56,71 @@ exports[`App router component component no network failure snapshot 1`] = `
</ExperimentProvider>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
<FooterSlot />
<ZendeskFab />
</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"
/>
<script
src="undefined/optimizelyjs/fakeId.js"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</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"
/>
<script
src="fake.url"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</main>
</AppWrapper>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>
@@ -67,6 +135,10 @@ exports[`App router component component refresh failure snapshot 1`] = `
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<div>
<AppWrapper>
@@ -81,9 +153,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
</Alert>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<FooterSlot />
<ZendeskFab />
</div>
</Fragment>

View File

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

View File

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

View File

@@ -1,17 +0,0 @@
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

@@ -1,16 +0,0 @@
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,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import useNoticesWrapperData from './hooks';
import NoticesWrapper from '.';
@@ -22,13 +22,15 @@ describe('NoticesWrapper component', () => {
it('does not show children if redirected', () => {
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
expect(el.children().length).toEqual(0);
expect(el.instance.children.length).toEqual(0);
});
it('shows children if not redirected', () => {
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
expect(el.children().length).toEqual(2);
expect(el.children().at(0).matchesElement(children[0])).toEqual(true);
expect(el.children().at(1).matchesElement(children[1])).toEqual(true);
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

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

View File

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

View File

@@ -1,11 +0,0 @@
// 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

@@ -17,6 +17,7 @@ const configuration = {
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',
};
const features = {};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { htmlProps } from 'data/constants/htmlKeys';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import useActionDisabledState from '../hooks';
@@ -59,10 +58,10 @@ describe('BeginCourseButton', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be disabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
expect(wrapper.instance.props.disabled).toEqual(true);
});
});
describe('enabled', () => {
@@ -70,13 +69,13 @@ describe('BeginCourseButton', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be enabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.instance.props.disabled).toEqual(false);
});
it('should track enter course clicked event on click, with exec ed param', () => {
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
props.cardId,
homeUrl + execEdPath(props.cardId),

View File

@@ -1,6 +1,5 @@
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { htmlProps } from 'data/constants/htmlKeys';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import useActionDisabledState from '../hooks';
@@ -57,10 +56,10 @@ describe('ResumeButton', () => {
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be disabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
expect(wrapper.instance.props.disabled).toEqual(true);
});
});
describe('enabled', () => {
@@ -68,13 +67,13 @@ describe('ResumeButton', () => {
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be enabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.instance.props.disabled).toEqual(false);
});
it('should track enter course clicked event on click, with exec ed param', () => {
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
props.cardId,
resumeUrl + execEdPath(props.cardId),

View File

@@ -1,7 +1,6 @@
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import { htmlProps } from 'data/constants/htmlKeys';
import useActionDisabledState from '../hooks';
import SelectSessionButton from './SelectSessionButton';
@@ -19,17 +18,17 @@ let wrapper;
describe('SelectSessionButton', () => {
const props = { cardId: 'cardId' };
it('default render', () => {
expect(wrapper).toMatchSnapshot();
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.prop(htmlProps.onClick).getMockName()).toEqual(
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(false);
expect(wrapper.instance.props.onClick.getMockName()).toEqual(
reduxHooks.useUpdateSelectSessionModalCallback().getMockName(),
);
});
test('disabled states', () => {
useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true });
expect(wrapper).toMatchSnapshot();
wrapper = shallow(<SelectSessionButton {...props} />);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
});
});

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Locked } from '@edx/paragon/icons';
import { Locked } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';

View File

@@ -1,8 +1,7 @@
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import { htmlProps } from 'data/constants/htmlKeys';
import useActionDisabledState from '../hooks';
import UpgradeButton from './UpgradeButton';
@@ -32,9 +31,9 @@ describe('UpgradeButton', () => {
describe('snapshot', () => {
test('can upgrade', () => {
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(false);
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
track.course.upgradeClicked,
props.cardId,
upgradeUrl,
@@ -43,8 +42,8 @@ describe('UpgradeButton', () => {
test('cannot upgrade', () => {
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
});
});
});

View File

@@ -1,7 +1,6 @@
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import track from 'tracking';
import { htmlProps } from 'data/constants/htmlKeys';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ViewCourseButton from './ViewCourseButton';
@@ -29,18 +28,18 @@ const homeUrl = 'homeUrl';
describe('ViewCourseButton', () => {
test('learner can view course', () => {
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
track.course.enterCourseClicked,
defaultProps.cardId,
homeUrl,
));
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
expect(wrapper.instance.props.disabled).toEqual(false);
});
test('learner cannot view course', () => {
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
});
});

View File

@@ -1,8 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectSessionButton default render 1`] = `undefined`;
exports[`SelectSessionButton disabled states 1`] = `
exports[`SelectSessionButton default render 1`] = `
<ActionButton
disabled={false}
onClick={[MockFunction mockOpenSessionModal]}
@@ -10,3 +8,12 @@ exports[`SelectSessionButton disabled states 1`] = `
Select Session
</ActionButton>
`;
exports[`SelectSessionButton disabled states 1`] = `
<ActionButton
disabled={true}
onClick={[MockFunction mockOpenSessionModal]}
>
Select Session
</ActionButton>
`;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ActionRow } from '@edx/paragon';
import { ActionRow } from '@openedx/paragon';
import { reduxHooks } from 'hooks';

View File

@@ -1,6 +1,6 @@
import { StrictDict } from 'utils';
import { defineMessages } from '@edx/frontend-platform/i18n';
export const messages = StrictDict({
const messages = defineMessages({
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 '@edx/paragon';
import { CheckCircle } from '@edx/paragon/icons';
import { MailtoLink, Hyperlink } from '@openedx/paragon';
import { CheckCircle } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from 'hooks';
@@ -26,17 +26,14 @@ export const CertificateBanner = ({ cardId }) => {
const { formatMessage } = useIntl();
const formatDate = useFormatDate();
const emailLink = address => address && <MailtoLink to={address}>{address}</MailtoLink>;
const emailLink = address => <MailtoLink to={address}>{address}</MailtoLink>;
if (certificate.isRestricted) {
return (
<Banner variant="danger">
{formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) })}
{ supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
{isVerified && ' '}
{isVerified && formatMessage(
messages.certRefundContactBilling,
{ billingEmail: emailLink(billingEmail) },
)}
{isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))}
</Banner>
);
}

View File

@@ -1,4 +1,4 @@
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import CertificateBanner from './CertificateBanner';
@@ -21,10 +21,6 @@ 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',
@@ -42,16 +38,19 @@ 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 **/
@@ -62,7 +61,29 @@ describe('CertificateBanner', () => {
isRestricted: true,
},
});
expect(wrapper).toMatchSnapshot();
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();
});
test('is restricted and verified', () => {
const wrapper = createWrapper({
@@ -73,21 +94,64 @@ describe('CertificateBanner', () => {
isVerified: true,
},
});
expect(wrapper).toMatchSnapshot();
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).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: false },
certificate: { isDownloadable: true },
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and audit', () => {
const wrapper = createWrapper({
@@ -95,17 +159,17 @@ describe('CertificateBanner', () => {
isAudit: true,
},
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and has finished', () => {
const wrapper = createWrapper({
courseRun: { isArchived: true },
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and not audit and not finished', () => {
const wrapper = createWrapper({});
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and is earned but unavailable', () => {
const wrapper = createWrapper({
@@ -116,7 +180,7 @@ describe('CertificateBanner', () => {
isEarnedButUnavailable: true,
},
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and not downloadable render empty', () => {
const wrapper = createWrapper({
@@ -124,7 +188,7 @@ describe('CertificateBanner', () => {
isPassing: true,
},
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
});
describe('behavior', () => {
@@ -133,8 +197,12 @@ describe('CertificateBanner', () => {
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
const bannerMessage = wrapper.instance.findByType('format-message-function').map(el => el.props.message.defaultMessage).join('\n');
expect(bannerMessage).toEqual(messages.certRestricted.defaultMessage);
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
});
@@ -146,8 +214,12 @@ describe('CertificateBanner', () => {
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
const bannerMessage = wrapper.instance.findByType('format-message-function').map(el => el.props.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 '@edx/paragon';
import { Hyperlink } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from 'hooks';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Hyperlink } from '@edx/paragon';
import { shallow } from '@edx/react-unit-test-utils';
import { Hyperlink } from '@openedx/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).toMatchSnapshot();
expect(el.snapshot).toMatchSnapshot();
});
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.text()).toContain(messages.upgradeToAccess.defaultMessage);
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.instance.children[0].children[2].el).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).toMatchSnapshot();
expect(el.snapshot).toMatchSnapshot();
});
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.find(Hyperlink).text()).toEqual(messages.findAnotherCourse.defaultMessage);
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);
});
});
describe('course run active and cannot upgrade', () => {
@@ -94,31 +94,31 @@ describe('CourseBanner', () => {
render({ courseRun: { isActive: true } });
});
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
expect(el).toMatchSnapshot();
expect(el.snapshot).toMatchSnapshot();
});
test('messages: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
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);
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);
});
});
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.html()).toEqual('');
expect(el.instance.children).toEqual([]);
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
expect(el.html()).toEqual('');
expect(el.instance.children).toEqual([]);
});
describe('unmet prerequisites', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
});
test('snapshot: unmetPrerequisites', () => {
expect(el).toMatchSnapshot();
expect(el.snapshot).toMatchSnapshot();
});
test('messages: prerequisitesNotMet', () => {
expect(el.text()).toContain(messages.prerequisitesNotMet.defaultMessage);
expect(el.instance.children[0].children[0].el).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).toMatchSnapshot());
test('messages', () => expect(el.text()).toEqual(''));
test('snapshot', () => expect(el.snapshot).toMatchSnapshot());
test('messages', () => expect(el.instance.children).toEqual([]));
});
describe('has start date', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } } });
});
test('snapshot', () => expect(el).toMatchSnapshot());
test('snapshot', () => expect(el.snapshot).toMatchSnapshot());
test('messages: courseHasNotStarted', () => {
expect(el.text()).toContain(
expect(el.instance.children[0].children[0].el).toContain(
formatMessage(messages.courseHasNotStarted, { startDate: courseRunData.startDate }),
);
});
@@ -147,7 +147,7 @@ describe('CourseBanner', () => {
render({ enrollment: { coursewareAccess: { isStaff: true } } });
});
test('snapshot: isStaff', () => {
expect(el).toMatchSnapshot();
expect(el.snapshot).toMatchSnapshot();
});
});
test('snapshot: stacking banners', () => {
@@ -160,6 +160,6 @@ describe('CourseBanner', () => {
},
},
});
expect(el).toMatchSnapshot();
expect(el.snapshot).toMatchSnapshot();
});
});

View File

@@ -6,6 +6,7 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
>
<p
className="credit-error-msg"
data-testid="credit-error-msg"
>
<format-message-function
message={
@@ -17,9 +18,11 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
}
values={
Object {
"supportEmailLink": <EmailLink
address="test-support-email"
/>,
"supportEmailLink": <MailtoLink
to="test-support-email"
>
test-support-email
</MailtoLink>,
}
}
/>
@@ -30,6 +33,22 @@ 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,13 +15,14 @@ export const CreditBanner = ({ cardId }) => {
if (hookData === null) {
return null;
}
const { ContentComponent, error, supportEmail } = hookData;
const supportEmailLink = (<EmailLink address={supportEmail} />);
const supportEmailLink = (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>);
return (
<Banner {...(error && { variant: 'danger' })}>
{error && (
<p className="credit-error-msg">
{formatMessage(messages.error, { supportEmailLink })}
<p className="credit-error-msg" data-testid="credit-error-msg">
{supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)}
</p>
)}
<ContentComponent cardId={cardId} />

View File

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

View File

@@ -1,11 +1,16 @@
import { StrictDict } from 'utils';
import { defineMessages } from '@edx/frontend-platform/i18n';
export const messages = StrictDict({
const messages = defineMessages({
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

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from 'testUtils';
@@ -45,21 +45,21 @@ describe('MustRequestContent component', () => {
describe('render', () => {
describe('rendered CreditContent component', () => {
beforeEach(() => {
component = el.find('CreditContent');
component = el.instance.findByType('CreditContent');
});
test('action.onClick calls createCreditRequest from useCreditRequestData hook', () => {
expect(component.props().action.onClick).toEqual(createCreditRequest);
expect(component[0].props.action.onClick).toEqual(createCreditRequest);
});
test('action.message is formatted requestCredit message', () => {
expect(component.props().action.message).toEqual(
expect(component[0].props.action.message).toEqual(
formatMessage(messages.requestCredit),
);
});
test('action.disabled is false', () => {
expect(component.props().action.disabled).toEqual(false);
expect(component[0].props.action.disabled).toEqual(false);
});
test('message is formatted mustRequest message', () => {
expect(component.props().message).toEqual(
expect(component[0].props.message).toEqual(
formatMessage(messages.mustRequest, {
linkToProviderSite: <ProviderLink cardId={cardId} />,
requestCredit: <b>{formatMessage(messages.requestCredit)}</b>,
@@ -67,7 +67,7 @@ describe('MustRequestContent component', () => {
);
});
test('requestData drawn from useCreditRequestData hook', () => {
expect(component.props().requestData).toEqual(requestData);
expect(component[0].props.requestData).toEqual(requestData);
});
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
@@ -40,21 +40,21 @@ describe('PendingContent component', () => {
describe('render', () => {
describe('rendered CreditContent component', () => {
beforeEach(() => {
component = el.find('CreditContent');
component = el.instance.findByType('CreditContent');
});
test('action.href will go to provider status site', () => {
expect(component.props().action.href).toEqual(providerStatusUrl);
expect(component[0].props.action.href).toEqual(providerStatusUrl);
});
test('action.message is formatted requestCredit message', () => {
expect(component.props().action.message).toEqual(
expect(component[0].props.action.message).toEqual(
formatMessage(messages.viewDetails),
);
});
test('action.disabled is false', () => {
expect(component.props().action.disabled).toEqual(false);
expect(component[0].props.action.disabled).toEqual(false);
});
test('message is formatted pending message', () => {
expect(component.props().message).toEqual(
expect(component[0].props.message).toEqual(
formatMessage(messages.received, { providerName }),
);
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
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.find('CreditContent'); };
const loadComponent = () => { component = el.instance.findByType('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.props().action).toEqual(undefined);
expect(component[0].props.action).toEqual(undefined);
});
test('message is formatted rejected message', () => {
expect(component.props().message).toEqual(formatMessage(
expect(component[0].props.message).toEqual(formatMessage(
messages.rejected,
{
linkToProviderSite: <ProviderLink cardId={cardId} />,

View File

@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ActionRow, Button } from '@edx/paragon';
import { ActionRow, Button } from '@openedx/paragon';
import CreditRequestForm from './CreditRequestForm';
export const CreditContent = ({ action, message, requestData }) => (
<>
<div className="message-copy credit-msg">
<div className="message-copy credit-msg" data-testid="credit-msg">
{message}
</div>
{action && (
@@ -21,6 +21,7 @@ export const CreditContent = ({ action, message, requestData }) => (
variant="outline-primary"
className="border-gray-400"
onClick={action.onClick}
data-testid="action-row-btn"
>
{action.message}
</Button>

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { keyStore } from 'utils';
@@ -46,19 +46,19 @@ describe('CreditRequestForm component', () => {
shallowRender(requestData);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
expect(el.snapshot).toMatchSnapshot();
});
it('loads Form with requestData url', () => {
expect(el.find('Form').props().action).toEqual(requestData.url);
expect(el.instance.findByType('Form')[0].props.action).toEqual(requestData.url);
});
it('loads a textarea form control for each requestData parameter', () => {
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);
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);
});
});
});

View File

@@ -4,7 +4,7 @@ import { render } from '@testing-library/react';
import useCreditRequestFormData from './hooks';
import CreditRequestForm from '.';
jest.unmock('@edx/paragon');
jest.unmock('@openedx/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 '@edx/paragon';
import { Hyperlink } from '@openedx/paragon';
export const ProviderLink = ({ cardId }) => {
const credit = reduxHooks.useCardCreditData(cardId);

View File

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

View File

@@ -4,6 +4,7 @@ exports[`CreditContent component render with action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>
@@ -13,6 +14,7 @@ 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]}
@@ -40,6 +42,7 @@ exports[`CreditContent component render without action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>

View File

@@ -1,6 +1,6 @@
import { StrictDict } from 'utils';
import { defineMessages } from '@edx/frontend-platform/i18n';
export const messages = StrictDict({
const messages = defineMessages({
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 '@edx/paragon';
import { Button, MailtoLink } from '@openedx/paragon';
import { utilHooks, reduxHooks } from 'hooks';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
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).toMatchSnapshot();
expect(el.snapshot).toMatchSnapshot();
});
test('snapshot: expiration warning', () => {
render({ entitlement: { showExpirationWarning: true } });
expect(el).toMatchSnapshot();
expect(el.snapshot).toMatchSnapshot();
});
test('no display if sessions available and not displaying warning', () => {
render();

View File

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

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Program } from '@edx/paragon/icons';
import { Program } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
@@ -15,16 +15,20 @@ export const RelatedProgramsBanner = ({ cardId }) => {
const programData = reduxHooks.useCardRelatedProgramsData(cardId);
if (!programData?.length) {
return null;
}
return (
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>
)
<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 = {

View File

@@ -1,4 +1,4 @@
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import RelatedProgramsBanner from '.';
@@ -37,6 +37,6 @@ describe('RelatedProgramsBanner', () => {
length: 2,
});
const el = shallow(<RelatedProgramsBanner cardId={cardId} />);
expect(el).toMatchSnapshot();
expect(el.snapshot).toMatchSnapshot();
});
});

View File

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

View File

@@ -15,26 +15,46 @@ exports[`CertificateBanner snapshot is passing and is earned but unavailable 1`]
</Banner>
`;
exports[`CertificateBanner snapshot is passing and not downloadable render empty 1`] = `""`;
exports[`CertificateBanner snapshot is passing and not downloadable render empty 1`] = `null`;
exports[`CertificateBanner snapshot is restricted 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
If you would like a refund on your Certificate of Achievement, please contact us.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with billing email 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
<format-message-function
message={
Object {
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
Object {
"supportEmail": <MailtoLink
to="suport@email"
"billingEmail": <MailtoLink
to="billing@email"
>
suport@email
billing@email
</MailtoLink>,
}
}
@@ -42,7 +62,7 @@ exports[`CertificateBanner snapshot is restricted 1`] = `
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified 1`] = `
exports[`CertificateBanner snapshot is restricted and verified with support and billing email 1`] = `
<Banner
variant="danger"
>
@@ -86,6 +106,66 @@ exports[`CertificateBanner snapshot is restricted and verified 1`] = `
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with support email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
Object {
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
Object {
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
If you would like a refund on your Certificate of Achievement, please contact us.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted with billing email 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted with support email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
Object {
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
Object {
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
</Banner>
`;
exports[`CertificateBanner snapshot not passing and audit 1`] = `
<Banner>
Grade required to pass the course: 0.8%

View File

@@ -1,4 +1,4 @@
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
@@ -22,11 +22,11 @@ describe('CourseCardBanners', () => {
};
test('renders default CourseCardBanners', () => {
const wrapper = shallow(<CourseCardBanners {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
test('render with isEnrolled false', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
const wrapper = shallow(<CourseCardBanners {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

@@ -1,6 +1,6 @@
import { StrictDict } from 'utils';
import { defineMessages } from '@edx/frontend-platform/i18n';
export const messages = StrictDict({
const messages = defineMessages({
auditAccessExpired: {
id: 'learner-dash.courseCard.banners.auditAccessExpired',
description: 'Audit access expiration banner message',
@@ -31,11 +31,21 @@ export const messages = StrictDict({
description: 'Restricted certificate warning message',
defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.',
},
certRestrictedNoEmail: {
id: 'learner-dash.courseCard.banners.certificateRestrictedNoEmail',
description: 'Restricted certificate warning message',
defaultMessage: 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.',
},
certRefundContactBilling: {
id: 'learner-dash.courseCard.banners.certificateRefundContactBilling',
description: 'Message to learners to contact billing for certificate refunds',
defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}',
},
certRefundContactBillingNoEmail: {
id: 'learner-dash.courseCard.banners.certificateRefundContactBillingNoEmail',
description: 'Message to learners to contact billing for certificate refunds',
defaultMessage: 'If you would like a refund on your Certificate of Achievement, please contact us.',
},
passingGrade: {
id: 'learner-dash.courseCard.banners.passingGrade',
description: 'Message to learners with minimum passing grade for the course',

View File

@@ -1,54 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCard Details component does not have change session button on regular course 1`] = `
<span
className="small"
data-testid="CourseCardDetails"
>
provider-name
test-course-number
• access-message
</span>
<div>
<span
class="small"
data-testid="CourseCardDetails"
>
provider-name
test-course-number
• access-message
</span>
</div>
`;
exports[`CourseCard Details component has change session button on entitlement course 1`] = `
<span
className="small"
data-testid="CourseCardDetails"
>
provider-name
test-course-number
• access-message
<Button
className="m-0 p-0"
onClick={[MockFunction useSelectSession.openSessionModal]}
size="inline"
variant="link"
<div>
<span
class="small"
data-testid="CourseCardDetails"
>
change-or-leave-session-message
</Button>
</span>
provider-name
test-course-number
• access-message
<button
class="m-0 p-0"
variant="link"
>
change-or-leave-session-message
</button>
</span>
</div>
`;
exports[`CourseCard Details component has change session button on entitlement course but no access message 1`] = `
<span
className="small"
data-testid="CourseCardDetails"
>
provider-name
test-course-number
<Button
className="m-0 p-0"
onClick={[MockFunction useSelectSession.openSessionModal]}
size="inline"
variant="link"
<div>
<span
class="small"
data-testid="CourseCardDetails"
>
change-or-leave-session-message
</Button>
</span>
provider-name
test-course-number
<button
class="m-0 p-0"
variant="link"
>
change-or-leave-session-message
</button>
</span>
</div>
`;

View File

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

View File

@@ -1,5 +1,5 @@
@import "~@edx/brand/paragon/variables";
@import "~@edx/paragon/scss/core/core";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
a.course-card-title {

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { render } from '@testing-library/react';
import CourseCardDetails from '.';
@@ -28,27 +28,44 @@ describe('CourseCard Details component', () => {
...defaultHooks,
...hookOverrides,
});
return shallow(<CourseCardDetails cardId={cardId} />);
return render(<CourseCardDetails cardId={cardId} />);
};
const fetchSeparators = (wrapper) => {
const elements = wrapper.container.querySelectorAll('*');
let separatorsCount = 0;
elements.forEach((element) => {
// Use a regular expression to find all occurrences of '•' in the text content
const separatorMatches = element.textContent.match(/•/g);
// If matches are found, add the count to the total
if (separatorMatches) {
separatorsCount += separatorMatches.length;
}
});
return separatorsCount;
};
test('has change session button on entitlement course', () => {
const wrapper = createWrapper();
expect(wrapper).toMatchSnapshot();
expect(wrapper.container).toMatchSnapshot();
// it has 3 separator, 4 column
expect(wrapper.text().match(/•/g)).toHaveLength(3);
expect(fetchSeparators(wrapper)).toBe(3);
});
test('has change session button on entitlement course but no access message', () => {
const wrapper = createWrapper({ accessMessage: null });
expect(wrapper).toMatchSnapshot();
expect(wrapper.container).toMatchSnapshot();
// it has 2 separator, 3 column
expect(wrapper.text().match(/•/g)).toHaveLength(2);
expect(fetchSeparators(wrapper)).toBe(2);
});
test('does not have change session button on regular course', () => {
const wrapper = createWrapper({ isEntitlement: false });
expect(wrapper).toMatchSnapshot();
expect(wrapper.container).toMatchSnapshot();
// it has 2 separator, 3 column
expect(wrapper.text().match(/•/g)).toHaveLength(2);
expect(fetchSeparators(wrapper)).toBe(2);
});
});

View File

@@ -1,6 +1,6 @@
import { StrictDict } from 'utils';
import { defineMessages } from '@edx/frontend-platform/i18n';
export const messages = StrictDict({
const messages = defineMessages({
accessExpired: {
id: 'learner-dash.courseCard.CourseCardDetails.accessExpired',
description: 'Course access expiration date message on course card for expired access.',

View File

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

View File

@@ -1,4 +1,4 @@
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
@@ -36,9 +36,9 @@ describe('CourseCardImage', () => {
describe('snapshot', () => {
test('renders clickable link course Image', () => {
const wrapper = shallow(<CourseCardImage {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.type()).toBe('a');
expect(wrapper.prop('onClick')).toEqual(
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.type).toBe('a');
expect(wrapper.instance.props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(
track.course.courseImageClicked,
props.cardId,
@@ -49,8 +49,8 @@ describe('CourseCardImage', () => {
test('renders disabled link', () => {
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
const wrapper = shallow(<CourseCardImage {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.type()).toBe('div');
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.type).toBe('div');
});
});
describe('behavior', () => {

View File

@@ -4,7 +4,7 @@ import * as ReactShare from 'react-share';
import { StrictDict } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@edx/paragon';
import { Dropdown } from '@openedx/paragon';
import track from 'tracking';
import { reduxHooks } from 'hooks';

View File

@@ -21,6 +21,7 @@ jest.mock('tracking', () => ({
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn().mockReturnValue({
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
}),

View File

@@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown, Icon, IconButton } from '@edx/paragon';
import { MoreVert } from '@edx/paragon/icons';
import { Dropdown, Icon, IconButton } from '@openedx/paragon';
import { MoreVert } from '@openedx/paragon/icons';
import { StrictDict } from '@edx/react-unit-test-utils';
import EmailSettingsModal from 'containers/EmailSettingsModal';

View File

@@ -1,6 +1,6 @@
import { when } from 'jest-when';
import { Dropdown } from '@edx/paragon';
import { Dropdown } from '@openedx/paragon';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -12,6 +12,7 @@ import * as hooks from './hooks';
import CourseCardMenu, { testIds } from '.';
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn().mockReturnValue({
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
}),

View File

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

View File

@@ -1,4 +1,4 @@
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
@@ -34,10 +34,10 @@ describe('CourseCardTitle', () => {
describe('snapshot', () => {
test('renders clickable link course title', () => {
const wrapper = shallow(<CourseCardTitle {...props} />);
expect(wrapper).toMatchSnapshot();
const title = wrapper.find('.course-card-title');
expect(title.type()).toBe('a');
expect(title.prop('onClick')).toEqual(
expect(wrapper.snapshot).toMatchSnapshot();
const title = wrapper.instance.findByTestId('CourseCardTitle');
expect(title[0].type).toBe('a');
expect(title[0].props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(
track.course.courseTitleClicked,
props.cardId,
@@ -48,10 +48,10 @@ describe('CourseCardTitle', () => {
test('renders disabled link', () => {
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
const wrapper = shallow(<CourseCardTitle {...props} />);
expect(wrapper).toMatchSnapshot();
const title = wrapper.find('.course-card-title');
expect(title.type()).toBe('span');
expect(title.prop('onClick')).toBeUndefined();
expect(wrapper.snapshot).toMatchSnapshot();
const title = wrapper.instance.findByTestId('CourseCardTitle');
expect(title[0].type).toBe('span');
expect(title[0].props.onClick).toBeUndefined();
});
});
describe('behavior', () => {

View File

@@ -2,21 +2,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Icon } from '@edx/paragon';
import { Program } from '@edx/paragon/icons';
import { Button, Icon } from '@openedx/paragon';
import { Program } from '@openedx/paragon/icons';
import RelatedProgramsBadgeModal from 'containers/RelatedProgramsModal';
import useRelatedProgramsBadgeData from './hooks';
export const RelatedProgramsBadge = ({ cardId }) => {
const {
isOpen,
openModal,
closeModal,
numPrograms,
programsMessage,
isOpen, openModal, closeModal, numPrograms, programsMessage,
} = useRelatedProgramsBadgeData({ cardId });
return (numPrograms > 0) && (
if (numPrograms === 0) {
return null;
}
return (
<>
<Button
data-testid="RelatedProgramsBadge"

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import useRelatedProgramsBadge from './hooks';
import RelatedProgramsBadge from '.';
@@ -25,6 +25,6 @@ describe('RelatedProgramsBadge component', () => {
});
test('snapshot: 3 programs', () => {
useRelatedProgramsBadge.mockReturnValueOnce(hookProps);
expect(shallow(<RelatedProgramsBadge cardId={cardId} />)).toMatchSnapshot();
expect(shallow(<RelatedProgramsBadge cardId={cardId} />).snapshot).toMatchSnapshot();
});
});

View File

@@ -15,27 +15,29 @@ exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
}
tabIndex="-1"
>
<img
alt="Course thumbnail"
className="pgn__card-image-cap show"
src="banner-img-src"
/>
<span
className="course-card-verify-ribbon-container"
title="You're enrolled as a verified student"
>
<Badge
as="div"
className="w-100"
variant="success"
>
Verified
</Badge>
<Fragment>
<img
alt="ID Verified Ribbon/Badge"
src="test-file-stub"
alt="Course thumbnail"
className="pgn__card-image-cap show"
src="banner-img-src"
/>
</span>
<span
className="course-card-verify-ribbon-container"
title="You're enrolled as a verified student"
>
<Badge
as="div"
className="w-100"
variant="success"
>
Verified
</Badge>
<img
alt="ID Verified Ribbon/Badge"
src="test-file-stub"
/>
</span>
</Fragment>
</a>
`;
@@ -43,26 +45,28 @@ exports[`CourseCardImage snapshot renders disabled link 1`] = `
<div
className="pgn__card-wrapper-image-cap overflow-visible orientation"
>
<img
alt="Course thumbnail"
className="pgn__card-image-cap show"
src="banner-img-src"
/>
<span
className="course-card-verify-ribbon-container"
title="You're enrolled as a verified student"
>
<Badge
as="div"
className="w-100"
variant="success"
>
Verified
</Badge>
<Fragment>
<img
alt="ID Verified Ribbon/Badge"
src="test-file-stub"
alt="Course thumbnail"
className="pgn__card-image-cap show"
src="banner-img-src"
/>
</span>
<span
className="course-card-verify-ribbon-container"
title="You're enrolled as a verified student"
>
<Badge
as="div"
className="w-100"
variant="success"
>
Verified
</Badge>
<img
alt="ID Verified Ribbon/Badge"
src="test-file-stub"
/>
</span>
</Fragment>
</div>
`;

View File

@@ -1,5 +1,5 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { useWindowSize, breakpoints } from '@edx/paragon';
import { useWindowSize, breakpoints } from '@openedx/paragon';
import { reduxHooks } from 'hooks';
export const useIsCollapsed = () => {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Card } from '@edx/paragon';
import { Card } from '@openedx/paragon';
import { useIsCollapsed } from './hooks';
import CourseCardBanners from './components/CourseCardBanners';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import CourseCard from '.';
import hooks from './hooks';
@@ -20,10 +20,10 @@ const cardId = 'test-card-id';
describe('CourseCard component', () => {
test('snapshot: collapsed', () => {
hooks.useIsCollapsed.mockReturnValueOnce(true);
expect(shallow(<CourseCard cardId={cardId} />)).toMatchSnapshot();
expect(shallow(<CourseCard cardId={cardId} />).snapshot).toMatchSnapshot();
});
test('snapshot: not collapsed', () => {
hooks.useIsCollapsed.mockReturnValueOnce(false);
expect(shallow(<CourseCard cardId={cardId} />)).toMatchSnapshot();
expect(shallow(<CourseCard cardId={cardId} />).snapshot).toMatchSnapshot();
});
});

View File

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

View File

@@ -2,18 +2,19 @@ import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Chip } from '@edx/paragon';
import { CloseSmall } from '@edx/paragon/icons';
import { Button, Chip } from '@openedx/paragon';
import { CloseSmall } from '@openedx/paragon/icons';
import { reduxHooks } from 'hooks';
import messages from './messages';
import './index.scss';
export const ActiveCourseFilters = ({
filters,
setFilters,
handleRemoveFilter,
}) => {
const { formatMessage } = useIntl();
const clearFilters = reduxHooks.useClearFilters();
return (
<div id="course-list-active-filters">
{filters.map(filter => (
@@ -25,7 +26,7 @@ export const ActiveCourseFilters = ({
{formatMessage(messages[filter])}
</Chip>
))}
<Button variant="link" onClick={setFilters.clear}>
<Button variant="link" onClick={clearFilters}>
{formatMessage(messages.clearAll)}
</Button>
</div>
@@ -33,10 +34,6 @@ export const ActiveCourseFilters = ({
};
ActiveCourseFilters.propTypes = {
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
setFilters: PropTypes.shape({
remove: PropTypes.func,
clear: PropTypes.func,
}).isRequired,
handleRemoveFilter: PropTypes.func.isRequired,
};

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