Compare commits

...

297 Commits

Author SHA1 Message Date
Adolfo R. Brandes
021f52b303 feat: remove upgrade button
This removes the upgrade button as default content in CourseCardActionSlot.

This is, for the moment, a Sumac-specific change.  The button is still
present in master, pending proper removal (including the component
definitions from the rest of the repository) prior to the U release by
the maintainers.  See:

https://github.com/openedx/frontend-app-learner-dashboard/issues/438
2024-12-06 15:55:31 -03:00
Adolfo R. Brandes
8e0e217402 fix: Use browserslist-config
We were installing browserslist-config but not declaring it.  This had
the effect that webpack - and likely others - were not using it.
2024-12-06 11:08:23 -03:00
Diana Olarte
261448dee9 fix: display programs tab only if it is configured
fix: apply feedback
2024-12-02 07:50:28 -08:00
Diana Olarte
5ef5ed954c fix: display SUPPORT_URL only if the url is configured 2024-12-02 07:50:28 -08:00
Brian Smith
08d47dd9f1 feat(deps): update header to 5.6.0 (#485) 2024-10-22 19:19:30 -04:00
Jason Wesson
f250efb660 docs: improve the image example for course card slot 2024-10-21 13:01:43 -06:00
Jason Wesson
c144c04aee fix: modify tests for course card and import paths 2024-10-21 13:01:43 -06:00
Jason Wesson
0a52025a99 feat: add plugin slot for course card action 2024-10-21 13:01:43 -06:00
renovate[bot]
e4e02d4da2 fix(deps): update dependency @openedx/paragon to v22.9.0 2024-10-21 04:16:50 +00:00
Maxwell Frank
0408a54372 refactor: plugin slot implementation (#465) 2024-10-15 15:04:52 -04:00
renovate[bot]
134c741cf8 fix(deps): update dependency react-router-dom to v6.27.0 2024-10-14 11:06:00 +00:00
renovate[bot]
756e85f046 fix(deps): update dependency react-intl to v6.8.0 2024-10-14 06:53:38 +00:00
renovate[bot]
8b532aa49a fix(deps): update dependency @edx/frontend-platform to v8.1.2 2024-10-14 04:09:09 +00:00
renovate[bot]
cc544e4591 fix(deps): update dependency @openedx/paragon to v22.8.1 2024-10-07 09:26:22 +00:00
renovate[bot]
1bd6f71ac1 fix(deps): update dependency @edx/frontend-component-header to v5.5.0 2024-10-07 06:22:31 +00:00
renovate[bot]
8914c7f4cc fix(deps): update dependency dompurify to v2.5.7 2024-09-30 10:40:04 +00:00
renovate[bot]
636216c5d3 chore(deps): update dependency @openedx/frontend-build to v14.1.5 2024-09-30 06:25:59 +00:00
renovate[bot]
a174abbc09 fix(deps): update dependency react-router-dom to v6.26.2 2024-09-23 08:10:12 +00:00
renovate[bot]
5134f8f85b chore(deps): update dependency @openedx/frontend-build to v14.1.4 2024-09-23 05:01:59 +00:00
edX requirements bot
1007dc40fb chore: enable github action auto update in dependabot.yml (#457)
Co-authored-by: Maxwell Frank <92897870+MaxFrank13@users.noreply.github.com>
2024-09-17 09:13:17 -04:00
renovate[bot]
767596301a chore(deps): update dependency react-dev-utils to v12 (#443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 13:38:42 -04:00
renovate[bot]
d76d13bcc2 fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.5 2024-09-16 08:10:12 +00:00
renovate[bot]
bd495e98ee chore(deps): update dependency husky to v9.1.6 2024-09-16 04:43:31 +00:00
Jason Wesson
2f8ff3b517 test: update jest snapshots 2024-09-13 08:46:23 -07:00
Jason Wesson
629de04289 feat: remove Zendesk component from App 2024-09-13 08:46:23 -07:00
Justin Hynes
b4b3d0718d build: Switch to ubuntu-latest for builds (#454) 2024-09-09 14:29:47 -04:00
Feanil Patel
ed7a3ffdbc build: Switch to ubuntu-latest for builds
This code does not have any dependencies that are specific to any specific
version of ubuntu.  So instead of testing on a specific version and then needing
to do work to keep the versions up-to-date, we switch to the ubuntu-latest
target which should be sufficient for testing purposes.

This work is being done as a part of https://github.com/openedx/platform-roadmap/issues/377

closes https://github.com/openedx/frontend-app-learner-dashboard/issues/420
2024-09-09 10:00:38 -04:00
renovate[bot]
0cfebb6976 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.3.0 2024-09-09 06:47:17 +00:00
renovate[bot]
48e2c72180 chore(deps): update dependency @openedx/frontend-build to v14.1.2 2024-09-09 05:07:35 +00:00
renovate[bot]
3ce54cfc4a fix(deps): update dependency core-js to v3.38.1 2024-09-02 06:50:10 +00:00
renovate[bot]
8969d011ff chore(deps): update dependency @openedx/frontend-build to v14.1.1 2024-09-02 04:21:13 +00:00
Deborah Kaplan
8fd6f2c7dc chore: removing an unnecessary import (#440) 2024-08-29 09:00:08 -04:00
Deborah Kaplan
a2041bfc11 chore: removing an unnecessary import
removing an unnecessary import

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

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

See https://github.com/openedx/wg-frontend/issues/179
2024-06-17 12:16:53 -03:00
renovate[bot]
a2ccda7b30 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.2.1 2024-06-17 10:40:26 +00:00
renovate[bot]
54cf4bb8fd chore(deps): update dependency @openedx/frontend-build to v14.0.9 2024-06-17 06:48:46 +00:00
Hunia Fatima
229436cddf perf: version check workflow for lockfile updated 2024-06-12 17:28:00 +05:00
Adolfo R. Brandes
e94dd56fb1 fix: Remove edX-specific reference
Remove edx-Specific reference from email confirmation banner.
2024-06-10 11:34:48 -03:00
renovate[bot]
3becef3468 fix(deps): update dependency @edx/openedx-atlas to v0.6.1 2024-06-10 10:22:25 +00:00
renovate[bot]
3570ead725 fix(deps): update dependency @edx/frontend-platform to v8.0.4 2024-06-10 07:19:45 +00:00
Bilal Qamar
17c5fd09f9 feat: updated frontend-build & frontend-platform major versions (#256) 2024-05-30 17:14:41 +05:00
Bilal Qamar
506f8f795f Merge branch master into bilalqamar95/jest-v29-upgrade 2024-05-30 17:09:44 +05:00
renovate[bot]
e73880b442 fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.2 2024-05-27 04:46:08 +00:00
Bilal Qamar
08050f458e Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2024-05-20 16:30:16 +05:00
renovate[bot]
75f19a28b7 fix(deps): update dependency core-js to v3.37.1 2024-05-20 11:12:50 +00:00
Bilal Qamar
ecc4c4c2e0 refactor: update package-lock 2024-05-20 16:08:06 +05:00
Bilal Qamar
28da100ca2 Merge branch master into bilalqamar95/jest-v29-upgrade 2024-05-20 16:07:50 +05:00
renovate[bot]
0fbac0715d fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.1 2024-05-20 07:01:59 +00:00
Adolfo R. Brandes
49f9e6e424 feat: use frontend-plugin-framework to provide a FooterSlot (#345) 2024-05-17 13:05:30 -03:00
Brian Smith
17eff1da7b feat: use frontend-plugin-framework to provide a FooterSlot 2024-05-17 10:54:53 -04:00
Muhammad Abdullah Waheed
0fcaa64a6e fix: fixed snapshots 2024-05-17 16:54:08 +05:00
Bilal Qamar
506d3f655c refactor: updated version for frontend-plugin-framework 2024-05-17 16:35:59 +05:00
Bilal Qamar
37bb54d28e refactor: updated package-lock 2024-05-16 16:42:33 +05:00
Bilal Qamar
1d53ef6153 refactor: react-unit-test-util & hotjar major version upgrades, updated snapshots 2024-05-16 16:19:50 +05:00
Bilal Qamar
59d90f5dc8 Merge branch master into bilalqamar95/jest-v29-upgrade 2024-05-16 16:19:13 +05:00
renovate[bot]
61d881b1cd fix(deps): update dependency core-js to v3.37.0 2024-05-13 09:08:02 +00:00
renovate[bot]
77adb60167 fix(deps): update dependency classnames to v2.5.1 2024-05-13 06:08:29 +00:00
renovate[bot]
b2b199e0e2 fix(deps): update dependency @openedx/paragon to v22.4.0 2024-05-13 04:07:20 +00:00
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
Bilal Qamar
92f712d670 feat: updated build and platform major versions, along with edx packages 2024-04-24 14:10:11 +05: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
Bilal Qamar
11b7e48080 refactor: updated snapshots for failing tests 2024-04-04 16:33:13 +05:00
Bilal Qamar
a051d712dc refactor: updated frontend-build 2024-04-04 16:32:59 +05:00
Bilal Qamar
8c76b5c689 Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2024-04-04 16:28:33 +05:00
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
Bilal Qamar
7fccd94d6b Merge branch 'master' into bilalqamar95/jest-v29-upgrade 2023-12-22 16:07:09 +05:00
Maria Grimaldi
bed2c341b5 refactor: hide switch dashboard option behind flag (#237) 2023-12-07 09:32:19 -05:00
Bilal Qamar
c405bc63ea chore: bumped jest to v29 2023-12-05 16:17:36 +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
Syed Ali Abbas Zaidi
3cdcc1fe61 chore: bump frontend-platform (#216) 2023-10-16 13:09:55 +05:00
Shahbaz Shabbir
82ff0d7ddb fix: get brand logo file path from env (#205)
Co-authored-by: Brian Smith <112954497+brian-smith-tcril@users.noreply.github.com>
2023-10-12 14:06:01 -04:00
Abdullah Waheed
7375c8f27b fix: upgraded frontend-build to fix security issue 2023-10-10 18:29:42 +05:00
Jenkins
1478956e34 chore(i18n): update translations 2023-10-08 12:47:23 -04:00
Abdullah Waheed
0cf98c9b78 feat: babel-plugin-react-intl to babel-plugin-formatjs migration 2023-10-04 19:55:38 +05:00
Syed Ali Abbas Zaidi
f049712430 feat: upgrade react router to v6 (#126)
Co-authored-by: Matthew Carter <mcarter@edx.org>
2023-10-03 15:10:27 -04:00
Juliana Kang
c977de2df9 feat: Update header user dropdown back to Order History (#208)
REV-3693
2023-10-02 15:20:43 -04:00
Juliana Kang
4b20c5bbdd Revert "Revert "feat: Update header user dropdown back to Order History"" 2023-09-18 14:14:21 -04:00
Juliana Kang
0c1fa2f030 Revert "feat: Update header user dropdown back to Order History" (#207)
REV-3693
2023-09-18 14:13:47 -04:00
Juliana Kang
91117cce6a Revert "feat: Update header user dropdown back to Order History" 2023-09-18 13:52:13 -04:00
Juliana Kang
e6dba8bdc2 feat: Update header user dropdown back to Order History (#206)
REV-3693
2023-09-18 10:34:49 -04:00
julianajlk
1d67ac5f24 test: Update snapshot for Order History 2023-09-15 12:27:05 -04:00
julianajlk
60d2f22c50 feat: Update header user dropdown back to Order History 2023-09-15 12:12:07 -04:00
Syed Sajjad Hussain Shah
5dc89d7404 fix: collapsed navbar icon fix (#204)
Co-authored-by: Matthew Carter <mcarter@edx.org>
2023-09-07 17:11:15 -04:00
Zainab Amir
0f24d3a52d fix: recommendations card design and painted door eventing (#203) 2023-09-06 19:26:21 +05:00
Syed Sajjad Hussain Shah
fc885d02dc fix: recommendations card design and painted door eventing 2023-09-06 17:22:27 +05:00
Syed Sajjad Hussain Shah
2e09d3632e feat: add painted door button for no recommendations (#198) 2023-09-01 10:50:56 +05:00
Syed Sajjad Hussain Shah
d8cb46da60 Merge branch 'master' into sajjad/VAN-1618 2023-09-01 10:32:16 +05:00
Mashal Malik
199d6e7c60 feat: update react & react-dom to v17 (#161) 2023-08-28 10:31:57 -04:00
mubbsharanwar
64563d58f9 fix: update dashboard recommendations url
Point cross product recommendations url from learner_recommendations to edx-recommendations plugin.

VAN-1596
2023-08-28 11:50:41 +05:00
Jenkins
1e9a0a87b6 chore(i18n): update translations 2023-08-27 12:47:12 -04:00
Syed Sajjad Hussain Shah
d42d0cdc59 feat: add painted door button for no recommendations
VAN-1618
2023-08-24 13:30:39 +05:00
Mubbshar Anwar
8fef92d94d fix: update dashboard recommendations url (#195)
Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
Co-authored-by: Matthew Carter <mcarter@edx.org>
2023-08-24 11:12:39 +05:00
Ben Warzeski
b41eee47c9 Bw/recommendations painted door exp (#197)
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2023-08-23 11:53:19 -04:00
Ben Warzeski
909f3f1f47 Bw/fix email modal (#193) 2023-08-22 15:12:54 -04:00
Ben Warzeski
ce269e8c8f feat: Exec Education flag around course card menu and actions (#188)
Co-authored-by: jajjibhai008 <ejazofficial122@gmail.com>
2023-08-15 16:27:32 -04:00
Ejaz Ahmad
86a4573405 feat: show unenrollment button for executive education courses (#185) 2023-08-11 19:14:26 +05:00
jajjibhai008
be2258e409 feat: show unenrollment button for executive education courses 2023-08-11 12:46:28 +05:00
Ejaz Ahmad
be8cb85773 feat: frontend changes for executive education courses on B2C dashboard (#181) 2023-08-10 12:24:17 +05:00
jajjibhai008
a2c003e542 feat: frontend changes for executive education courses on B2C dashboard 2023-08-09 19:35:33 +05:00
Leangseu Kim
f1cfe3de68 chore: update email for ci workflow 2023-08-07 10:18:00 -04:00
Omar Al-Ithawi
d43c17a663 feat: include paragon in atlas pull (#179)
This pull request is part of the [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) which is sparked by the [Translation Infrastructure update OEP-58](https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0058-arch-translations-management.html#specification).
2023-07-25 11:19:35 -04:00
Mashal Malik
c01042f1df chore: add paragon messages (#172) 2023-07-21 11:16:34 +05:00
Jody Bailey
ed2368222f fix: removed info Optimizely logs (#177) 2023-07-17 14:57:57 +02:00
Jody Bailey
103a67654c feat: Implemented product recommendations experiment (#174) 2023-07-11 16:45:14 +02:00
Jenkins
58c3720087 chore(i18n): update translations 2023-07-09 12:46:59 -04:00
leangseu-edx
4e47018a81 fix: stop user from unenroll after earned the certificate (#162) 2023-07-06 13:30:36 -04:00
Jody Bailey
e7d9255fe5 fix: initial optimizely and segment events (#170) 2023-06-30 12:40:59 +02:00
Jody Bailey
2c7e10ffc2 fix: adjusted footer widget to show placeholder for no recommendations (#169) 2023-06-29 08:58:17 +02:00
Jody Bailey
43aa5b088e fix: course list styling for hidden panel and new endpoint integration (#165)
Co-authored-by: Ben Warzeski <bwarzesk@gmail.com>
2023-06-27 16:28:07 +02:00
Jenkins
86b1f5df1a chore(i18n): update translations 2023-06-23 06:55:17 -04:00
Raza Dar
5c52b6861e feat: Changed Order History header menu item title to Orders & Subscriptions (#164) 2023-06-21 12:18:41 +05:00
Raza Dar
a358a6014f test: removed the old flag code 2023-06-21 00:03:07 +05:00
Raza Dar
6ebc94506b feat: update removed flag check for this change 2023-06-20 17:30:44 +05:00
Raza Dar
59ab63807f feat: update the flag SUBSCRIPTIONS_ORDERS_MENU_ITEM_ENABLED 2023-06-20 17:30:44 +05:00
Raza Dar
322a79afaa feat: update Order History changed to Orders & Subscriptions 2023-06-20 17:30:44 +05:00
Jenkins
c458f4942f chore(i18n): update translations 2023-06-18 12:46:55 -04:00
Jody Bailey
93a4dfb4d9 feat: Added cross product recommendations experiment initial render + query logic (#158) 2023-06-15 15:06:32 +02:00
Ghassan Maslamani
f92bd9c8f9 fix: force LMS url to reload when changed (#136) 2023-06-13 12:24:23 -03:00
leangseu-edx
5db95b0029 Revert "fix: stop user from unenroll after earned the certificate"
This reverts commit a479b7ead6.
2023-06-12 11:40:39 -04:00
Leangseu Kim
a479b7ead6 fix: stop user from unenroll after earned the certificate 2023-06-12 10:07:28 -04:00
Kris Hatcher
e43a49b431 feat: add career link to user dropdown (#152)
Co-authored-by: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com>
2023-06-05 11:50:40 -04:00
Mashal Malik
4643e0b130 refactor: update lock version file (#154) 2023-05-31 17:49:23 +05:00
Bilal Qamar
8c29abd0c8 feat: upgraded to node v18, added .nvmrc and updated workflows (#151) 2023-05-24 12:56:20 +05:00
Jason Wesson
d44b123815 fix: parse creditRequest data correctly and add ecommerce URL to envs (#150) 2023-05-19 11:46:02 -07:00
Jason Wesson
8829f756d8 fix: change ecommerce url reference 2023-05-19 15:29:40 +00:00
Jason Wesson
176a803f94 Merge branch 'master' into jwesson/fix-purchase-credit 2023-05-18 11:22:33 -07:00
Jason Wesson
309a07ffa9 fix: add ecommerce url env to env.prod 2023-05-18 18:11:34 +00:00
Jason Wesson
e3784d36f1 refactor: extract data closer to origin of API request 2023-05-18 18:09:16 +00:00
Jason Wesson
5048fffd04 fix: parse creditRequest data correctly and add ecommerce URL to dev and test envs 2023-05-17 20:19:09 +00:00
Jenkins
5ca3036849 chore(i18n): update translations 2023-05-14 12:46:46 -04:00
Leangseu Kim
e57f44068b fix: missing image 2023-05-10 11:01:58 -04:00
leangseu-edx
a4d10b6c72 chore: refactor disable course action into a single hook (#145) 2023-05-09 14:30:44 -04:00
Omar Al-Ithawi
5769629250 feat: use atlas in make pull_translations (#137)
- Bump frontend-platform to bring intl-imports.js script
 - Move all i18n imports into `src/i18n/index.js` so intl-imports.js can override it with latest translations
 - Add `atlas` into `make pull_translations` when `OPENEDX_ATLAS_PULL` environment variable is set.

This pull request is part of the [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) which is sparked by the [Translation Infrastructure update OEP-58](https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0058-arch-translations-management.html#specification).
2023-05-09 10:05:48 -04:00
Jenkins
a59ff5e7e8 chore(i18n): update translations 2023-05-07 12:46:45 -04:00
Leangseu Kim
9a9c0583ca chore: update zendesk chat department 2023-05-03 12:47:06 -04:00
Jenkins
2f409e5168 chore(i18n): update translations 2023-04-30 12:46:45 -04:00
Hamzah Ullah
cf35c7d611 fix: account for isLearnerPortalEnabled when determining hasAvailableDashboards (#141) 2023-04-28 11:29:44 -04:00
Hamzah Ullah
4a2eee2a1d chore: update test and snapshot 2023-04-28 11:09:07 -04:00
Adam Stankiewicz
0ed2b10b13 fix: change button case for enterpriseDialogConfirmButton 2023-04-26 16:57:41 -04:00
Adam Stankiewicz
01f67265f6 chore: remove console.log 2023-04-26 16:40:20 -04:00
Adam Stankiewicz
8a73043368 fix: account for isLearnerPortalEnabled when determining hasAvailableDashboards 2023-04-26 16:00:38 -04:00
Jansen Kantor
b09c36e13e fix: noticeswrapper api response error (#139) 2023-04-21 10:09:22 -04:00
Jansen Kantor
14f7389900 Jkantor/notices (#134)
Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
2023-04-19 14:35:46 -04:00
Leangseu Kim
895e867b91 fix: make credit actions disable on masquerade 2023-04-19 11:55:05 -04:00
Leangseu Kim
6bc60bad33 chore: remove incorrect information in unenroll popup 2023-04-19 09:25:58 -04:00
leangseu-edx
5e716ece2d feat: Remove Learner Home from web crawling (#133) 2023-04-13 11:35:32 -04:00
Ben Warzeski
320f6acc21 fix: show cert for not-passing courses and hide link when missing URL (#131) 2023-04-10 10:07:02 -04:00
Jenkins
af51373e2c chore(i18n): update translations 2023-04-09 12:46:42 -04:00
Mubbshar Anwar
5dd00e9f24 refactor: update API endpoint (#129) 2023-04-06 08:39:27 -04:00
Ben Warzeski
63eaa00ee1 Bw/fix network args (#130) 2023-03-30 10:51:30 -04:00
Leangseu Kim
e25610c66e fix: disable title link on homeUrl undefined 2023-03-23 10:13:52 -04:00
Leangseu Kim
5724d051b2 fix: begin button disable when audit access expire 2023-03-21 16:06:18 -04:00
Leangseu Kim
9a5ac5ddf7 chore: add es, fr_ca, pt_BR translation
chore: remove intl as it is no longer needed

chore: update test snaphot
2023-03-17 13:56:40 -04:00
Leangseu Kim
145c18d9ed fix: credit cors error 2023-03-16 11:32:24 -04:00
Jenkins
b4bb924659 chore(i18n): update translations 2023-03-14 10:42:13 -04:00
Leangseu Kim
45e8113553 chore: empty out translation file 2023-03-13 11:09:26 -04:00
Mashal Malik
cfb9bfdb6b Update transifex api from v2 to v3 (#118) 2023-03-13 10:58:34 -04:00
Nathan Sprenkle
6a73054a9c fix: update links to new MFE experiences in header (#121) 2023-03-08 01:23:32 -05:00
Nathan Sprenkle
5d88e8d1ec fix: update menu item link for variant header (#120) 2023-03-07 18:20:23 -05:00
Nathan Sprenkle
19d7aa3e33 fix: update menu item link for account and profile (#119) 2023-03-07 17:48:34 -05:00
346 changed files with 15583 additions and 44141 deletions

7
.env
View File

@@ -2,6 +2,7 @@ NODE_ENV='production'
NODE_PATH=./src
BASE_URL=''
LMS_BASE_URL=''
ECOMMERCE_BASE_URL=''
LOGIN_URL=''
LOGOUT_URL=''
CSRF_TOKEN_API_PATH=''
@@ -35,3 +36,9 @@ ZENDESK_KEY=''
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false

View File

@@ -2,12 +2,12 @@ NODE_ENV='development'
PORT=1996
BASE_URL='localhost:1996'
LMS_BASE_URL='http://localhost:18000'
ECOMMERCE_BASE_URL='http://localhost:18130'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL='http://localhost:18000/support'
SUPPORT_URL=''
CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
@@ -42,3 +42,9 @@ ZENDESK_KEY=''
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false

View File

@@ -2,12 +2,12 @@ NODE_ENV='test'
PORT=1996
BASE_URL='localhost:1996'
LMS_BASE_URL='http://localhost:18000'
ECOMMERCE_BASE_URL='http://localhost:18130'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL='http://localhost:18000/support'
SUPPORT_URL=''
CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
@@ -41,3 +41,9 @@ ZENDESK_KEY='test-zendesk-key'
HOTJAR_APP_ID='hot-jar-app-id'
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=true
ENABLE_PROGRAMS=false

View File

@@ -1,10 +1,11 @@
const { createConfig } = require('@edx/frontend-build');
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('eslint', {
rules: {
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
'import/no-self-import': 'off',
'import/no-import-module-exports': 'off',
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
},
});

9
.github/CODEOWNERS vendored
View File

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

7
.github/dependabot.yml vendored Normal file
View File

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

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

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

View File

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

View File

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

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

1
.nvmrc Normal file
View File

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

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

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

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

View File

@@ -1,4 +1,4 @@
const { createConfig } = require('@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',

49025
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,10 +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" />
</head>
<body>
<div id="root"></div>

2
public/robots.txt Normal file
View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { shallow } from '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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
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';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton';
import messages from './messages';
@@ -14,15 +15,14 @@ export const UpgradeButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { canUpgrade } = reduxHooks.useCardEnrollmentData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData();
const { disableUpgradeCourse } = useActionDisabledState(cardId);
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
track.course.upgradeClicked,
cardId,
upgradeUrl,
);
const isEnabled = (!isMasquerading && canUpgrade);
const enabledProps = {
as: 'a',
href: upgradeUrl,
@@ -32,8 +32,8 @@ export const UpgradeButton = ({ cardId }) => {
<ActionButton
iconBefore={Locked}
variant="outline-primary"
disabled={!isEnabled}
{...isEnabled && enabledProps}
disabled={disableUpgradeCourse}
{...!disableUpgradeCourse && enabledProps}
>
{formatMessage(messages.upgrade)}
</ActionButton>

View File

@@ -1,8 +1,8 @@
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';
jest.mock('tracking', () => ({
@@ -13,15 +13,13 @@ jest.mock('tracking', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })),
useTrackCourseEvent: jest.fn(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableUpgradeCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
describe('UpgradeButton', () => {
@@ -33,25 +31,19 @@ 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,
));
});
test('cannot upgrade', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false });
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
test('masquerading', () => {
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,16 +26,28 @@ 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>
);
}
if (certificate.isDownloadable) {
return (
<Banner variant="success" icon={CheckCircle}>
{formatMessage(messages.certReady)}
{certificate.certPreviewUrl && (
<>
{' '}
<Hyperlink isInline destination={certificate.certPreviewUrl}>
{formatMessage(messages.viewCertificate)}
</Hyperlink>
</>
)}
</Banner>
);
@@ -63,17 +75,6 @@ export const CertificateBanner = ({ cardId }) => {
</Banner>
);
}
if (certificate.isDownloadable) {
return (
<Banner variant="success" icon={CheckCircle}>
{formatMessage(messages.certReady)}
{' '}
<Hyperlink isInline destination={certificate.certPreviewUrl}>
{formatMessage(messages.viewCertificate)}
</Hyperlink>
</Banner>
);
}
if (certificate.isEarnedButUnavailable) {
return (
<Banner>

View File

@@ -1,4 +1,4 @@
import { shallow } from '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,18 +38,22 @@ describe('CertificateBanner', () => {
};
const defaultCourseRun = { isArchived: false };
const defaultGrade = { isPassing: false };
const defaultPlatformSettings = {};
const createWrapper = ({
certificate = {},
enrollment = {},
grade = {},
courseRun = {},
platformSettings = {},
}) => {
reduxHooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
reduxHooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings });
return shallow(<CertificateBanner {...props} />);
};
/** TODO: Update tests to validate snapshots **/
describe('snapshot', () => {
test('is restricted', () => {
const wrapper = createWrapper({
@@ -61,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({
@@ -72,7 +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.snapshot).toMatchSnapshot();
});
test('not passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: false },
certificate: { isDownloadable: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and audit', () => {
const wrapper = createWrapper({
@@ -80,28 +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();
});
test('is passing and is downloadable', () => {
const wrapper = createWrapper({
grade: {
isPassing: true,
},
certificate: {
isDownloadable: true,
},
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and is earned but unavailable', () => {
const wrapper = createWrapper({
@@ -112,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({
@@ -120,7 +188,7 @@ describe('CertificateBanner', () => {
isPassing: true,
},
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.snapshot).toMatchSnapshot();
});
});
describe('behavior', () => {
@@ -129,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);
});
@@ -142,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';
@@ -36,11 +36,9 @@ export const CourseBanner = ({ cardId }) => {
<Banner>
{formatMessage(messages.auditAccessExpired)}
{' '}
{
<Hyperlink isInline destination="">
{formatMessage(messages.findAnotherCourse)}
</Hyperlink>
}
<Hyperlink isInline destination="">
{formatMessage(messages.findAnotherCourse)}
</Hyperlink>
</Banner>
))}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from '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,20 +6,23 @@ exports[`CreditBanner component render with error state snapshot 1`] = `
>
<p
className="credit-error-msg"
data-testid="credit-error-msg"
>
<format-message-function
message={
Object {
{
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
"description": "",
"id": "learner-dash.courseCard.banners.credit.error",
}
}
values={
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

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

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';
@@ -10,6 +10,7 @@ import ApprovedContent from './ApprovedContent';
jest.mock('hooks', () => ({
reduxHooks: {
useCardCreditData: jest.fn(),
useMasqueradeData: jest.fn(),
},
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
@@ -22,6 +23,7 @@ const credit = {
providerName: 'test-credit-provider-name',
};
reduxHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
describe('ApprovedContent component', () => {
beforeEach(() => {
@@ -36,16 +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[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

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
@@ -7,7 +7,9 @@ import { reduxHooks } from 'hooks';
import messages from './messages';
import PendingContent from './PendingContent';
jest.mock('hooks', () => ({ reduxHooks: { useCardCreditData: jest.fn() } }));
jest.mock('hooks', () => ({
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
@@ -17,7 +19,11 @@ let component;
const cardId = 'test-card-id';
const providerName = 'test-credit-provider-name';
const providerStatusUrl = 'test-credit-provider-status-url';
reduxHooks.useCardCreditData.mockReturnValue({ providerName, providerStatusUrl });
reduxHooks.useCardCreditData.mockReturnValue({
providerName,
providerStatusUrl,
});
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
const render = () => {
el = shallow(<PendingContent cardId={cardId} />);
@@ -34,16 +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(formatMessage(messages.viewDetails));
expect(component[0].props.action.message).toEqual(
formatMessage(messages.viewDetails),
);
});
test('action.disabled is 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,24 +1,27 @@
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 && (
<ActionRow className="mt-4">
<Button
as="a"
href={action.href}
disabled={!!action.disabled}
// make sure href is not undefined. Paragon won't disable the button if href is undefined.
href={action.href || '#'}
rel="noopener"
target="_blank"
variant="outline-primary"
className="border-gray-400"
onClick={action.onClick}
data-testid="action-row-btn"
>
{action.message}
</Button>
@@ -36,6 +39,7 @@ CreditContent.propTypes = {
href: PropTypes.string,
onClick: PropTypes.func,
message: PropTypes.string,
disabled: PropTypes.bool,
}),
message: PropTypes.node.isRequired,
requestData: PropTypes.shape({

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow } from '@edx/react-unit-test-utils';
import CreditContent from './CreditContent';
@@ -8,6 +8,7 @@ const action = {
href: 'test-action-href',
onClick: jest.fn().mockName('test-action-onClick'),
message: 'test-action-message',
disabled: false,
};
const message = 'test-message';
@@ -21,19 +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.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 = shallow(<CreditContent {...props} action={{ ...action, disabled: true }} />);
expect(el.instance.findByTestId('action-row-btn')[0].props.disabled).toEqual(true);
});
});
describe('without action', () => {
@@ -41,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,8 @@ exports[`CreditContent component render with action snapshot 1`] = `
<Button
as="a"
className="border-gray-400"
data-testid="action-row-btn"
disabled={false}
href="test-action-href"
onClick={[MockFunction test-action-onClick]}
rel="noopener"
@@ -24,8 +27,8 @@ exports[`CreditContent component render with action snapshot 1`] = `
</ActionRow>
<CreditRequestForm
requestData={
Object {
"parameters": Object {
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",
@@ -39,13 +42,14 @@ exports[`CreditContent component render without action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>
<CreditRequestForm
requestData={
Object {
"parameters": Object {
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",

View File

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

View File

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

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

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

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Program } from '@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 = {

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