Compare commits

...

417 Commits

Author SHA1 Message Date
faa45786f7 Merge pull request 'andal-lnd/fornt-end' (#1) from andal-lnd/fornt-end into master
Reviewed-on: #1
Reviewed-by: Damar <damar@noreply.git.andalsoftware.com>
Reviewed-by: tauficls <tauficls@noreply.git.andalsoftware.com>
2026-04-01 08:40:05 +07:00
Banu
add424ac60 feat: updates for andal-lnd dashboard 2026-03-30 14:47:15 +07:00
renovate[bot]
305960a029 chore(deps): update dependency react-share to v5.3.0 (#807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-16 05:34:21 +00:00
renovate[bot]
a580d245c0 chore(deps): update dependency @tanstack/react-query to v5.90.21 (#803)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 04:54:21 +00:00
Muhammad Arslan
ca954e139d feat: showing course unenroll survey is configurable now (#738) 2026-03-06 09:36:09 -05:00
Jacobo Dominguez
0d2eb96c86 React query and react context conversion (#786)
Migrate from Redux to React Query and React Context.  This modernizes state management while maintaining all existing functionality.  All the redux code and files were removed, including all redux and related packages.
2026-03-03 12:45:57 -03:00
Brian Smith
f1180bffde fix(docs): use correct image for custom course banner (#796) 2026-03-02 14:04:16 +00:00
renovate[bot]
f93598a9e4 chore(deps): update dependency @edx/frontend-platform to v8.5.5 (#798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 09:35:51 +00:00
renovate[bot]
35680b8b05 fix(deps): update dependency core-js to v3.48.0 (#799)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 05:31:49 +00:00
Brian Smith
5d1000530d fix(deps): regenerate package-lock.json (#788) 2026-02-12 10:50:38 -05:00
Maxwell Frank
eef5d3f671 fix: update react-share to v5 (#795) 2026-02-12 09:03:29 -05:00
Maxwell Frank
8fc839dc3d fix: remove unused universal-cookie dep (#794) 2026-02-11 09:45:37 -05:00
Deimer Morales
b21e6e553d fix: include frontend component header translation (#793) 2026-02-10 09:23:17 -05:00
renovate[bot]
cfc82975d8 chore(deps): update dependency @edx/frontend-platform to v8.5.4 (#784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 06:04:14 +00:00
renovate[bot]
50ea19d6af chore(deps): update dependency lodash to v4.17.23 [security] (#783)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 05:47:14 +00:00
renovate[bot]
9e61ae677e chore(deps): update dependency @openedx/paragon to v23.19.1 (#781)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 08:33:59 +00:00
renovate[bot]
247794b21d fix(deps): update dependency react-router-dom to v6.30.3 (#780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 05:34:34 +00:00
renovate[bot]
0a50937b4c chore(deps): update dependency @openedx/paragon to v23.18.2 (#771)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-22 05:05:47 +00:00
dependabot[bot]
22a1c658f1 chore(deps): bump actions/checkout from 5 to 6 (#750)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Maxwell Frank <92897870+MaxFrank13@users.noreply.github.com>
2025-12-18 09:00:20 -05:00
Maxwell Frank
75396f1dab fix(deps): remove filesize dependency (#767)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 08:55:13 -05:00
Ejaz Ahmad
62099a50eb fix: env variables fetching issue for translations (#766) 2025-12-17 08:47:23 -05:00
renovate[bot]
19ccb8ab87 chore(deps): update dependency @reduxjs/toolkit to v2.11.2 (#761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 06:31:30 +00:00
renovate[bot]
a3e2c80537 chore(deps): update dependency @reduxjs/toolkit to v2.11.1 (#756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 12:40:25 +00:00
renovate[bot]
324cb525c6 fix(deps): update dependency core-js to v3.47.0 (#757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 08:39:38 +00:00
renovate[bot]
f14ab8851d chore(deps): update dependency @openedx/paragon to v23.18.1 (#755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 05:37:57 +00:00
Ejaz Ahmad
c277150716 feat: added the ability for instances to use local translations from extra repositories (#752) 2025-12-03 08:31:14 -05:00
renovate[bot]
2a0ed5714f chore(deps): update dependency @reduxjs/toolkit to v2.11.0 (#749)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 09:04:15 +00:00
renovate[bot]
1f0b758705 chore(deps): update dependency @openedx/paragon to v23.18.0 (#748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 05:30:08 +00:00
renovate[bot]
85a5a6e94e chore(deps): update dependency @openedx/paragon to v23.17.0 (#746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 09:13:57 +00:00
renovate[bot]
f59b5013c8 fix(deps): update dependency react-router-dom to v6.30.2 (#745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 06:51:01 +00:00
Maxwell Frank
86b6574c60 [DEPR] feat!: remove notices wrapper (#731) 2025-11-13 12:19:12 -05:00
renovate[bot]
c38e80505c fix(deps): update dependency @edx/frontend-component-header to v8 (#744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 12:57:02 -05:00
renovate[bot]
b926e13c01 chore(deps): update dependency @reduxjs/toolkit to v2.10.1 (#743)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 05:33:11 +00:00
renovate[bot]
0a4285aad3 chore(deps): update dependency @openedx/paragon to v23.16.0 (#742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 21:53:16 +00:00
renovate[bot]
e70fa29261 chore(deps): update dependency @reduxjs/toolkit to v2.9.2 (#741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 19:17:47 +00:00
renovate[bot]
b6ba8fb366 chore(deps): update dependency @edx/frontend-platform to v8.5.2 (#740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 08:45:29 +00:00
renovate[bot]
2221655950 chore(deps): update dependency @edx/frontend-component-footer to v14.9.3 (#739)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 07:03:55 +00:00
dependabot[bot]
0db621b134 chore(deps): bump actions/setup-node from 5 to 6 (#737) 2025-10-23 13:58:00 -04:00
renovate[bot]
aaf2e36fe9 chore(deps): update dependency @reduxjs/toolkit to v2.9.1 (#736)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 15:56:50 +00:00
renovate[bot]
b527fbcfba chore(deps): update dependency @openedx/paragon to v23.14.9 (#735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-20 05:13:03 +00:00
Feanil Patel
fa3f7b27cf fix: Run npm audit fix to update dependencies. (#734) 2025-10-17 13:23:06 -04:00
Feanil Patel
3b0c58f376 fix: Run npm audit fix to update dependencies. 2025-10-15 10:10:56 -04:00
renovate[bot]
f903392ca1 fix(deps): update dependency core-js to v3.46.0 (#733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 08:39:42 +00:00
renovate[bot]
89032f09f4 chore(deps): update dependency @openedx/paragon to v23.14.8 (#732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 04:46:26 +00:00
renovate[bot]
03f146cce9 chore(deps): update dependency @testing-library/jest-dom to v6.9.1 (#728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 06:04:32 +00:00
renovate[bot]
7dd2bda86e chore(deps): update dependency @openedx/paragon to v23.14.4 (#724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 02:33:18 -04:00
Feanil Patel
410f2f730f build: remove unused reactifex packages
Remove reactifex and/or @edx/reactifex packages from devDependencies
as they are no longer needed. Translation extraction functionality has
been verified to work correctly without these dependencies.

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

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 15:05:17 -04:00
Diana Villalvazo
75865290bf test: Deprecate react-unit-test-utils 5/15 (#659) 2025-06-17 20:32:59 -04:00
Diana Villalvazo
57fe161b16 test: Deprecate react-unit-test-utils 4/15 (#656) 2025-06-17 20:09:36 -04:00
Diana Villalvazo
5c4dfd5de3 test: Deprecate react-unit-test-utils 3/15 (#655) 2025-06-17 20:00:44 -04:00
Diana Villalvazo
b0daefa2bf test: Deprecate react-unit-test-utils 2/15 (#643) 2025-06-17 19:47:36 -04:00
Diana Villalvazo
42a21180c7 test: Deprecate react-unit-test-utils 1/15 (#640) 2025-06-17 12:52:56 -04:00
renovate[bot]
c4205e9400 fix(deps): update dependency @edx/frontend-platform to v8.3.9 (#638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 13:16:07 +00:00
renovate[bot]
086c0638f1 fix(deps): update dependency @edx/frontend-component-footer to v14.9.0 (#639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 07:03:23 +00:00
renovate[bot]
9ef5840700 fix(deps): update dependency @edx/frontend-component-footer to v14.8.0 (#632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 08:44:31 +00:00
renovate[bot]
3c8c92ab92 fix(deps): update dependency @edx/frontend-platform to v8.3.8 (#631)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 05:44:31 +00:00
renovate[bot]
7488fe55f0 chore(deps): update dependency @openedx/frontend-build to v14.6.0 (#610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jason Wesson <jsnwesson@gmail.com>
2025-05-28 20:40:21 +00:00
renovate[bot]
fe6c726306 fix(deps): update dependency core-js to v3.42.0 (#630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 12:30:22 +00:00
renovate[bot]
8c3d62c12b fix(deps): update dependency @openedx/paragon to v22.18.1 (#629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 05:35:25 +00:00
diana-villalvazo-wgu
66794acf17 feat: add advertised start date on course card (#619) 2025-05-23 15:51:37 -04:00
Maxwell Frank
2f3f3bcd8b fix: email banner bug (#628) 2025-05-23 13:49:01 -04:00
diana-villalvazo-wgu
dcab4f1b75 feat: add NON_BROWSABLE_COURSES to MFE config (#620) 2025-05-21 13:21:31 -04:00
renovate[bot]
66fdd79bdf fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 13:48:56 +00:00
renovate[bot]
0ea9f6d193 fix(deps): update dependency @reduxjs/toolkit to v2.8.2 (#622)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 13:33:24 +00:00
renovate[bot]
fee6a26366 fix(deps): update dependency @edx/frontend-component-footer to v14.7.2 (#621)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 06:54:58 +00:00
Justin Hynes
1301fddbc9 Audit usage of header elements in Learner Dashboard (#618) 2025-05-16 12:57:24 -04:00
diana-villalvazo-wgu
5dcd596ac9 chore: change heading for confirm email 2025-05-16 09:49:14 -06:00
diana-villalvazo-wgu
a5522faa42 test: update related tests 2025-05-16 09:27:30 -06:00
diana-villalvazo-wgu
3542c38472 fix: incorrect usage of header elements 2025-05-16 09:27:30 -06:00
Victor Navarro
14c03d8461 fix: add missing translation for notices not found (#612) 2025-05-15 11:57:21 -04:00
diana-villalvazo-wgu
5562d8a339 chore: Clean up learnerDashboardHeader files (#614) 2025-05-13 12:23:20 -03:00
diana-villalvazo-wgu
a9194261c8 chore: Replace query-string with URLSearchParams (#613)
Co-authored-by: diana-villalvazo-wgu <dianaximena.villalva@wgu.edu>
2025-05-13 12:12:06 -03:00
renovate[bot]
11a7512fea fix(deps): update dependency @edx/openedx-atlas to ^0.7.0 (#616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 11:15:48 +00:00
renovate[bot]
be620a80fa fix(deps): update dependency @edx/frontend-platform to v8.3.6 (#615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 07:56:10 +00:00
Edward Zarecor
1d9eb08e59 E0d/i18n fixes (#600)
Co-authored-by: I18n Fix Bot <i18n-fix@example.com>
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
2025-05-09 20:14:57 +00:00
renovate[bot]
05e9626e57 fix(deps): update dependency @edx/frontend-component-footer to v14.7.1 (#611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 05:38:36 +00:00
Brian Smith
fe386e31ee feat: import FooterSlot from component package instead of slot package (#604) 2025-04-24 12:25:32 -04:00
Brian Smith
cb1de82f0a feat: standardize slot ids (#608) 2025-04-24 07:47:52 -04:00
renovate[bot]
2337843d54 fix(deps): update dependency @edx/frontend-component-header to v6.4.0 (#607)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 20:17:47 +00:00
renovate[bot]
70da0d38ed fix(deps): update dependency @edx/frontend-component-header to v6.3.0 (#606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 10:26:20 +00:00
renovate[bot]
154a2583f6 fix(deps): update dependency @edx/frontend-component-footer to v14.6.0 (#605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 07:27:36 +00:00
renovate[bot]
633050739e fix(deps): update dependency @edx/frontend-component-footer to v14.4.0 (#602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 12:28:07 +00:00
renovate[bot]
61d24d29f1 chore(deps): update dependency @openedx/frontend-build to v14.5.0 (#601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 07:11:40 +00:00
Ivo Branco
a210f23c9f fix: unenroll reasons translation
The unenroll reasons select wasn't being translated.
2025-04-09 10:17:21 -04:00
renovate[bot]
b16908842e fix(deps): update dependency @edx/frontend-platform to v8.3.4 (#596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
2025-04-08 09:47:48 -04:00
renovate[bot]
b80cab7a66 chore(deps): update dependency @openedx/frontend-build to v14.4.2 (#595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-08 09:42:55 -04:00
Brian Smith
6b8cd1f780 feat: upgrade to react 18 (#593) 2025-04-04 10:24:46 -04:00
Régis Behmo
78c5d73900 chore: remove husky 🪓🐶 (#594) 2025-04-03 09:17:58 -04:00
renovate[bot]
eb3fc9412d fix(deps): update dependency @openedx/paragon to v22.17.0 (#592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 10:52:17 +00:00
renovate[bot]
3caf6fd67a chore(deps): update dependency @openedx/frontend-build to v14.4.1 (#591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 06:45:28 +00:00
Brian Smith
d48cb3d9fc chore(deps): update @openedx dependencies to versions that support React 18 (#590) 2025-03-27 16:16:48 -04:00
Maxwell Frank
d3b4a7fc84 feat: remove upgrade refs course banner (#585) 2025-03-26 09:48:14 -04:00
Maxwell Frank
9e63777c5c fix: CourseBanner slot readme (#589) 2025-03-25 11:36:10 -04:00
renovate[bot]
cf2f3acc51 fix(deps): update dependency @openedx/frontend-slot-footer to v1.1.0 (#588)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 06:45:05 +00:00
Maxwell Frank
54f8bc86e3 fix: rename course banner slot (#586) 2025-03-21 11:08:32 -04:00
renovate[bot]
10961010ba fix(deps): update dependency @openedx/frontend-plugin-framework to v1.6.0 (#584)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 05:59:50 +00:00
Kira Miller
3c1b749395 fix: changing slot id 2025-03-11 08:39:12 -06:00
Kira Miller
845ee09bf2 fix: PR requests 2025-03-11 08:39:12 -06:00
Kira Miller
1efec09f44 fix: PR requests 2025-03-11 08:39:12 -06:00
Kira Miller
aa1cae5200 fix: pr requests 2025-03-11 08:39:12 -06:00
Kira Miller
77ab48c59f fix: renaming slot 2025-03-11 08:39:12 -06:00
Kira Miller
5d2b33abd3 feat: adding new plugin slot for an enterprise modal 2025-03-11 08:39:12 -06:00
renovate[bot]
dd4f61eec3 fix(deps): update dependency @edx/frontend-enterprise-hotjar to v7.2.0 (#581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 12:27:57 +00:00
renovate[bot]
8f7580ec30 chore(deps): update dependency @openedx/frontend-build to v14.3.2 (#580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 05:34:46 +00:00
Ankush Chudiwal
fbf24e42d3 fix: broken skip link in Learner Dashboard (#522)
Co-authored-by: Deborah Kaplan <deborahgu@users.noreply.github.com>
2025-03-03 14:52:33 -05:00
Jason Wesson
e764e9c502 Revert "feat: adding new plugin slot for an enterprise modal"
This reverts commit f110a0ade8.
2025-03-03 12:40:17 -06:00
Jason Wesson
13721f2770 Revert "fix: renaming slot"
This reverts commit 6a43918b56.
2025-03-03 12:40:17 -06:00
Jason Wesson
960647ce9f Revert "fix: pr requests"
This reverts commit 86fd29309a.
2025-03-03 12:40:17 -06:00
Jason Wesson
44c797854f Revert "fix: PR requests"
This reverts commit 57d09af61d.
2025-03-03 12:40:17 -06:00
Jason Wesson
3ea088e411 Revert "fix: PR requests"
This reverts commit 76783133da.
2025-03-03 12:40:17 -06:00
Jason Wesson
4a18c890c3 Revert "fix: changing slot id"
This reverts commit b26d4632c9.
2025-03-03 12:40:17 -06:00
renovate[bot]
e1c1c51704 chore(deps): update dependency @openedx/frontend-build to v14.3.1 (#578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 10:39:48 +00:00
renovate[bot]
f83f3a1850 fix(deps): update dependency @openedx/paragon to v22.15.3 (#577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 05:37:49 +00:00
Kira Miller
b26d4632c9 fix: changing slot id 2025-02-27 11:28:33 -06:00
Kira Miller
76783133da fix: PR requests 2025-02-27 11:28:33 -06:00
Kira Miller
57d09af61d fix: PR requests 2025-02-27 11:28:33 -06:00
Kira Miller
86fd29309a fix: pr requests 2025-02-27 11:28:33 -06:00
Kira Miller
6a43918b56 fix: renaming slot 2025-02-27 11:28:33 -06:00
Kira Miller
f110a0ade8 feat: adding new plugin slot for an enterprise modal 2025-02-27 11:28:33 -06:00
Feanil Patel
93bd883a01 Update catalog-info file for release data (#570) 2025-02-26 09:07:11 -05:00
salman2013
61375c9e95 chore: update catalog-info-file for release data and remove openedx.yaml file 2025-02-24 15:36:50 -05:00
renovate[bot]
c2f4be5063 fix(deps): update dependency @openedx/paragon to v22.15.2 (#574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 11:48:06 +00:00
renovate[bot]
14bde7fc3f fix(deps): update dependency @edx/frontend-component-header to v5.8.3 (#573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 07:03:02 +00:00
Maxim Beder
b5e2a94480 test: update test snapshots 2025-02-14 11:51:25 -08:00
Maxim Beder
edcf2fd756 fix: course image height on IOS Safari
Course thumbnails on IOS Safari stretch to the full height of the image,
instead of being limited by width and preserving aspect ratio. This
seems to be a IOS Safari specific behavior[1].

Learner dashboard MFE uses a custom implementation of CourseCardImage,
because the one in Paragon currently doesn't allow the image to be
clickable. Because of that, we are fixing this issue in this repo for
now, instead of fixing it in Paragon, until Paragon updates their
implementation and this repo is updated to use a newer version of
Paragon.

1: https://stackoverflow.com/a/44250830
2025-02-14 11:51:25 -08:00
Maxwell Frank
9228d017af feat: course banner slot (#559) 2025-02-12 14:13:31 -05:00
renovate[bot]
1104c58611 fix(deps): update dependency react-router-dom to v6.29.0 (#561)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 08:57:13 +00:00
renovate[bot]
3d7366ac1d fix(deps): update dependency @openedx/paragon to v22.15.1 (#560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 05:15:00 +00:00
renovate[bot]
0f19ff9a02 fix(deps): update dependency @reduxjs/toolkit to v2.5.1 (#556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 10:02:08 +00:00
renovate[bot]
a21caead92 fix(deps): update dependency @openedx/paragon to v22.14.0 (#557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 05:56:20 +00:00
renovate[bot]
2b287c6332 chore(deps): update dependency @edx/browserslist-config to v1.5.0 (#552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 09:40:34 +00:00
renovate[bot]
8b67abd304 fix(deps): update dependency react-router-dom to v6.28.2 (#551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 05:03:34 +00:00
Maxwell Frank
abae82b507 fix: remove remaining UpgradeButton definition and tests (#548) 2025-01-14 14:36:38 -05:00
Maxwell Frank
777d3aa45c feat!: remove UpgradeButton (#536) 2025-01-13 13:50:50 -05:00
renovate[bot]
ce595d0e62 fix(deps): update dependency react-router-dom to v6.28.1 (#544)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 13:52:09 +00:00
renovate[bot]
0fd242eb74 fix(deps): update dependency core-js to v3.40.0 (#543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 08:36:11 +00:00
renovate[bot]
d2215570da fix(deps): update dependency @edx/frontend-platform to v8.1.5 (#542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 05:04:41 +00:00
paulbert
b6bef24ace refactor!: Remove ZendeskFab component
Deletes the ZendeskFab component and associated mock, removes react-zendesk package, and removes env variables for Zendesk
2025-01-08 14:07:21 -08:00
renovate[bot]
bb5a2aa3fd fix(deps): update dependency @reduxjs/toolkit to v2.5.0 (#540)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 09:27:09 +00:00
renovate[bot]
77d1ba93c3 fix(deps): update dependency @openedx/paragon to v22.13.0 (#539)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 06:39:30 +00:00
renovate[bot]
4aa786c595 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.4.1 (#538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 10:23:13 +00:00
renovate[bot]
a5ff2eceae chore(deps): update dependency @edx/browserslist-config to v1.4.0 (#537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 06:34:26 +00:00
renovate[bot]
84b281aa51 fix(deps): update dependency @edx/frontend-platform to v8.1.3 (#534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 10:29:28 +00:00
renovate[bot]
dc5c655314 fix(deps): update dependency @edx/frontend-component-header to v5.8.2 (#533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 06:45:43 +00:00
Brian Smith
2140d8821d chore: add dev script to package.json (#530) 2024-12-13 12:29:02 -05:00
Deborah Kaplan
63860e95ce chore: removing send mail on failure gh action
This action frequently fails, and the current maintainers
(@openedx/2U-aperture) don't require it, as we both monitor the success
of the workflows and we have workflow failure email notifications
enabled. After discussion with Axim, we are removing the  action. It can
always be added back (and potentially debugged) later if another
maintainer would benefit from it.

FIXES: APER-3814
2024-12-09 12:57:47 -08:00
renovate[bot]
1474c4c546 chore(deps): update dependency jest-when to v3.7.0 (#527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 11:45:54 +00:00
renovate[bot]
e2e51dc030 chore(deps): update dependency @openedx/frontend-build to v14.2.2 (#526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 08:22:06 +00:00
Adolfo R. Brandes
604298eaca 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:11 -03:00
renovate[bot]
f9d13c4058 fix(deps): update dependency @edx/frontend-component-header to v5.8.1 (#518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 11:07:31 +00:00
renovate[bot]
e1db6807ef fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.7 (#517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 07:43:53 +00:00
renovate[bot]
d8e1f82bdf fix(deps): update dependency @edx/frontend-enterprise-hotjar to v7 2024-11-26 15:49:09 -06:00
dependabot[bot]
c5a78e01f2 build(deps): bump dawidd6/action-send-mail from 3 to 4
Bumps [dawidd6/action-send-mail](https://github.com/dawidd6/action-send-mail) from 3 to 4.
- [Release notes](https://github.com/dawidd6/action-send-mail/releases)
- [Commits](https://github.com/dawidd6/action-send-mail/compare/v3...v4)

---
updated-dependencies:
- dependency-name: dawidd6/action-send-mail
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-26 15:43:43 -06:00
renovate[bot]
22e4b9facc chore(deps): update dependency @openedx/frontend-build to v14.2.0 (#514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 18:36:14 +00:00
renovate[bot]
1ae555eac9 chore(deps): update dependency husky to v9.1.7 (#513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 06:29:05 +00:00
Diana Olarte
a0e5f75f0b fix: apply feedback 2024-11-21 09:10:34 -05:00
Diana Olarte
2e101d5c23 fix: display programs tab only if it is configured 2024-11-21 09:10:34 -05:00
Deborah Kaplan
ce1848a5c3 Revert "fix: display programs only if the url is configured (#479)" (#504) 2024-11-18 14:45:16 -05:00
Deborah Kaplan
ee515ad666 Revert "fix: display programs only if the url is configured (#479)"
This reverts commit e8886c9d9d.
2024-11-18 14:37:40 -05:00
Deborah Kaplan
bc449a3c34 build(deps): bump codecov/codecov-action from 4 to 5 (#502) 2024-11-18 12:57:22 -05:00
dependabot[bot]
3012f64b4b build(deps): bump codecov/codecov-action from 4 to 5
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 16:47:36 +00:00
Diana Olarte
e8886c9d9d fix: display programs only if the url is configured (#479)
Removes the link of programs from the Header if the service is not configured.
2024-11-18 11:51:56 -03:00
renovate[bot]
a074459e03 fix(deps): update dependency react-intl to v6.8.9 (#501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 11:02:36 +00:00
renovate[bot]
b87e12d2cb fix(deps): update dependency @edx/frontend-component-header to v5.7.2 (#500)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 06:15:08 +00:00
Emad Rad
bf2bc405d0 chore: npm publish action removed (#490) 2024-11-13 15:36:19 -05:00
Juan Carlos Iasenza (Aulasneo)
9fecc65680 chore: remove unused dependencies 2024-11-13 15:29:53 -05:00
Maxwell Frank
486a0232e3 fix: update husky (#493) 2024-11-13 09:25:16 -05:00
renovate[bot]
e68dc88d6c fix(deps): update dependency react-intl to v6.8.7 (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 10:35:23 +00:00
renovate[bot]
f777eaabff fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.6 (#491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 07:42:48 +00:00
renovate[bot]
36080e7074 fix(deps): update dependency @edx/frontend-component-header to v5.7.1 2024-11-04 10:42:27 +00:00
renovate[bot]
bdeb7e1381 fix(deps): update dependency react-intl to v6.8.6 2024-11-04 06:09:44 +00:00
Deborah Kaplan
ecf7b56acf fix(deps): update dependency @reduxjs/toolkit to v2 (#482) 2024-11-01 12:29:05 -04:00
renovate[bot]
92a2ec1fb0 fix(deps): update dependency @reduxjs/toolkit to v2 2024-10-31 19:41:35 +00:00
Bilal Qamar
892262a107 test: Remove support for Node 18 (#436)
Co-authored-by: Jason Wesson <jsnwesson@gmail.com>
2024-10-31 15:39:36 -04:00
Deborah Kaplan
0e10a9b34b fix(deps): update dependency filesize to v10 (#484) 2024-10-30 11:58:49 -04:00
renovate[bot]
d872a57160 fix(deps): update dependency filesize to v10 2024-10-28 22:04:14 +00:00
Deborah Kaplan
0d38f107bd fix(deps): update dependency dompurify to v3 (#483) 2024-10-28 18:02:25 -04:00
renovate[bot]
1217e086c0 fix(deps): update dependency dompurify to v3 2024-10-28 12:48:57 +00:00
renovate[bot]
44e3d58e14 fix(deps): update dependency react-intl to v6.8.4 2024-10-28 11:24:24 +00:00
renovate[bot]
8b52cfc4d3 chore(deps): update dependency redux-mock-store to v1.5.5 2024-10-28 06:24:19 +00:00
Diana Olarte
c93d94035a fix: display SUPPORT_URL only if the url is configured 2024-10-25 12:18:49 -03: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
500 changed files with 25279 additions and 36284 deletions

11
.env
View File

@@ -1,4 +1,5 @@
NODE_ENV='production' NODE_ENV='production'
APP_ID='learner-dashboard'
NODE_PATH=./src NODE_PATH=./src
BASE_URL='' BASE_URL=''
LMS_BASE_URL='' LMS_BASE_URL=''
@@ -32,13 +33,15 @@ ENTERPRISE_MARKETING_UTM_SOURCE=''
ENTERPRISE_MARKETING_UTM_CAMPAIGN='' ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='' ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
LEARNING_BASE_URL='' LEARNING_BASE_URL=''
ZENDESK_KEY=''
HOTJAR_APP_ID='' HOTJAR_APP_ID=''
HOTJAR_VERSION='6' HOTJAR_VERSION='6'
HOTJAR_DEBUG='' HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='' ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL='' ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL='' CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY='' ENABLE_EDX_PERSONAL_DASHBOARD=false
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
SHOW_UNENROLL_SURVEY=true
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -1,4 +1,5 @@
NODE_ENV='development' NODE_ENV='development'
APP_ID='learner-dashboard'
PORT=1996 PORT=1996
BASE_URL='localhost:1996' BASE_URL='localhost:1996'
LMS_BASE_URL='http://localhost:18000' LMS_BASE_URL='http://localhost:18000'
@@ -8,7 +9,6 @@ LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.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_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 FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token' CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -21,7 +21,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY='' SEGMENT_KEY=''
FEATURE_FLAGS={} FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000' MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL='http://localhost:18000/support' SUPPORT_URL=''
CONTACT_URL='http://localhost:18000/contact' CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx' OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service' TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
@@ -39,12 +39,15 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer' ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
LEARNING_BASE_URL='http://localhost:2000' LEARNING_BASE_URL='http://localhost:2000'
SESSION_COOKIE_DOMAIN='localhost' SESSION_COOKIE_DOMAIN='localhost'
ZENDESK_KEY=''
HOTJAR_APP_ID='' HOTJAR_APP_ID=''
HOTJAR_VERSION='6' HOTJAR_VERSION='6'
HOTJAR_DEBUG='' HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://localhost:1997' ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995' ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL='' CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY='' ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
SHOW_UNENROLL_SURVEY=true
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -1,4 +1,5 @@
NODE_ENV='test' NODE_ENV='test'
APP_ID='learner-dashboard'
PORT=1996 PORT=1996
BASE_URL='localhost:1996' BASE_URL='localhost:1996'
LMS_BASE_URL='http://localhost:18000' LMS_BASE_URL='http://localhost:18000'
@@ -8,7 +9,6 @@ LOGOUT_URL='http://localhost:18000/logout'
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.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_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 FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
CSRF_TOKEN_API_PATH='/csrf/api/v1/token' CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -21,7 +21,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY='' SEGMENT_KEY=''
FEATURE_FLAGS={} FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000' MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL='http://localhost:18000/support' SUPPORT_URL=''
CONTACT_URL='http://localhost:18000/contact' CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx' OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service' TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
@@ -38,13 +38,14 @@ ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral' ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer' ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
LEARNING_BASE_URL='http://localhost:2000' LEARNING_BASE_URL='http://localhost:2000'
ZENDESK_KEY='test-zendesk-key'
HOTJAR_APP_ID='hot-jar-app-id' HOTJAR_APP_ID='hot-jar-app-id'
HOTJAR_VERSION='6' HOTJAR_VERSION='6'
HOTJAR_DEBUG='' HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://account-settings-url.test' ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test' ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL='' CAREER_LINK_URL=''
OPTIMIZELY_FULL_STACK_SDK_KEY='SDK Key' ENABLE_EDX_PERSONAL_DASHBOARD=true
EXPERIMENT_08_23_VAN_PAINTED_DOOR=true ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
SHOW_UNENROLL_SURVEY=true
PARAGON_THEME_URLS={}

View File

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

8
.github/CODEOWNERS vendored
View File

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

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,18 +10,16 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v6
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v3 uses: actions/setup-node@v6
with: with:
node-version: ${{ env.NODE_VER }} node-version-file: '.nvmrc'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -31,6 +29,9 @@ jobs:
- name: Lint - name: Lint
run: npm run lint run: npm run lint
- name: Type check
run: npm run types
- name: Test - name: Test
run: npm run test run: npm run test
@@ -39,19 +40,7 @@ jobs:
run: npm run build run: npm run build
- name: Run Coverage - name: Run Coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v5
- name: Send failure notification
if: ${{ failure() }}
uses: dawidd6/action-send-mail@v3
with: with:
server_address: email-smtp.us-east-1.amazonaws.com token: ${{ secrets.CODECOV_TOKEN }}
server_port: 465 fail_ci_if_error: true
username: ${{ secrets.EDX_SMTP_USERNAME }}
password: ${{ secrets.EDX_SMTP_PASSWORD }}
subject: CI workflow failed in ${{github.repository}}
to: masters-grades@edx.org
from: github-actions <github-actions@edx.org>
body: CI workflow in ${{github.repository}} failed!
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
}}"

View File

@@ -1,35 +0,0 @@
name: Release CI
on:
push:
tags:
- "*"
jobs:
release:
name: Release
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VER }}
- name: Install dependencies
run: npm ci
- name: Create Build
run: npm run build
- name: Release Package
env:
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
run: npm semantic-release

2
.gitignore vendored
View File

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

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint

2
.nvmrc
View File

@@ -1 +1 @@
18.15 24

View File

@@ -1,27 +0,0 @@
{
"branch": "master",
"tagFormat": "v${version}",
"verifyConditions": [
"@semantic-release/npm",
{
"path": "@semantic-release/github",
"assets": {
"path": "dist/*"
}
}
],
"analyzeCommits": "@semantic-release/commit-analyzer",
"generateNotes": "@semantic-release/release-notes-generator",
"prepare": "@semantic-release/npm",
"publish": [
"@semantic-release/npm",
{
"path": "@semantic-release/github",
"assets": {
"path": "dist/*"
}
}
],
"success": [],
"fail": []
}

View File

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

View File

@@ -1,8 +1,6 @@
npm-install-%: ## install specified % npm package npm-install-%: ## install specified % npm package
npm install $* --save-dev npm install $* --save-dev
git add package.json git add package.json
export TRANSIFEX_RESOURCE = frontend-app-learner-dashboard
transifex_langs = "ar,fr,fr_CA,es_419,pt_BR,zh_CN"
intl_imports = ./node_modules/.bin/intl-imports.js intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js transifex_utils = ./node_modules/.bin/transifex-utils.js
@@ -14,6 +12,11 @@ transifex_temp = ./temp/babel-plugin-formatjs
NPM_TESTS=build i18n_extract lint test NPM_TESTS=build i18n_extract lint test
# Variables for additional translation sources and imports (define in edx-internal if needed)
ATLAS_EXTRA_SOURCES ?=
ATLAS_EXTRA_INTL_IMPORTS ?=
ATLAS_OPTIONS ?=
.PHONY: test .PHONY: test
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
@@ -42,34 +45,19 @@ detect_changed_source_translations:
# Checking for changed translations... # Checking for changed translations...
git diff --exit-code $(i18n) git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations: pull_translations:
rm -rf src/i18n/messages rm -rf src/i18n/messages
mkdir src/i18n/messages mkdir src/i18n/messages
cd src/i18n/messages \ cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \ && atlas pull $(ATLAS_OPTIONS) \
translations/frontend-platform/src/i18n/messages:frontend-platform \
translations/paragon/src/i18n/messages:paragon \ translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \ translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard \
$(ATLAS_EXTRA_SOURCES)
$(intl_imports) paragon frontend-component-footer frontend-app-learner-dashboard $(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-learner-dashboard $(ATLAS_EXTRA_INTL_IMPORTS)
endif
# This target is used by CI. # This target is used by CI.
validate-no-uncommitted-package-lock-changes: validate-no-uncommitted-package-lock-changes:

View File

@@ -1,65 +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.
## License
The code in this repository is licensed under the AGPLv3 unless otherwise
noted.
Please see `LICENSE <LICENSE>`_ for details.
## 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
* [Learner Home project info at the 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.

97
README.rst Normal file
View File

@@ -0,0 +1,97 @@
|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
------------
Contributions are very welcome. Please read `So you want to contribute to Open edX <https://docs.openedx.org/en/latest/developers/quickstarts/so_you_want_to_contribute.html>`_ for details on how to get started as an Open edX contributor.
This project is currently accepting all types of contributions — bug fixes, security fixes, maintenance work, or new features.
However, if you intend to add a new feature, make sure it has gone through the `Product Review process <https://openedx.atlassian.net/wiki/spaces/COMM/pages/3875962884/How+to+submit+an+open+source+contribution+for+Product+Review>`_.
When proposing a change, create an issue in this repo to get the discussion started.
License
-------
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.

25
catalog-info.yaml Normal file
View File

@@ -0,0 +1,25 @@
# 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"
openedx.org/release: "master"
spec:
type: 'service'
lifecycle: 'production'
owner: 2U-aperture
# (Optional) An array of different components or resources.

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

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

View File

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

View File

@@ -1,9 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
tags:
- frontend-app
- masters
oeps:
oep-2: true # Repository metadata
openedx-release: {ref: master}

29999
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,9 @@
"type": "git", "type": "git",
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git" "url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
}, },
"browserslist": [
"extends @edx/browserslist-config"
],
"scripts": { "scripts": {
"build": "fedx-scripts webpack", "build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract", "i18n_extract": "fedx-scripts formatjs extract",
@@ -13,11 +16,11 @@
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/", "lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"semantic-release": "semantic-release", "semantic-release": "semantic-release",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/learner-dashboard/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests", "test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
"quality": "npm run lint-fix && npm run test", "quality": "npm run lint-fix && npm run test",
"watch-tests": "jest --watch", "watch-tests": "jest --watch",
"snapshot": "fedx-scripts jest --updateSnapshot", "types": "tsc --noEmit"
"prepare": "husky install"
}, },
"author": "edX", "author": "edX",
"license": "AGPL-3.0", "license": "AGPL-3.0",
@@ -27,73 +30,48 @@
}, },
"dependencies": { "dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.0", "@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-footer": "^12.2.1", "@edx/frontend-component-header": "^8.0.0",
"@edx/frontend-enterprise-hotjar": "^2.0.0", "@edx/frontend-enterprise-hotjar": "7.2.0",
"@edx/frontend-platform": "^5.5.4", "@edx/frontend-platform": "^8.3.1",
"@edx/paragon": "^20.44.0", "@edx/openedx-atlas": "^0.7.0",
"@edx/react-unit-test-utils": "^1.7.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4", "@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15", "@fortawesome/react-fontawesome": "^0.2.0",
"@optimizely/react-sdk": "^2.9.2", "@openedx/frontend-plugin-framework": "^1.7.0",
"@redux-beacon/segment": "^1.1.0", "@openedx/paragon": "^23.4.5",
"@reduxjs/toolkit": "^1.6.1", "@tanstack/react-query": "^5.90.16",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.21.4",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"core-js": "3.16.2", "core-js": "3.48.0",
"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", "font-awesome": "4.7.0",
"history": "5.0.1", "history": "5.3.0",
"html-react-parser": "^1.3.0",
"jest": "^26.6.3",
"jest-when": "^3.6.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"prop-types": "15.7.2", "prop-types": "15.8.1",
"query-string": "7.0.1", "react": "^18.3.1",
"react": "^17.0.2", "react-dom": "^18.3.1",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-intl": "^5.20.9", "react-intl": "6.8.9",
"react-pdf": "^5.5.0", "react-router-dom": "6.30.3",
"react-redux": "^7.2.4", "react-share": "^5.2.2",
"react-router-dom": "6.15.0", "regenerator-runtime": "^0.14.0",
"react-share": "^4.4.0", "util": "^0.12.4"
"react-zendesk": "^0.1.13",
"redux": "4.1.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",
"reselect": "^4.0.0",
"universal-cookie": "^4.0.4",
"util": "^0.12.4",
"whatwg-fetch": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@edx/frontend-build": "13.0.1", "@edx/browserslist-config": "^1.3.0",
"@edx/reactifex": "^2.1.1", "@edx/typescript-config": "^1.1.0",
"@testing-library/jest-dom": "^5.14.1", "@openedx/frontend-build": "^14.6.2",
"@testing-library/react": "^12.1.0", "@testing-library/jest-dom": "^6.6.3",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0", "@testing-library/react": "^16.2.0",
"axios-mock-adapter": "^1.20.0", "@testing-library/user-event": "^14.6.1",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^13.0.0",
"fetch-mock": "^9.11.0",
"husky": "^7.0.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest-expect-message": "^1.0.2", "jest": "^29.7.0",
"react-dev-utils": "^11.0.4", "jest-environment-jsdom": "^29.7.0",
"react-test-renderer": "^17.0.2", "jest-expect-message": "^1.1.3",
"redux-mock-store": "^1.5.4", "jest-when": "^3.6.0",
"semantic-release": "^20.1.3" "react-dev-utils": "^12.0.0",
"react-test-renderer": "^18.3.1"
} }
} }

View File

@@ -5,75 +5,29 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { logError } from '@edx/frontend-platform/logging'; import { logError } from '@edx/frontend-platform/logging';
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar'; import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
import { ErrorPage, AppContext } from '@edx/frontend-platform/react'; import { ErrorPage } from '@edx/frontend-platform/react';
import Footer from '@edx/frontend-component-footer'; import { Alert } from '@openedx/paragon';
import { Alert } from '@edx/paragon';
import { RequestKeys } from 'data/constants/requests';
import store from 'data/store';
import {
selectors,
actions,
} from 'data/redux';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard'; import Dashboard from 'containers/Dashboard';
import ZendeskFab from 'components/ZendeskFab';
import { ExperimentProvider } from 'ExperimentContext';
import track from 'tracking'; import AppWrapper from 'containers/AppWrapper';
import fakeData from 'data/services/lms/fakeData/courses';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader'; import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { useInitializeLearnerHome } from 'data/hooks';
import { useMasquerade } from 'data/context';
import messages from './messages'; import messages from './messages';
import './App.scss'; import './App.scss';
export const App = () => { export const App = () => {
const { authenticatedUser } = React.useContext(AppContext);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const isFailed = { const { masqueradeUser } = useMasquerade();
initialize: reduxHooks.useRequestIsFailed(RequestKeys.initialize), const { data, isError } = useInitializeLearnerHome();
refreshList: reduxHooks.useRequestIsFailed(RequestKeys.refreshList), const hasNetworkFailure = !masqueradeUser && isError;
}; const supportEmail = data?.platformSettings?.supportEmail || undefined;
const hasNetworkFailure = isFailed.initialize || isFailed.refreshList;
const { supportEmail } = reduxHooks.usePlatformSettingsData();
const loadData = reduxHooks.useLoadData();
const optimizelyScript = () => {
if (getConfig().OPTIMIZELY_URL) {
return <script src={getConfig().OPTIMIZELY_URL} />;
} if (getConfig().OPTIMIZELY_PROJECT_ID) {
return (
<script
src={`${getConfig().MARKETING_SITE_BASE_URL}/optimizelyjs/${getConfig().OPTIMIZELY_PROJECT_ID}.js`}
/>
);
}
return null;
};
/* istanbul ignore next */
React.useEffect(() => { React.useEffect(() => {
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
window.loadEmptyData = () => {
loadData({ ...fakeData.globalData, courses: [] });
};
window.loadMockData = () => {
loadData({
...fakeData.globalData,
courses: [
...fakeData.courseRunData,
...fakeData.entitlementData,
],
});
};
window.store = store;
window.selectors = selectors;
window.actions = actions;
window.track = track;
}
if (getConfig().HOTJAR_APP_ID) { if (getConfig().HOTJAR_APP_ID) {
try { try {
initializeHotjar({ initializeHotjar({
@@ -85,32 +39,27 @@ export const App = () => {
logError(error); logError(error);
} }
} }
}, [authenticatedUser, loadData]); }, []);
return ( return (
<> <>
<Helmet> <Helmet>
<title>{formatMessage(messages.pageTitle)}</title> <title>{formatMessage(messages.pageTitle)}</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" /> <link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
{optimizelyScript()}
</Helmet> </Helmet>
<div> <div>
<AppWrapper> <AppWrapper>
<LearnerDashboardHeader /> <LearnerDashboardHeader />
<main> <main id="main">
{hasNetworkFailure {hasNetworkFailure
? ( ? (
<Alert variant="danger"> <Alert variant="danger">
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} /> <ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
</Alert> </Alert>
) : ( ) : (
<ExperimentProvider> <Dashboard />
<Dashboard />
</ExperimentProvider>
)} )}
</main> </main>
</AppWrapper> </AppWrapper>
<Footer logo={getConfig().LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
<ZendeskFab />
</div> </div>
</> </>
); );

View File

@@ -1,14 +1,91 @@
// frontend-app-*/src/index.scss // frontend-app-*/src/index.scss
@import "~@edx/brand/paragon/fonts"; @use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
@import "~@edx/brand/paragon/variables";
@import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
$fa-font-path: "~font-awesome/fonts"; $fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome"; @import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work // Andal Learning Brand Colors - Override Paragon variables
:root {
--pgn-color-primary: #ff4f00;
--pgn-color-primary-100: #ffe6cc;
--pgn-color-primary-200: #ffcc99;
--pgn-color-primary-300: #ffb366;
--pgn-color-primary-400: #ff9933;
--pgn-color-primary-500: #ff4f00;
--pgn-color-primary-600: #cc3f00;
--pgn-color-primary-700: #992f00;
--pgn-color-primary-800: #661f00;
--pgn-color-primary-900: #330f00;
--pgn-color-action-primary: #ff4f00;
--pgn-color-action-primary-hover: #cc3f00;
--pgn-color-action-primary-focus: #992f00;
--pgn-color-action-primary-active: #992f00;
}
// Override all button variants to use Andal orange
.btn-primary {
background-color: #ff4f00 !important;
border-color: #ff4f00 !important;
&:hover {
background-color: #cc3f00 !important;
border-color: #cc3f00 !important;
}
&:focus {
background-color: #992f00 !important;
border-color: #992f00 !important;
box-shadow: 0 0 0 2px #ffcc99 !important;
}
}
.btn-outline-primary {
color: #ff4f00 !important;
border-color: #ff4f00 !important;
&:hover {
background-color: #ff4f00 !important;
color: #fff !important;
}
&:focus {
box-shadow: 0 0 0 2px #ffcc99 !important;
}
}
.btn-brand {
background-color: #ff4f00 !important;
border-color: #ff4f00 !important;
color: #fff !important;
&:hover {
background-color: #cc3f00 !important;
border-color: #cc3f00 !important;
}
&:focus {
box-shadow: 0 0 0 2px #ffcc99 !important;
}
}
// Override link colors
a {
color: #ff4f00;
&:hover {
color: #cc3f00;
}
}
// Override active states
.active {
background-color: #ff4f00 !important;
color: #fff !important;
}
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/_footer"; @import "~@edx/frontend-component-footer/dist/_footer";
.text-ellipsis { .text-ellipsis {

View File

@@ -1,168 +1,131 @@
import React from 'react'; import { render, screen, waitFor } from '@testing-library/react';
import { Helmet } from 'react-helmet';
import { shallow } from '@edx/react-unit-test-utils';
import Footer from '@edx/frontend-component-footer'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { RequestKeys } from 'data/constants/requests'; import { useInitializeLearnerHome } from 'data/hooks';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import { ExperimentProvider } from 'ExperimentContext';
import { App } from './App'; import { App } from './App';
import messages from './messages'; import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => 'Footer'); jest.mock('data/hooks', () => ({
useInitializeLearnerHome: jest.fn(),
}));
jest.mock('containers/Dashboard', () => 'Dashboard'); jest.mock('data/context', () => ({
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader'); useMasquerade: jest.fn(() => ({ masqueradeUser: null })),
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('ExperimentContext', () => ({
ExperimentProvider: 'ExperimentProvider',
})); }));
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
actions: 'redux.actions',
thunkActions: 'redux.thunkActions',
}));
jest.mock('hooks', () => ({
reduxHooks: {
useRequestIsFailed: jest.fn(),
usePlatformSettingsData: jest.fn(),
useLoadData: jest.fn(),
},
}));
jest.mock('data/store', () => 'data/store');
const logo = 'fakeLogo.png'; jest.mock('@edx/frontend-component-footer', () => ({
FooterSlot: jest.fn(() => <div>FooterSlot</div>),
}));
jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => <div>LearnerDashboardHeader</div>));
jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
jest.mock('@edx/frontend-platform', () => ({ jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({})), getConfig: jest.fn(() => ({})),
})); }));
const loadData = jest.fn(); jest.mock('@edx/frontend-platform/react', () => ({
reduxHooks.useLoadData.mockReturnValue(loadData); ...jest.requireActual('@edx/frontend-platform/react'),
ErrorPage: () => 'ErrorPage',
}));
let el; const supportEmail = 'test@support.com';
useInitializeLearnerHome.mockReturnValue({
const supportEmail = 'test-support-url'; data: {
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail }); platformSettings: {
supportEmail,
},
},
isError: false,
});
describe('App router component', () => { describe('App router component', () => {
const { formatMessage } = useIntl();
describe('component', () => { describe('component', () => {
const runBasicTests = () => { const runBasicTests = () => {
test('snapshot', () => { expect(el.snapshot).toMatchSnapshot(); }); it('displays title in helmet component', async () => {
it('displays title in helmet component', () => { await waitFor(() => expect(document.title).toEqual(messages.pageTitle.defaultMessage));
const control = el.instance
.findByType(Helmet)[0]
.findByType('title')[0];
expect(control.children[0].el).toEqual(formatMessage(messages.pageTitle));
}); });
it('displays learner dashboard header', () => { it('displays learner dashboard header', () => {
expect(el.instance.findByType(LearnerDashboardHeader).length).toEqual(1); const learnerDashboardHeader = screen.getByText('LearnerDashboardHeader');
}); expect(learnerDashboardHeader).toBeInTheDocument();
test('Footer logo drawn from env variable', () => {
expect(el.instance.findByType(Footer)[0].props.logo).toEqual(logo);
}); });
it('wraps the header and main components in an AppWrapper widget container', () => { it('wraps the header and main components in an AppWrapper widget container', () => {
const container = el.instance.findByType(AppWrapper)[0]; const appWrapper = screen.getByText('LearnerDashboardHeader').parentElement;
expect(container.children[0].type).toEqual('LearnerDashboardHeader'); expect(appWrapper).toHaveClass('AppWrapper');
expect(container.children[1].type).toEqual('main'); expect(appWrapper.children[1].id).toEqual('main');
});
it('displays footer slot', () => {
const footerSlot = screen.getByText('FooterSlot');
expect(footerSlot).toBeInTheDocument();
}); });
}; };
describe('no network failure', () => { describe('no network failure', () => {
beforeAll(() => { beforeEach(() => {
reduxHooks.useRequestIsFailed.mockReturnValue(false); jest.clearAllMocks();
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo }); getConfig.mockReturnValue({});
el = shallow(<App />); render(<IntlProvider locale="en"><App /></IntlProvider>);
}); });
runBasicTests(); runBasicTests();
it('loads dashboard', () => { it('loads dashboard', () => {
const main = el.instance.findByType('main')[0]; const dashboard = screen.getByText('Dashboard');
expect(main.children.length).toEqual(1); expect(dashboard).toBeInTheDocument();
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
).toEqual(true);
}); });
}); });
describe('no network failure with optimizely url', () => { describe('no network failure with optimizely url', () => {
beforeAll(() => { beforeEach(() => {
reduxHooks.useRequestIsFailed.mockReturnValue(false); jest.clearAllMocks();
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo, OPTIMIZELY_URL: 'fake.url' }); getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
el = shallow(<App />); render(<IntlProvider locale="en"><App /></IntlProvider>);
}); });
runBasicTests(); runBasicTests();
it('loads dashboard', () => { it('loads dashboard', () => {
const main = el.instance.findByType('main')[0]; const dashboard = screen.getByText('Dashboard');
expect(main.children.length).toEqual(1); expect(dashboard).toBeInTheDocument();
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
).toEqual(true);
}); });
}); });
describe('no network failure with optimizely project id', () => { describe('no network failure with optimizely project id', () => {
beforeAll(() => { beforeEach(() => {
reduxHooks.useRequestIsFailed.mockReturnValue(false); jest.clearAllMocks();
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo, OPTIMIZELY_PROJECT_ID: 'fakeId' }); getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
el = shallow(<App />); render(<IntlProvider locale="en"><App /></IntlProvider>);
}); });
runBasicTests(); runBasicTests();
it('loads dashboard', () => { it('loads dashboard', () => {
const main = el.instance.findByType('main')[0]; const dashboard = screen.getByText('Dashboard');
expect(main.children.length).toEqual(1); expect(dashboard).toBeInTheDocument();
const expProvider = main.children[0];
expect(expProvider.type).toEqual('ExperimentProvider');
expect(expProvider.children.length).toEqual(1);
expect(
expProvider.matches(shallow(<ExperimentProvider><Dashboard /></ExperimentProvider>)),
).toEqual(true);
}); });
}); });
describe('initialize failure', () => { describe('initialize failure', () => {
beforeAll(() => { beforeEach(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize); jest.clearAllMocks();
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo }); useInitializeLearnerHome.mockReturnValue({
el = shallow(<App />); data: null,
isError: true,
});
getConfig.mockReturnValue({});
render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
}); });
runBasicTests(); runBasicTests();
it('loads error page', () => { it('loads error page', () => {
const main = el.instance.findByType('main')[0]; const alert = screen.getByRole('alert');
expect(main.children.length).toEqual(1); expect(alert).toBeInTheDocument();
const alert = main.children[0]; const errorPage = screen.getByText('ErrorPage');
expect(alert.type).toEqual('Alert'); expect(errorPage).toBeInTheDocument();
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', () => { describe('refresh failure', () => {
beforeAll(() => { beforeEach(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList); getConfig.mockReturnValue({});
getConfig.mockReturnValue({ LOGO_POWERED_BY_OPEN_EDX_URL_SVG: logo }); render(<IntlProvider locale="en"><App /></IntlProvider>);
el = shallow(<App />);
}); });
runBasicTests(); runBasicTests();
it('loads error page', () => { it('loads error page', () => {
const main = el.instance.findByType('main')[0]; const alert = screen.getByRole('alert');
expect(main.children.length).toEqual(1); expect(alert).toBeInTheDocument();
const alert = main.children[0]; const errorPage = screen.getByText('ErrorPage');
expect(alert.type).toEqual('Alert'); expect(errorPage).toBeInTheDocument();
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,64 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useWindowSize, breakpoints } from '@edx/paragon';
import { StrictDict } from 'utils';
import api from 'widgets/ProductRecommendations/api';
import * as module from './ExperimentContext';
export const state = StrictDict({
experiment: (val) => React.useState(val), // eslint-disable-line
countryCode: (val) => React.useState(val), // eslint-disable-line
});
export const useCountryCode = (setCountryCode) => {
React.useEffect(() => {
api
.fetchRecommendationsContext()
.then((response) => {
setCountryCode(response.data.countryCode);
})
.catch(() => {
setCountryCode('');
});
/* eslint-disable */
}, []);
};
export const ExperimentContext = React.createContext();
export const ExperimentProvider = ({ children }) => {
const [countryCode, setCountryCode] = module.state.countryCode(null);
const [experiment, setExperiment] = module.state.experiment({
isExperimentActive: false,
inRecommendationsVariant: true,
});
module.useCountryCode(setCountryCode);
const { width } = useWindowSize();
const isMobile = width < breakpoints.small.minWidth;
const contextValue = React.useMemo(
() => ({
experiment,
countryCode,
setExperiment,
setCountryCode,
isMobile,
}),
[experiment, countryCode, setExperiment, setCountryCode, isMobile]
);
return (
<ExperimentContext.Provider value={contextValue}>
{children}
</ExperimentContext.Provider>
);
};
export const useExperimentContext = () => React.useContext(ExperimentContext);
ExperimentProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default { useCountryCode, useExperimentContext };

View File

@@ -1,123 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { useWindowSize } from '@edx/paragon';
import api from 'widgets/ProductRecommendations/api';
import { MockUseState } from 'testUtils';
import * as experiment from 'ExperimentContext';
const state = new MockUseState(experiment);
jest.unmock('react');
jest.spyOn(React, 'useEffect').mockImplementation((cb, prereqs) => ({ useEffect: { cb, prereqs } }));
jest.mock('widgets/ProductRecommendations/api', () => ({
fetchRecommendationsContext: jest.fn(),
}));
describe('experiments context', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('useCountryCode', () => {
describe('behaviour', () => {
describe('useEffect call', () => {
let calls;
let cb;
const setCountryCode = jest.fn();
const successfulFetch = { data: { countryCode: 'ZA' } };
beforeEach(() => {
experiment.useCountryCode(setCountryCode);
({ calls } = React.useEffect.mock);
[[cb]] = calls;
});
it('calls useEffect once', () => {
expect(calls.length).toEqual(1);
});
describe('successfull fetch', () => {
it('sets the country code', async () => {
let resolveFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
new Promise((resolve) => {
resolveFn = resolve;
}),
);
cb();
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
expect(setCountryCode).not.toHaveBeenCalled();
resolveFn(successfulFetch);
await waitFor(() => {
expect(setCountryCode).toHaveBeenCalledWith(successfulFetch.data.countryCode);
});
});
});
describe('unsuccessfull fetch', () => {
it('sets the country code to an empty string', async () => {
let rejectFn;
api.fetchRecommendationsContext.mockReturnValueOnce(
new Promise((resolve, reject) => {
rejectFn = reject;
}),
);
cb();
expect(api.fetchRecommendationsContext).toHaveBeenCalled();
expect(setCountryCode).not.toHaveBeenCalled();
rejectFn();
await waitFor(() => {
expect(setCountryCode).toHaveBeenCalledWith('');
});
});
});
});
});
});
describe('ExperimentProvider', () => {
const { ExperimentProvider } = experiment;
const TestComponent = () => {
const {
experiment: exp,
setExperiment,
countryCode,
setCountryCode,
isMobile,
} = experiment.useExperimentContext();
expect(exp.isExperimentActive).toBeFalsy();
expect(exp.inRecommendationsVariant).toBeTruthy();
expect(countryCode).toBeNull();
expect(isMobile).toBe(false);
expect(setExperiment).toBeDefined();
expect(setCountryCode).toBeDefined();
return (
<div />
);
};
it('allows access to child components with the context stateful values', () => {
const countryCodeSpy = jest.spyOn(experiment, 'useCountryCode').mockImplementationOnce(() => {});
useWindowSize.mockImplementationOnce(() => ({ width: 577, height: 943 }));
state.mock();
mount(
<ExperimentProvider>
<TestComponent />
</ExperimentProvider>,
);
expect(countryCodeSpy).toHaveBeenCalledWith(state.setState.countryCode);
state.expectInitializedWith(state.keys.countryCode, null);
state.expectInitializedWith(state.keys.experiment, { isExperimentActive: false, inRecommendationsVariant: true });
});
});
});

View File

@@ -1,170 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App router component component initialize failure 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>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
</div>
</Fragment>
`;
exports[`App router component component no network failure 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>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
</div>
</Fragment>
`;
exports[`App router component component no network failure with optimizely project id snapshot 1`] = `
<Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
/>
<script
src="undefined/optimizelyjs/fakeId.js"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
</div>
</Fragment>
`;
exports[`App router component component no network failure with optimizely url snapshot 1`] = `
<Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
/>
<script
src="fake.url"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<ExperimentProvider>
<Dashboard />
</ExperimentProvider>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
</div>
</Fragment>
`;
exports[`App router component component refresh failure 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>
<Alert
variant="danger"
>
<ErrorPage
message="If you experience repeated failures, please email support at test-support-url"
/>
</Alert>
</main>
</AppWrapper>
<Footer
logo="fakeLogo.png"
/>
<ZendeskFab />
</div>
</Fragment>
`;

View File

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

View File

@@ -0,0 +1,17 @@
<svg width="518" height="87" viewBox="0 0 518 87" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M512.654 43.4098C512.654 35.747 509.327 31.2325 503.564 31.2325C497.861 31.2325 494.474 35.747 494.474 43.4098C494.474 51.0726 497.861 55.5871 503.564 55.5871C509.327 55.5871 512.654 51.0726 512.654 43.4098ZM518 56.0029C518 67.111 513.188 72.3978 503.029 72.3978C499.286 72.3978 496.019 71.8632 492.87 70.6751V65.5072C496.019 67.1704 499.049 68.0021 502.198 68.0021C509.148 68.0021 512.654 64.3786 512.654 56.9534V54.3397C510.455 58.1414 507.01 60.0422 502.257 60.0422C498.277 60.0422 495.009 58.5572 492.573 55.5277C490.137 52.4982 488.949 48.4589 488.949 43.4098C488.949 38.3607 490.137 34.3807 492.573 31.3513C495.009 28.3218 498.277 26.7773 502.257 26.7773C507.01 26.7773 510.455 28.6782 512.654 32.4799V27.5496H518V56.0029Z" fill="black"/>
<path d="M484.03 50.3004C484.149 55.7653 484.209 59.864 484.209 59.864L478.743 59.9234C478.743 59.9234 478.862 60.7551 478.683 51.0726C478.565 45.4294 478.565 40.6179 478.565 40.6179C478.565 34.4401 476.129 31.4107 471.376 31.4107C465.613 31.4107 462.049 35.3312 462.049 41.6871V60.0422H456.702C456.702 60.0422 456.702 46.7363 456.702 37.1726C456.702 32.5987 453.375 33.0145 453.375 33.0145V27.5496C453.375 27.5496 456.88 27.5496 458.841 29.688C460.564 31.5889 460.623 33.7273 460.623 33.7273C461.811 29.7474 466.802 26.7773 472.03 26.7773C479.456 26.7773 483.911 31.4701 483.911 40.4397C483.911 40.4397 483.911 41.5089 484.03 50.3004Z" fill="black"/>
<path d="M443.611 27.5485H448.958C448.958 27.5485 448.661 42.8147 448.779 51.4873C448.779 55.5861 452.166 55.1702 452.166 55.1702V60.2194C452.166 60.2194 443.492 60.0412 443.611 52.2002C443.73 42.6365 443.611 27.5485 443.611 27.5485ZM446.225 14.3613C444.264 14.3613 442.66 15.9652 442.66 17.9254C442.66 19.8857 444.264 21.4895 446.225 21.4895C448.185 21.4895 449.789 19.9451 449.789 17.9254C449.789 15.9058 448.185 14.3613 446.225 14.3613Z" fill="black"/>
<path d="M436.376 50.3004C436.495 55.7653 436.554 59.864 436.554 59.864L431.089 59.9234C431.089 59.9234 431.207 60.7551 431.029 51.0726C430.91 45.4294 430.91 40.6179 430.91 40.6179C430.91 34.4401 428.475 31.4107 423.722 31.4107C417.959 31.4107 414.395 35.3312 414.395 41.6871V60.0422H409.048C409.048 60.0422 409.048 46.7363 409.048 37.1726C409.048 32.5987 405.721 33.0145 405.721 33.0145V27.5496C405.721 27.5496 409.226 27.5496 411.186 29.688C412.909 31.5889 412.969 33.7273 412.969 33.7273C414.157 29.7474 419.147 26.7773 424.375 26.7773C431.801 26.7773 436.257 31.4701 436.257 40.4397C436.257 40.4397 436.257 41.5089 436.376 50.3004Z" fill="black"/>
<path d="M403.931 32.4208C402.208 32.5396 400.961 32.5396 399 33.49C396.386 34.7969 393.237 37.7076 393.237 42.9349C393.237 49.8849 393.237 60.0426 393.237 60.0426H387.891V27.5499H393.237V32.599C395.257 29.5695 397.337 27.3717 401.911 27.1935C402.684 27.1341 402.446 27.1341 403.931 27.1935V32.4208Z" fill="black"/>
<path d="M369.696 43.7068C365.359 43.7068 362.388 44.182 360.725 45.1918C359.061 46.2017 358.23 47.8649 358.23 50.241C358.23 54.0427 360.903 56.4187 365.181 56.4187C371.122 56.4187 374.983 51.8448 374.983 44.8948V43.7068H369.696ZM380.271 51.9636C380.271 56.1811 384.251 55.8247 384.251 55.8247V59.9234C384.251 59.9234 381.221 60.2799 379.142 58.6166C377.241 57.1316 376.112 54.9337 376.112 54.9337C373.676 58.9136 369.042 60.8739 363.755 60.8739C357.101 60.8739 352.883 56.8346 352.883 50.5974C352.883 43.291 357.814 39.5487 367.498 39.5487H374.983V39.0141C374.983 34.0837 372.191 31.2919 366.309 31.2919C363.577 31.2919 360.071 33.3709 360.19 36.935L355.259 37.0538C355.378 28.6782 362.923 26.7773 366.428 26.7773C375.577 26.7773 380.33 31.5889 380.33 41.5089C380.33 41.5089 380.211 47.8055 380.271 51.9636Z" fill="black"/>
<path d="M349.423 42.4594V45.073H324.887C325.362 52.4388 329.402 56.3593 336.472 56.3593C340.571 56.3593 344.374 55.4089 348.176 53.3299V58.379C344.374 60.0422 340.274 60.8739 336.175 60.8739C331.006 60.8739 326.848 59.3888 323.818 56.3593C320.788 53.3299 319.303 49.2906 319.303 44.1226C319.303 38.8359 320.729 34.6184 323.58 31.4701C326.491 28.3218 330.353 26.7773 335.224 26.7773C339.621 26.7773 343.067 28.203 345.621 30.9949C348.176 33.7867 349.423 37.6478 349.423 42.4594ZM344.076 40.9149C344.017 35.0342 340.571 31.2919 335.284 31.2919C329.343 31.2919 325.6 34.7966 325.065 40.9149H344.076Z" fill="black"/>
<path d="M308.35 14.897H313.696C313.696 14.897 313.875 38.4794 313.875 51.1914C313.875 55.4089 319.459 55.3495 319.459 55.3495L319.4 60.161C319.4 60.161 313.578 60.3392 310.488 56.0029C309.003 53.9832 308.35 51.726 308.35 48.7559C308.35 35.3905 308.35 14.897 308.35 14.897Z" fill="black"/>
<path d="M279.439 14.897H284.786C284.786 14.897 284.965 38.4794 284.965 51.1914C284.965 55.4089 290.549 55.3495 290.549 55.3495L290.49 60.161C290.49 60.161 284.667 60.3392 281.578 56.0029C280.093 53.9832 279.439 51.726 279.439 48.7559C279.439 35.3905 279.439 14.897 279.439 14.897Z" fill="black"/>
<path d="M261.065 43.7068C256.728 43.7068 253.757 44.182 252.094 45.1918C250.431 46.2017 249.599 47.8649 249.599 50.241C249.599 54.0427 252.272 56.4187 256.55 56.4187C262.491 56.4187 266.352 51.8448 266.352 44.8948V43.7068H261.065ZM271.64 51.9636C271.64 56.1811 275.62 55.8247 275.62 55.8247V59.9234C275.62 59.9234 272.59 60.2799 270.511 58.6166C268.61 57.1316 267.481 54.9337 267.481 54.9337C265.045 58.9136 260.411 60.8739 255.124 60.8739C248.47 60.8739 244.252 56.8346 244.252 50.5974C244.252 43.291 249.183 39.5487 258.867 39.5487H266.352V39.0141C266.352 34.0837 263.56 31.2919 257.678 31.2919C254.946 31.2919 251.44 33.3709 251.559 36.935L246.628 37.0538C246.747 28.6782 254.292 26.7773 257.797 26.7773C266.946 26.7773 271.699 31.5889 271.699 41.5089C271.699 41.5089 271.58 47.8055 271.64 51.9636Z" fill="black"/>
<path d="M233.216 32.4799V14.897H238.563C238.563 14.897 238.682 38.42 238.682 50.8943C238.682 55.2901 238.682 59.864 238.682 59.864L233.81 59.9234L233.87 55.4089C231.671 59.27 227.572 60.8738 222.819 60.8738C218.898 60.8738 215.69 59.3294 213.195 56.1811C210.759 53.0328 209.512 48.9341 209.512 43.8256C209.512 38.717 210.759 34.6183 213.195 31.47C215.69 28.3217 218.898 26.7773 222.819 26.7773C227.572 26.7773 231.018 28.6187 233.216 32.4799ZM215.037 43.8256C215.037 51.6666 218.423 56.4187 224.126 56.4187C229.77 56.4187 233.216 51.6666 233.216 43.8256C233.216 35.9845 229.77 31.2324 224.126 31.2324C218.423 31.2324 215.037 35.9845 215.037 43.8256Z" fill="black"/>
<path d="M204.591 50.3004C204.71 55.7653 204.769 59.864 204.769 59.864L199.303 59.9234C199.303 59.9234 199.422 60.7551 199.244 51.0726C199.125 45.4294 199.125 40.6179 199.125 40.6179C199.125 34.4401 196.689 31.4107 191.937 31.4107C186.174 31.4107 182.609 35.3312 182.609 41.6871V60.0422H177.262C177.262 60.0422 177.262 46.7363 177.262 37.1726C177.262 32.5987 173.936 33.0145 173.936 33.0145V27.5496C173.936 27.5496 177.441 27.5496 179.401 29.688C181.124 31.5889 181.183 33.7273 181.183 33.7273C182.372 29.7474 187.362 26.7773 192.59 26.7773C200.016 26.7773 204.472 31.4701 204.472 40.4397C204.472 40.4397 204.472 41.5089 204.591 50.3004Z" fill="black"/>
<path d="M159.067 43.7068C154.73 43.7068 151.759 44.182 150.096 45.1918C148.432 46.2017 147.601 47.8649 147.601 50.241C147.601 54.0427 150.274 56.4187 154.552 56.4187C160.493 56.4187 164.354 51.8448 164.354 44.8948V43.7068H159.067ZM169.642 51.9636C169.642 56.1811 173.622 55.8247 173.622 55.8247V59.9234C173.622 59.9234 170.592 60.2799 168.513 58.6166C166.612 57.1316 165.483 54.9337 165.483 54.9337C163.047 58.9136 158.413 60.8739 153.126 60.8739C146.472 60.8739 142.254 56.8346 142.254 50.5974C142.254 43.291 147.185 39.5487 156.869 39.5487H164.354V39.0141C164.354 34.0837 161.562 31.2919 155.68 31.2919C152.948 31.2919 149.442 33.3709 149.561 36.935L144.63 37.0538C144.749 28.6782 152.294 26.7773 155.799 26.7773C164.948 26.7773 169.701 31.5889 169.701 41.5089C169.701 41.5089 169.582 47.8055 169.642 51.9636Z" fill="black"/>
<path d="M8.40549 86.8917C3.76365 86.8917 0 83.0908 0 78.4008V55.9119C0 51.2229 3.76265 47.421 8.40549 47.421H121.955C121.91 52.4182 121.875 57.4144 121.851 62.4106C121.821 68.5717 121.808 74.7338 121.81 80.8959C124.658 80.8726 127.506 80.8482 130.355 80.8249V86.8907L8.40549 86.8917Z" fill="#FF4F00"/>
<path d="M124.912 40.1348H9.02975C4.08481 40.1348 0.0752715 36.3339 0.0752715 31.6439V8.49088C0.0752715 3.80189 4.08481 0 9.02975 0H130.282C130.264 1.47615 130.246 2.95331 130.229 4.42945C129.018 4.43249 127.808 4.43554 126.596 4.43858C125.745 8.73218 124.895 13.0268 124.045 17.3204C122.613 23.2077 121.182 29.0951 119.749 34.9814H124.935C124.927 36.6989 124.919 38.4173 124.912 40.1348Z" fill="#FF4F00"/>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

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

View File

@@ -1,27 +1,27 @@
import { shallow } from 'enzyme'; import { render, screen } from '@testing-library/react';
import { Alert } from '@edx/paragon';
import Banner from './Banner'; import Banner from './Banner';
describe('Banner', () => { describe('Banner component', () => {
const props = { it('renders children content', () => {
children: 'Hello, world!', render(<Banner>Test content</Banner>);
}; expect(screen.getByText('Test content')).toBeInTheDocument();
describe('snapshot', () => { });
test('renders default banner', () => {
const wrapper = shallow(<Banner {...props} />);
expect(wrapper).toMatchSnapshot();
});
test('renders with variants', () => {
const wrapper = shallow(<Banner {...props} variant="success" />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(Alert).prop('variant')).toEqual('success'); it('uses default props correctly', () => {
}); render(<Banner>Test content</Banner>);
test('renders with custom class', () => { const banner = screen.getByRole('alert');
const wrapper = shallow(<Banner {...props} className="custom-class" />); expect(banner).toHaveClass('mb-0');
expect(wrapper).toMatchSnapshot(); });
});
it('accepts custom variant prop', () => {
render(<Banner variant="success">Test content</Banner>);
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-success');
});
it('accepts custom className prop', () => {
render(<Banner className="custom-class">Test content</Banner>);
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('custom-class');
}); });
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,34 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
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.children().length).toEqual(0);
});
it('shows children if not redirected', () => {
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
expect(el.children().length).toEqual(2);
expect(el.children().at(0).matchesElement(children[0])).toEqual(true);
expect(el.children().at(1).matchesElement(children[1])).toEqual(true);
});
});
});

View File

@@ -1,65 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ZendeskFab snapshot 1`] = `
<Zendesk
cookies={true}
defer={true}
webWidget={
Object {
"answerBot": Object {
"avatar": Object {
"name": Object {
"*": "edX Support",
},
"url": "https://edx-cdn.org/v3/prod/favicon.ico",
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": Object {
"*": "edX Support",
},
},
"chat": Object {
"departments": Object {
"enabled": Array [
"account settings",
"billing and payments",
"certificates",
"deadlines",
"errors and technical issues",
"other",
"proctoring",
],
},
"suppress": false,
},
"contactForm": Object {
"attachments": true,
"selectTicketForm": Object {
"*": "Please choose your request type:",
},
"ticketForms": Array [
Object {
"fields": Array [
Object {
"id": "description",
"prefill": Object {
"*": "",
},
},
],
"id": 360003368814,
"subject": false,
},
],
},
"contactOptions": Object {
"enabled": false,
},
"helpCenter": Object {
"originalArticleButton": true,
},
}
}
/>
`;

View File

@@ -1,56 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import Zendesk from 'react-zendesk';
import messages from './messages';
const ZendeskFab = () => {
const { formatMessage } = useIntl();
const setting = {
cookies: true,
webWidget: {
contactOptions: {
enabled: false,
},
chat: {
suppress: false,
departments: {
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
},
},
contactForm: {
ticketForms: [
{
id: 360003368814,
subject: false,
fields: [{ id: 'description', prefill: { '*': '' } }],
},
],
selectTicketForm: {
'*': formatMessage(messages.selectTicketForm),
},
attachments: true,
},
helpCenter: {
originalArticleButton: true,
},
answerBot: {
suppress: false,
contactOnlyAfterQuery: true,
title: { '*': formatMessage(messages.supportTitle) },
avatar: {
url: 'https://edx-cdn.org/v3/prod/favicon.ico',
name: { '*': formatMessage(messages.supportTitle) },
},
},
},
};
return (
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
);
};
export default ZendeskFab;

View File

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

View File

@@ -1,16 +0,0 @@
import { StrictDict } from 'utils';
export const messages = StrictDict({
supportTitle: {
id: 'zendesk.supportTitle',
description: 'Title for the support button',
defaultMessage: 'edX Support',
},
selectTicketForm: {
id: 'zendesk.selectTicketForm',
description: 'Select ticket form',
defaultMessage: 'Please choose your request type:',
},
});
export default messages;

View File

@@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Banner snapshot renders default banner 1`] = `
<Alert
className="mb-0"
icon={[MockFunction icons.Info]}
variant="info"
>
Hello, world!
</Alert>
`;
exports[`Banner snapshot renders with custom class 1`] = `
<Alert
className="custom-class"
icon={[MockFunction icons.Info]}
variant="info"
>
Hello, world!
</Alert>
`;
exports[`Banner snapshot renders with variants 1`] = `
<Alert
className="mb-0"
icon={[MockFunction icons.Info]}
variant="success"
>
Hello, world!
</Alert>
`;

View File

@@ -1,7 +1,9 @@
const configuration = { const configuration = {
// BASE_URL: process.env.BASE_URL, // BASE_URL: process.env.BASE_URL,
APP_ID: process.env.APP_ID,
LMS_BASE_URL: process.env.LMS_BASE_URL, LMS_BASE_URL: process.env.LMS_BASE_URL,
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL, ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
CREDIT_PURCHASE_URL: process.env.CREDIT_PURCHASE_URL,
// LOGIN_URL: process.env.LOGIN_URL, // LOGIN_URL: process.env.LOGIN_URL,
// LOGOUT_URL: process.env.LOGOUT_URL, // LOGOUT_URL: process.env.LOGOUT_URL,
// CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH, // CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
@@ -12,11 +14,14 @@ const configuration = {
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME, // ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL, LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '', SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
ZENDESK_KEY: process.env.ZENDESK_KEY,
SUPPORT_URL: process.env.SUPPORT_URL || null, SUPPORT_URL: process.env.SUPPORT_URL || null,
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null, CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
LOGO_URL: process.env.LOGO_URL, LOGO_URL: process.env.LOGO_URL,
ENABLE_EDX_PERSONAL_DASHBOARD: process.env.ENABLE_EDX_PERSONAL_DASHBOARD === 'true',
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
ENABLE_PROGRAMS: process.env.ENABLE_PROGRAMS === 'true',
NON_BROWSABLE_COURSES: process.env.NON_BROWSABLE_COURSES === 'true',
SHOW_UNENROLL_SURVEY: process.env.SHOW_UNENROLL_SURVEY === 'true',
}; };
const features = {}; const features = {};

View File

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

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import AppWrapper from './index';
describe('AppWrapper', () => {
it('should render children without modification', () => {
render(
<AppWrapper>
<div>Test Child</div>
</AppWrapper>,
);
expect(screen.getByText('Test Child')).toBeInTheDocument();
});
});

View File

@@ -1,10 +1,8 @@
@import "@edx/paragon/scss/core/core";
.course-card { .course-card {
.card { .card {
.pgn__card-wrapper-image-cap.vertical { .pgn__card-wrapper-image-cap.vertical {
display: flex; display: flex;
min-height: $card-image-vertical-max-height; min-height: var(--pgn-size-card-image-vertical-max-height);
} }
.pgn__card-image-cap { .pgn__card-image-cap {
border-bottom-left-radius: 0 !important; border-bottom-left-radius: 0 !important;
@@ -53,11 +51,11 @@
> .alert { > .alert {
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
padding: map-get($spacers, 3) map-get($spacers, 4); padding: var(--pgn-spacing-spacer-3) var(--pgn-spacing-spacer-4);
&:last-of-type { &:last-of-type {
border-bottom-left-radius: $alert-border-radius; border-bottom-left-radius: var(--pgn-size-alert-border-radius);
border-bottom-right-radius: $alert-border-radius; border-bottom-right-radius: var(--pgn-size-alert-border-radius);
} }
} }

View File

@@ -1,111 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCard component snapshot: collapsed 1`] = `
<div
className="mb-4.5 course-card"
data-testid="CourseCard"
id="test-card-id"
>
<Card
orientation="vertical"
>
<div
className="d-flex flex-column w-100"
>
<div>
<CourseCardImage
cardId="test-card-id"
orientation="horizontal"
/>
<Card.Body>
<Card.Header
actions={
<CourseCardMenu
cardId="test-card-id"
/>
}
title={
<CourseCardTitle
cardId="test-card-id"
/>
}
/>
<Card.Section
className="pt-0"
>
<CourseCardDetails
cardId="test-card-id"
/>
</Card.Section>
<Card.Footer
orientation="vertical"
>
<CourseCardActions
cardId="test-card-id"
/>
</Card.Footer>
</Card.Body>
</div>
<CourseCardBanners
cardId="test-card-id"
/>
</div>
</Card>
</div>
`;
exports[`CourseCard component snapshot: not collapsed 1`] = `
<div
className="mb-4.5 course-card"
data-testid="CourseCard"
id="test-card-id"
>
<Card
orientation="horizontal"
>
<div
className="d-flex flex-column w-100"
>
<div
className="d-flex"
>
<CourseCardImage
cardId="test-card-id"
orientation="horizontal"
/>
<Card.Body>
<Card.Header
actions={
<CourseCardMenu
cardId="test-card-id"
/>
}
title={
<CourseCardTitle
cardId="test-card-id"
/>
}
/>
<Card.Section
className="pt-0"
>
<CourseCardDetails
cardId="test-card-id"
/>
</Card.Section>
<Card.Footer
orientation="horizontal"
>
<CourseCardActions
cardId="test-card-id"
/>
</Card.Footer>
</Card.Body>
</div>
<CourseCardBanners
cardId="test-card-id"
/>
</div>
</Card>
</div>
`;

View File

@@ -1,14 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ActionButton snapshot is collapsed 1`] = `
<Button
arbitary="props"
size="sm"
/>
`;
exports[`ActionButton snapshot is not collapsed 1`] = `
<Button
arbitary="props"
/>
`;

View File

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

View File

@@ -1,6 +1,33 @@
import { useWindowSize, breakpoints } from '@edx/paragon'; import { useWindowSize, breakpoints } from '@openedx/paragon';
import useIsCollapsed from './hooks'; import useIsCollapsed from './hooks';
jest.mock('@openedx/paragon', () => ({
...jest.requireActual('@openedx/paragon'),
useWindowSize: jest.fn(),
breakpoints: {
extraSmall: {
minWidth: 0,
maxWidth: 575,
},
small: {
minWidth: 576,
maxWidth: 767,
},
medium: {
minWidth: 768,
maxWidth: 991,
},
large: {
minWidth: 992,
maxWidth: 1199,
},
extraLarge: {
minWidth: 1200,
maxWidth: 100000,
},
},
}));
describe('useIsCollapsed', () => { describe('useIsCollapsed', () => {
it('returns true only when it is between medium and small', () => { it('returns true only when it is between medium and small', () => {
// make sure all three breakpoints gap is large enough for test // make sure all three breakpoints gap is large enough for test

View File

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

View File

@@ -1,5 +1,4 @@
import { shallow } from 'enzyme'; import { render, screen } from '@testing-library/react';
import ActionButton from '.'; import ActionButton from '.';
import useIsCollapsed from './hooks'; import useIsCollapsed from './hooks';
@@ -8,18 +7,22 @@ jest.mock('./hooks', () => jest.fn());
describe('ActionButton', () => { describe('ActionButton', () => {
const props = { const props = {
arbitary: 'props', className: 'custom-class',
children: 'Test',
}; };
describe('snapshot', () => {
test('is collapsed', () => { it('is collapsed', async () => {
useIsCollapsed.mockReturnValueOnce(true); useIsCollapsed.mockReturnValue(true);
const wrapper = shallow(<ActionButton {...props} />); render(<ActionButton {...props} />);
expect(wrapper).toMatchSnapshot(); const button = screen.getByRole('button', { name: 'Test' });
}); expect(button).toHaveClass('btn-sm', 'custom-class');
test('is not collapsed', () => { });
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<ActionButton {...props} />); it('is not collapsed', () => {
expect(wrapper).toMatchSnapshot(); useIsCollapsed.mockReturnValue(false);
}); render(<ActionButton {...props} />);
const button = screen.getByRole('button', { name: 'Test' });
expect(button).toBeInTheDocument();
expect(button).not.toHaveClass('size', 'sm');
}); });
}); });

View File

@@ -1,21 +1,29 @@
import React from 'react'; import React, { useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
import track from 'tracking'; import track from 'tracking';
import { reduxHooks } from 'hooks'; import { useCourseData, useCourseTrackingEvent } from 'hooks';
import { useInitializeLearnerHome } from 'data/hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton'; import ActionButton from './ActionButton';
import messages from './messages'; import messages from './messages';
export const BeginCourseButton = ({ cardId }) => { export const BeginCourseButton = ({ cardId }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId); const { data: learnerData } = useInitializeLearnerHome();
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId); const courseData = useCourseData(cardId);
const homeUrl = courseData?.courseRun?.homeUrl;
const execEdTrackingParam = useMemo(() => {
const isExecEd2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(courseData.enrollment.mode);
const { authOrgId } = learnerData.enterpriseDashboard || {};
return isExecEd2UCourse ? `?org_id=${authOrgId}` : '';
}, [courseData.enrollment.mode, learnerData.enterpriseDashboard]);
const { disableBeginCourse } = useActionDisabledState(cardId); const { disableBeginCourse } = useActionDisabledState(cardId);
const handleClick = reduxHooks.useTrackCourseEvent( const handleClick = useCourseTrackingEvent(
track.course.enterCourseClicked, track.course.enterCourseClicked,
cardId, cardId,
homeUrl + execEdTrackingParam, homeUrl + execEdTrackingParam,

View File

@@ -1,86 +1,90 @@
import { shallow } from 'enzyme'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { htmlProps } from 'data/constants/htmlKeys'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import track from 'tracking'; import track from 'tracking';
import { useCourseData, useCourseTrackingEvent } from 'hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import BeginCourseButton from './BeginCourseButton'; import BeginCourseButton from './BeginCourseButton';
jest.mock('hooks', () => ({
useCourseData: jest.fn().mockReturnValue({
enrollment: { mode: 'executive-education' },
courseRun: { homeUrl: 'home-url' },
}),
useCourseTrackingEvent: jest.fn().mockReturnValue({
trackCourseEvent: jest.fn(),
}),
}));
jest.mock('data/hooks', () => ({
useInitializeLearnerHome: jest.fn().mockReturnValue({
data: {
enterpriseDashboard: {
authOrgId: 'test-org-id',
},
},
}),
}));
jest.mock('tracking', () => ({ jest.mock('tracking', () => ({
course: { course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'), enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
}, },
})); }));
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardExecEdTrackingParam: jest.fn(),
useTrackCourseEvent: jest.fn(),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false }))); jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper; jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const homeUrl = 'home-url'; const homeUrl = 'home-url';
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`; const props = {
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath); cardId: 'cardId',
reduxHooks.useTrackCourseEvent.mockImplementation( };
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
); const renderComponent = () => render(<IntlProvider locale="en"><BeginCourseButton {...props} /></IntlProvider>);
describe('BeginCourseButton', () => { describe('BeginCourseButton', () => {
const props = {
cardId: 'cardId',
};
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
describe('behavior', () => { describe('initiliaze hooks', () => {
it('initializes course run data with cardId', () => { it('initializes course run data with cardId', () => {
wrapper = shallow(<BeginCourseButton {...props} />); renderComponent();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId); expect(useCourseData).toHaveBeenCalledWith(props.cardId);
});
it('loads exec education path param', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
}); });
it('loads disabled states for begin action from action hooks', () => { it('loads disabled states for begin action from action hooks', () => {
wrapper = shallow(<BeginCourseButton {...props} />); renderComponent();
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId); expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
}); });
}); });
describe('snapshot', () => { describe('behavior', () => {
describe('disabled', () => { describe('disabled', () => {
beforeEach(() => {
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
it('should be disabled', () => { it('should be disabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(true); useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
renderComponent();
const button = screen.getByRole('button', { name: 'Begin Course' });
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });
describe('enabled', () => { describe('enabled', () => {
beforeEach(() => {
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
it('should be enabled', () => { it('should be enabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(false); renderComponent();
const button = screen.getByRole('button', { name: 'Begin Course' });
expect(button).not.toHaveClass('disabled');
expect(button).not.toHaveAttribute('aria-disabled', 'true');
}); });
it('should track enter course clicked event on click, with exec ed param', () => { it('should track enter course clicked event on click, with exec ed param', () => {
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent( renderComponent();
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Begin Course' });
user.click(button);
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked, track.course.enterCourseClicked,
props.cardId, props.cardId,
homeUrl + execEdPath(props.cardId), `${homeUrl}?org_id=test-org-id`,
)); );
}); });
}); });
}); });

View File

@@ -1,21 +1,29 @@
import React from 'react'; import React, { useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
import track from 'tracking'; import track from 'tracking';
import { reduxHooks } from 'hooks'; import { useCourseTrackingEvent, useCourseData } from 'hooks';
import { useInitializeLearnerHome } from 'data/hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton'; import ActionButton from './ActionButton';
import messages from './messages'; import messages from './messages';
export const ResumeButton = ({ cardId }) => { export const ResumeButton = ({ cardId }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId); const { data: learnerData } = useInitializeLearnerHome();
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId); const courseData = useCourseData(cardId);
const resumeUrl = courseData?.courseRun?.resumeUrl;
const execEdTrackingParam = useMemo(() => {
const isExecEd2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(courseData.enrollment.mode);
const { authOrgId } = learnerData.enterpriseDashboard || {};
return isExecEd2UCourse ? `?org_id=${authOrgId}` : '';
}, [courseData.enrollment.mode, learnerData.enterpriseDashboard]);
const { disableResumeCourse } = useActionDisabledState(cardId); const { disableResumeCourse } = useActionDisabledState(cardId);
const handleClick = reduxHooks.useTrackCourseEvent( const handleClick = useCourseTrackingEvent(
track.course.enterCourseClicked, track.course.enterCourseClicked,
cardId, cardId,
resumeUrl + execEdTrackingParam, resumeUrl + execEdTrackingParam,

View File

@@ -1,84 +1,91 @@
import { shallow } from 'enzyme'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useCourseTrackingEvent, useCourseData } from 'hooks';
import { htmlProps } from 'data/constants/htmlKeys';
import { reduxHooks } from 'hooks';
import track from 'tracking'; import track from 'tracking';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ResumeButton from './ResumeButton'; import ResumeButton from './ResumeButton';
const authOrgId = 'auth-org-id';
jest.mock('data/hooks', () => ({
useInitializeLearnerHome: jest.fn().mockReturnValue({
data: {
enterpriseDashboard: {
authOrgId,
},
},
}),
}));
jest.mock('hooks', () => ({
useCourseData: jest.fn().mockReturnValue({
enrollment: { mode: 'executive-education' },
courseRun: { homeUrl: 'home-url' },
}),
useCourseTrackingEvent: jest.fn().mockReturnValue({
trackCourseEvent: jest.fn(),
}),
}));
jest.mock('tracking', () => ({ jest.mock('tracking', () => ({
course: { course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'), enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
}, },
})); }));
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardExecEdTrackingParam: jest.fn(),
useTrackCourseEvent: jest.fn(),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false }))); jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
const resumeUrl = 'resume-url'; jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
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; useCourseData.mockReturnValue({
enrollment: { mode: 'executive-education' },
courseRun: { resumeUrl: 'home-url' },
});
describe('ResumeButton', () => { describe('ResumeButton', () => {
const props = { const props = {
cardId: 'cardId', cardId: 'cardId',
}; };
describe('behavior', () => { describe('initialize hooks', () => {
beforeEach(() => render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>));
it('initializes course run data with cardId', () => { it('initializes course run data with cardId', () => {
wrapper = shallow(<ResumeButton {...props} />); expect(useCourseData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
});
it('loads exec education path param', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
}); });
it('loads disabled states for resume action from action hooks', () => { it('loads disabled states for resume action from action hooks', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId); expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
}); });
}); });
describe('snapshot', () => { describe('behavior', () => {
describe('disabled', () => { describe('disabled', () => {
beforeEach(() => { beforeEach(() => {
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true }); useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper).toMatchSnapshot();
}); });
it('should be disabled', () => { it('should be disabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(true); render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'Resume' });
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });
describe('enabled', () => { describe('enabled', () => {
beforeEach(() => {
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
it('should be enabled', () => { it('should be enabled', () => {
expect(wrapper.prop(htmlProps.disabled)).toEqual(false); render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'Resume' });
expect(button).toBeInTheDocument();
expect(button).not.toHaveClass('disabled');
expect(button).not.toHaveAttribute('aria-disabled', 'true');
}); });
it('should track enter course clicked event on click, with exec ed param', () => { it('should track enter course clicked event on click, with exec ed param', async () => {
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent( render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Resume' });
user.click(button);
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked, track.course.enterCourseClicked,
props.cardId, props.cardId,
resumeUrl + execEdPath(props.cardId), `home-url?org_id=${authOrgId}`,
)); );
}); });
}); });
}); });

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { useSelectSessionModal } from 'data/context';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton'; import ActionButton from './ActionButton';
import messages from './messages'; import messages from './messages';
@@ -11,11 +11,11 @@ import messages from './messages';
export const SelectSessionButton = ({ cardId }) => { export const SelectSessionButton = ({ cardId }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { disableSelectSession } = useActionDisabledState(cardId); const { disableSelectSession } = useActionDisabledState(cardId);
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId); const { updateSelectSessionModal } = useSelectSessionModal();
return ( return (
<ActionButton <ActionButton
disabled={disableSelectSession} disabled={disableSelectSession}
onClick={openSessionModal} onClick={() => updateSelectSessionModal(cardId)}
> >
{formatMessage(messages.selectSession)} {formatMessage(messages.selectSession)}
</ActionButton> </ActionButton>

View File

@@ -1,35 +1,47 @@
import { shallow } from 'enzyme'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useSelectSessionModal } from 'data/context';
import { reduxHooks } from 'hooks';
import { htmlProps } from 'data/constants/htmlKeys';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import SelectSessionButton from './SelectSessionButton'; import SelectSessionButton from './SelectSessionButton';
jest.mock('hooks', () => ({ jest.mock('data/context', () => ({
reduxHooks: { useSelectSessionModal: jest.fn().mockReturnValue({
useUpdateSelectSessionModalCallback: () => jest.fn().mockName('mockOpenSessionModal'), updateSelectSessionModal: jest.fn(),
}, }),
})); }));
jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false }))); jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false })));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper; jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
describe('SelectSessionButton', () => { describe('SelectSessionButton', () => {
const props = { cardId: 'cardId' }; const props = { cardId: 'cardId' };
it('default render', () => { it('default render', () => {
expect(wrapper).toMatchSnapshot(); render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
wrapper = shallow(<SelectSessionButton {...props} />); const button = screen.getByRole('button', { name: 'Select Session' });
expect(wrapper.prop(htmlProps.disabled)).toEqual(false); expect(button).toBeInTheDocument();
expect(wrapper.prop(htmlProps.onClick).getMockName()).toEqual(
reduxHooks.useUpdateSelectSessionModalCallback().getMockName(),
);
}); });
test('disabled states', () => { describe('if useActionDisabledState is false', () => {
useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true }); it('should disabled Select Session', () => {
expect(wrapper).toMatchSnapshot(); useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true });
wrapper = shallow(<SelectSessionButton {...props} />); render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
expect(wrapper.prop(htmlProps.disabled)).toEqual(true); const button = screen.getByRole('button', { name: 'Select Session' });
expect(button).toBeDisabled();
});
});
describe('on click', () => {
it('should call openSessionModal', async () => {
const mockedUpdateSelectSessionModal = jest.fn();
useSelectSessionModal.mockReturnValue({
updateSelectSessionModal: mockedUpdateSelectSessionModal,
});
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Select Session' });
await user.click(button);
expect(mockedUpdateSelectSessionModal).toHaveBeenCalledWith(props.cardId);
});
}); });
}); });

View File

@@ -1,45 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Locked } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton';
import messages from './messages';
export const UpgradeButton = ({ cardId }) => {
const { formatMessage } = useIntl();
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { disableUpgradeCourse } = useActionDisabledState(cardId);
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
track.course.upgradeClicked,
cardId,
upgradeUrl,
);
const enabledProps = {
as: 'a',
href: upgradeUrl,
onClick: trackUpgradeClick,
};
return (
<ActionButton
iconBefore={Locked}
variant="outline-primary"
disabled={disableUpgradeCourse}
{...!disableUpgradeCourse && enabledProps}
>
{formatMessage(messages.upgrade)}
</ActionButton>
);
};
UpgradeButton.propTypes = {
cardId: PropTypes.string.isRequired,
};
export default UpgradeButton;

View File

@@ -1,50 +0,0 @@
import { shallow } from 'enzyme';
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', () => ({
course: {
upgradeClicked: jest.fn().mockName('segment.trackUpgradeClicked'),
},
}));
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(),
useTrackCourseEvent: jest.fn(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableUpgradeCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
describe('UpgradeButton', () => {
const props = {
cardId: 'cardId',
};
const upgradeUrl = 'upgradeUrl';
reduxHooks.useCardCourseRunData.mockReturnValue({ upgradeUrl });
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(
track.course.upgradeClicked,
props.cardId,
upgradeUrl,
));
});
test('cannot upgrade', () => {
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
});
});
});

View File

@@ -4,17 +4,18 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking'; import track from 'tracking';
import { reduxHooks } from 'hooks'; import { useCourseTrackingEvent, useCourseData } from 'hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton'; import ActionButton from './ActionButton';
import messages from './messages'; import messages from './messages';
export const ViewCourseButton = ({ cardId }) => { export const ViewCourseButton = ({ cardId }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId); const courseData = useCourseData(cardId);
const homeUrl = courseData?.courseRun?.homeUrl;
const { disableViewCourse } = useActionDisabledState(cardId); const { disableViewCourse } = useActionDisabledState(cardId);
const handleClick = reduxHooks.useTrackCourseEvent( const handleClick = useCourseTrackingEvent(
track.course.enterCourseClicked, track.course.enterCourseClicked,
cardId, cardId,
homeUrl, homeUrl,

View File

@@ -1,46 +1,61 @@
import { shallow } from 'enzyme'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useCourseTrackingEvent } from 'hooks';
import track from 'tracking'; import track from 'tracking';
import { htmlProps } from 'data/constants/htmlKeys';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks'; import useActionDisabledState from '../hooks';
import ViewCourseButton from './ViewCourseButton'; import ViewCourseButton from './ViewCourseButton';
jest.mock('hooks', () => ({
useCourseData: jest.fn().mockReturnValue({
courseRun: { homeUrl: 'homeUrl' },
}),
useCourseTrackingEvent: jest.fn().mockReturnValue({
trackCourseEvent: jest.fn(),
}),
}));
jest.mock('tracking', () => ({ jest.mock('tracking', () => ({
course: { course: {
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'), enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
}, },
})); }));
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
useTrackCourseEvent: jest.fn(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false }))); jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const defaultProps = { cardId: 'cardId' }; const defaultProps = { cardId: 'cardId' };
const homeUrl = 'homeUrl'; const homeUrl = 'homeUrl';
describe('ViewCourseButton', () => { describe('ViewCourseButton', () => {
test('learner can view course', () => { it('learner can view course', async () => {
const wrapper = shallow(<ViewCourseButton {...defaultProps} />); render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
expect(wrapper).toMatchSnapshot(); const button = screen.getByRole('button', { name: 'View Course' });
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent( expect(button).toBeInTheDocument();
expect(button).not.toHaveClass('disabled');
expect(button).not.toHaveAttribute('aria-disabled', 'true');
});
it('calls trackCourseEvent on click', async () => {
const mockedTrackCourseEvent = jest.fn();
useCourseTrackingEvent.mockReturnValue(mockedTrackCourseEvent);
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'View Course' });
await user.click(button);
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked, track.course.enterCourseClicked,
defaultProps.cardId, defaultProps.cardId,
homeUrl, homeUrl,
)); );
expect(wrapper.prop(htmlProps.disabled)).toEqual(false); expect(mockedTrackCourseEvent).toHaveBeenCalled();
}); });
test('learner cannot view course', () => { it('learner cannot view course', () => {
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true }); useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
const wrapper = shallow(<ViewCourseButton {...defaultProps} />); render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
expect(wrapper).toMatchSnapshot(); const button = screen.getByRole('button', { name: 'View Course' });
expect(wrapper.prop(htmlProps.disabled)).toEqual(true); expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BeginCourseButton snapshot disabled snapshot 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
},
}
}
>
Begin Course
</ActionButton>
`;
exports[`BeginCourseButton snapshot enabled snapshot 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "home-urlexec-ed-tracking-path=cardId",
},
}
}
>
Begin Course
</ActionButton>
`;

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResumeButton snapshot disabled snapshot 1`] = `
<ActionButton
as="a"
disabled={true}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
"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={
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
},
}
}
>
Resume
</ActionButton>
`;

View File

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

View File

@@ -1,32 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UpgradeButton snapshot can upgrade 1`] = `
<ActionButton
as="a"
disabled={false}
href="upgradeUrl"
iconBefore={[MockFunction icons.Locked]}
onClick={
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.trackUpgradeClicked],
"upgradeUrl": "upgradeUrl",
},
}
}
variant="outline-primary"
>
Upgrade
</ActionButton>
`;
exports[`UpgradeButton snapshot cannot upgrade 1`] = `
<ActionButton
disabled={true}
iconBefore={[MockFunction icons.Locked]}
variant="outline-primary"
>
Upgrade
</ActionButton>
`;

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ViewCourseButton learner can view course 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
Object {
"trackCourseEvent": Object {
"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={
Object {
"trackCourseEvent": Object {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "homeUrl",
},
}
}
>
View Course
</ActionButton>
`;

View File

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

View File

@@ -1,114 +1,84 @@
import { shallow } from '@edx/react-unit-test-utils'; import { render, screen } from '@testing-library/react';
import { useCourseData } from 'hooks';
import { reduxHooks } from 'hooks';
import UpgradeButton from './UpgradeButton';
import SelectSessionButton from './SelectSessionButton';
import BeginCourseButton from './BeginCourseButton';
import ResumeButton from './ResumeButton';
import ViewCourseButton from './ViewCourseButton';
import CourseCardActions from '.'; import CourseCardActions from '.';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { ...jest.requireActual('hooks'),
useCardCourseRunData: jest.fn(), useCourseData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardEntitlementData: jest.fn(),
useMasqueradeData: jest.fn(),
},
})); }));
jest.mock('./UpgradeButton', () => 'UpgradeButton'); jest.mock('plugin-slots/CourseCardActionSlot', () => jest.fn(() => <div>CourseCardActionSlot</div>));
jest.mock('./SelectSessionButton', () => 'SelectSessionButton'); jest.mock('./SelectSessionButton', () => jest.fn(() => <div>SelectSessionButton</div>));
jest.mock('./ViewCourseButton', () => 'ViewCourseButton'); jest.mock('./ViewCourseButton', () => jest.fn(() => <div>ViewCourseButton</div>));
jest.mock('./BeginCourseButton', () => 'BeginCourseButton'); jest.mock('./BeginCourseButton', () => jest.fn(() => <div>BeginCourseButton</div>));
jest.mock('./ResumeButton', () => 'ResumeButton'); jest.mock('./ResumeButton', () => jest.fn(() => <div>ResumeButton</div>));
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const props = { cardId }; const props = { cardId };
let el;
describe('CourseCardActions', () => { describe('CourseCardActions', () => {
const mockHooks = ({ const mockHooks = ({
isEntitlement = false, isEntitlement = false,
isExecEd2UCourse = false,
isFulfilled = false, isFulfilled = false,
isArchived = false, isArchived = false,
isVerified = false,
hasStarted = false, hasStarted = false,
isMasquerading = false,
} = {}) => { } = {}) => {
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled }); useCourseData.mockReturnValueOnce({
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ isArchived }); enrollment: { hasStarted },
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted }); courseRun: { isArchived },
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading }); entitlement: isEntitlement !== null ? { isEntitlement, isFulfilled } : null,
});
}; };
const render = () => { const renderComponent = () => render(<CourseCardActions {...props} />);
el = shallow(<CourseCardActions {...props} />); describe('hooks', () => {
}; it('initializes hooks', () => {
describe('behavior', () => {
it('initializes redux hooks', () => {
mockHooks(); mockHooks();
render(); renderComponent();
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId); expect(useCourseData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
}); });
}); });
describe('output', () => { describe('output', () => {
describe('Exec Ed course', () => {
it('does not render upgrade button', () => {
mockHooks({ isExecEd2UCourse: true });
render();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0);
});
});
describe('entitlement course', () => { 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', () => { it('renders ViewCourseButton if fulfilled', () => {
mockHooks({ isEntitlement: true, isFulfilled: true }); mockHooks({ isEntitlement: true, isFulfilled: true });
render(); renderComponent();
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId); const ViewCourseButton = screen.getByText('ViewCourseButton');
expect(ViewCourseButton).toBeInTheDocument();
}); });
it('renders SelectSessionButton if not fulfilled', () => { it('renders SelectSessionButton if not fulfilled', () => {
mockHooks({ isEntitlement: true }); mockHooks({ isEntitlement: true });
render(); renderComponent();
expect(el.instance.findByType(SelectSessionButton)[0].props.cardId).toEqual(cardId); const SelectSessionButton = screen.getByText('SelectSessionButton');
expect(SelectSessionButton).toBeInTheDocument();
}); });
}); });
describe('verified course', () => { describe('not entitlement, verified, or exec ed', () => {
it('does not render upgrade button', () => { it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
mockHooks({ isVerified: true }); mockHooks({ isArchived: true, isEntitlement: null });
render(); renderComponent();
expect(el.instance.findByType(UpgradeButton).length).toEqual(0); const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
}); expect(CourseCardActionSlot).toBeInTheDocument();
}); const ViewCourseButton = screen.getByText('ViewCourseButton');
describe('not entielement, verified, or exec ed', () => { expect(ViewCourseButton).toBeInTheDocument();
it('renders UpgradeButton and ViewCourseButton for archived courses', () => {
mockHooks({ isArchived: true });
render();
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId);
expect(el.instance.findByType(ViewCourseButton)[0].props.cardId).toEqual(cardId);
}); });
describe('unstarted courses', () => { describe('unstarted courses', () => {
it('renders UpgradeButton and BeginCourseButton', () => { it('renders CourseCardActionSlot and BeginCourseButton', () => {
mockHooks(); mockHooks({ isEntitlement: null });
render(); renderComponent();
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId); const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
expect(el.instance.findByType(BeginCourseButton)[0].props.cardId).toEqual(cardId); expect(CourseCardActionSlot).toBeInTheDocument();
const BeginCourseButton = screen.getByText('BeginCourseButton');
expect(BeginCourseButton).toBeInTheDocument();
}); });
}); });
describe('active courses (started, and not archived)', () => { describe('active courses (started, and not archived)', () => {
it('renders UpgradeButton and ResumeButton', () => { it('renders CourseCardActionSlot and ResumeButton', () => {
mockHooks({ hasStarted: true }); mockHooks({ hasStarted: true, isEntitlement: null });
render(); renderComponent();
expect(el.instance.findByType(UpgradeButton)[0].props.cardId).toEqual(cardId); const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
expect(el.instance.findByType(ResumeButton)[0].props.cardId).toEqual(cardId); expect(CourseCardActionSlot).toBeInTheDocument();
const ResumeButton = screen.getByText('ResumeButton');
expect(ResumeButton).toBeInTheDocument();
}); });
}); });
}); });

View File

@@ -1,11 +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',
defaultMessage: 'Upgrade',
},
beginCourse: { beginCourse: {
id: 'learner-dash.courseCard.actions.beginCourse', id: 'learner-dash.courseCard.actions.beginCourse',
description: 'Course card begin-course button text', description: 'Course card begin-course button text',

View File

@@ -1,12 +1,14 @@
/* eslint-disable max-len */ /* eslint-disable max-len */
import React from 'react'; import React, { useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { MailtoLink, Hyperlink } from '@edx/paragon'; import { MailtoLink, Hyperlink } from '@openedx/paragon';
import { CheckCircle } from '@edx/paragon/icons'; import { CheckCircle } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { baseAppUrl } from 'data/services/lms/urls';
import { utilHooks, reduxHooks } from 'hooks'; import { useInitializeLearnerHome } from 'data/hooks';
import { utilHooks, useCourseData } from 'hooks';
import Banner from 'components/Banner'; import Banner from 'components/Banner';
import messages from './messages'; import messages from './messages';
@@ -14,15 +16,32 @@ import messages from './messages';
const { useFormatDate } = utilHooks; const { useFormatDate } = utilHooks;
export const CertificateBanner = ({ cardId }) => { export const CertificateBanner = ({ cardId }) => {
const certificate = reduxHooks.useCardCertificateData(cardId); const { data: learnerHomeData } = useInitializeLearnerHome();
const courseData = useCourseData(cardId);
const { const {
isAudit, certificate = {},
isVerified, isVerified = false,
} = reduxHooks.useCardEnrollmentData(cardId); isAudit = false,
const { isPassing } = reduxHooks.useCardGradeData(cardId); isPassing = false,
const { isArchived } = reduxHooks.useCardCourseRunData(cardId); isArchived = false,
const { minPassingGrade, progressUrl } = reduxHooks.useCardCourseRunData(cardId); minPassingGrade = 0,
const { supportEmail, billingEmail } = reduxHooks.usePlatformSettingsData(); progressUrl = '',
} = useMemo(() => ({
isVerified: courseData?.enrollment?.isVerified,
isAudit: courseData?.enrollment?.isAudit,
certificate: courseData?.certificate || {},
isPassing: courseData?.gradeData?.isPassing,
isArchived: courseData?.courseRun?.isArchived,
minPassingGrade: Math.floor((courseData?.courseRun?.minPassingGrade ?? 0) * 100),
progressUrl: baseAppUrl(courseData?.courseRun?.progressUrl || ''),
}), [courseData]);
const { supportEmail, billingEmail } = useMemo(
() => ({
supportEmail: learnerHomeData?.platformSettings?.supportEmail,
billingEmail: learnerHomeData?.platformSettings?.billingEmail,
}),
[learnerHomeData],
);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const formatDate = useFormatDate(); const formatDate = useFormatDate();
@@ -75,7 +94,7 @@ export const CertificateBanner = ({ cardId }) => {
</Banner> </Banner>
); );
} }
if (certificate.isEarnedButUnavailable) { if (certificate.isEarned && new Date(certificate.availableDate) > new Date()) {
return ( return (
<Banner> <Banner>
{formatMessage( {formatMessage(

View File

@@ -1,44 +1,49 @@
import { shallow } from 'enzyme'; import React from 'react';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { useCourseData } from 'hooks';
import { useInitializeLearnerHome } from 'data/hooks';
import CertificateBanner from './CertificateBanner'; import CertificateBanner from './CertificateBanner';
import messages from './messages';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
utilHooks: { utilHooks: {
useFormatDate: jest.fn(() => date => date), useFormatDate: jest.fn(() => date => date),
}, },
reduxHooks: { useCourseData: jest.fn(),
useCardCertificateData: jest.fn(),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardGradeData: jest.fn(),
usePlatformSettingsData: jest.fn(),
},
})); }));
jest.mock('components/Banner', () => 'Banner'); jest.mock('data/hooks', () => ({
useInitializeLearnerHome: jest.fn(),
}));
const defaultCertificate = {
availableDate: '10/20/3030',
isRestricted: false,
isDownloadable: false,
isEarnedButUnavailable: false,
};
const defaultEnrollment = {
isAudit: false,
isVerified: false,
};
const defaultCourseRun = { isArchived: false };
const defaultGrade = { isPassing: false };
const defaultPlatformSettings = {};
const props = { cardId: 'cardId' };
const supportEmail = 'suport@email.com';
const billingEmail = 'billing@email.com';
describe('CertificateBanner', () => { describe('CertificateBanner', () => {
const props = { cardId: 'cardId' }; useCourseData.mockReturnValue({
reduxHooks.useCardCourseRunData.mockReturnValue({ enrollment: {},
minPassingGrade: 0.8, certificate: {},
progressUrl: 'progressUrl', gradeData: {},
courseRun: {
minPassingGrade: 0.8,
progressUrl: 'progressUrl',
},
}); });
const defaultCertificate = {
availableDate: '10/20/3030',
isRestricted: false,
isDownloadable: false,
isEarnedButUnavailable: false,
};
const defaultEnrollment = {
isAudit: false,
isVerified: false,
};
const defaultCourseRun = { isArchived: false };
const defaultGrade = { isPassing: false };
const defaultPlatformSettings = {};
const createWrapper = ({ const createWrapper = ({
certificate = {}, certificate = {},
enrollment = {}, enrollment = {},
@@ -46,182 +51,227 @@ describe('CertificateBanner', () => {
courseRun = {}, courseRun = {},
platformSettings = {}, platformSettings = {},
}) => { }) => {
reduxHooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade }); useCourseData.mockReturnValue({
reduxHooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate }); enrollment: { ...defaultEnrollment, ...enrollment },
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment }); certificate: { ...defaultCertificate, ...certificate },
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun }); gradeData: { ...defaultGrade, ...grade },
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings }); courseRun: {
return shallow(<CertificateBanner {...props} />); ...defaultCourseRun,
...courseRun,
},
});
const lernearData = { data: { platformSettings: { ...defaultPlatformSettings, ...platformSettings } } };
useInitializeLearnerHome.mockReturnValue(lernearData);
return render(<IntlProvider locale="en"><CertificateBanner {...props} /></IntlProvider>);
}; };
/** TODO: Update tests to validate snapshots **/ beforeEach(() => {
describe('snapshot', () => { jest.clearAllMocks();
test('is restricted', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
});
expect(wrapper).toMatchSnapshot();
});
test('is restricted with support email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail: 'suport@email',
},
});
expect(wrapper).toMatchSnapshot();
});
test('is restricted with billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
billingEmail: 'billing@email',
},
});
expect(wrapper).toMatchSnapshot();
});
test('is restricted and verified', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
});
expect(wrapper).toMatchSnapshot();
});
test('is restricted and verified with support email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
},
});
expect(wrapper).toMatchSnapshot();
});
test('is restricted and verified with billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
billingEmail: 'billing@email',
},
});
expect(wrapper).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).toMatchSnapshot();
});
test('is passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: true },
certificate: { isDownloadable: true },
});
expect(wrapper).toMatchSnapshot();
});
test('not passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: false },
certificate: { isDownloadable: true },
});
expect(wrapper).toMatchSnapshot();
});
test('not passing and audit', () => {
const wrapper = createWrapper({
enrollment: {
isAudit: true,
},
});
expect(wrapper).toMatchSnapshot();
});
test('not passing and has finished', () => {
const wrapper = createWrapper({
courseRun: { isArchived: true },
});
expect(wrapper).toMatchSnapshot();
});
test('not passing and not audit and not finished', () => {
const wrapper = createWrapper({});
expect(wrapper).toMatchSnapshot();
});
test('is passing and is earned but unavailable', () => {
const wrapper = createWrapper({
grade: {
isPassing: true,
},
certificate: {
isEarnedButUnavailable: true,
},
});
expect(wrapper).toMatchSnapshot();
});
test('is passing and not downloadable render empty', () => {
const wrapper = createWrapper({
grade: {
isPassing: true,
},
});
expect(wrapper).toMatchSnapshot();
});
}); });
describe('behavior', () => { it('is restricted', () => {
it('is restricted', () => { createWrapper({
const wrapper = createWrapper({ certificate: {
certificate: { isRestricted: true,
isRestricted: true, },
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
expect(bannerMessage).toEqual(messages.certRestricted.defaultMessage);
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
}); });
it('is restricted and verified', () => { const banner = screen.getByRole('alert');
const wrapper = createWrapper({ expect(banner).toBeInTheDocument();
certificate: { const msg = screen.getByText((text) => text.includes('please let us know.'));
isRestricted: true, expect(msg).toBeInTheDocument();
}, expect(msg).not.toContain(supportEmail);
enrollment: { });
isVerified: true, it('is restricted with support email', () => {
}, createWrapper({
platformSettings: { certificate: {
supportEmail: 'suport@email', isRestricted: true,
billingEmail: 'billing@email', },
}, platformSettings: {
}); supportEmail,
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n'); },
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
expect(bannerMessage).toContain(messages.certRefundContactBilling.defaultMessage);
}); });
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
const msg = screen.getByText((text) => text.includes(supportEmail));
expect(msg).toBeInTheDocument();
});
it('is restricted with billing email but not verified', () => {
createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
billingEmail,
},
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-danger');
const msg = screen.queryByText((text) => text.includes(billingEmail));
expect(msg).not.toBeInTheDocument();
});
it('is restricted and verified', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
const restrictedMsg = screen.getByText((text) => text.includes('please let us know.'));
expect(restrictedMsg).toBeInTheDocument();
const refundMsg = screen.getByText((text) => text.includes('If you would like a refund'));
expect(refundMsg).toBeInTheDocument();
});
it('is restricted and verified with support email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail,
},
});
const restrictedMsg = screen.getByText((text) => text.includes(supportEmail));
expect(restrictedMsg).toBeInTheDocument();
const refundMsg = screen.getByText((text) => text.includes('If you would like a refund'));
expect(refundMsg).toBeInTheDocument();
expect(refundMsg.innerHTML).not.toContain(billingEmail);
});
it('is restricted and verified with billing email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
billingEmail,
},
});
const restrictedMsg = screen.queryByText((text) => text.includes('please let us know.'));
expect(restrictedMsg).toBeInTheDocument();
expect(restrictedMsg.innerHTML).not.toContain(supportEmail);
const refundMsg = screen.getByText((text) => text.includes(billingEmail));
expect(refundMsg).toBeInTheDocument();
});
it('is restricted and verified with support and billing email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail,
billingEmail,
},
});
const restrictedMsg = screen.getByText((text) => text.includes(supportEmail));
expect(restrictedMsg).toBeInTheDocument();
const refundMsg = screen.getByText((text) => text.includes(billingEmail));
expect(refundMsg).toBeInTheDocument();
});
it('is passing and is downloadable', () => {
createWrapper({
grade: { isPassing: true },
certificate: { isDownloadable: true },
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-success');
const readyMsg = screen.getByText((text) => text.includes('Congratulations.'));
expect(readyMsg).toBeInTheDocument();
});
it('not passing and is downloadable', () => {
createWrapper({
grade: { isPassing: false },
certificate: { isDownloadable: true },
});
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-success');
const readyMsg = screen.getByText((text) => text.includes('Congratulations.'));
expect(readyMsg).toBeInTheDocument();
});
it('not passing and audit', () => {
createWrapper({
enrollment: {
isAudit: true,
},
});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-info');
const auditMsg = screen.getByText((text) => text.includes('Grade required to pass the course:'));
expect(auditMsg).toBeInTheDocument();
});
it('not passing and has finished', () => {
createWrapper({
courseRun: { isArchived: true },
});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-warning');
const archivedMsg = screen.getByText('You are not eligible for a certificate.');
expect(archivedMsg).toBeInTheDocument();
});
it('not passing and not audit and not finished', () => {
createWrapper({});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-warning');
const msg = screen.getByText((text) => text.includes('Grade required for a certificate'));
expect(msg).toBeInTheDocument();
});
it('is passing and is earned but unavailable', () => {
createWrapper({
grade: {
isPassing: true,
},
certificate: {
isEarned: true,
availableDate: '10/20/3030',
},
});
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('alert-info');
const earnedMsg = screen.getByText((text) => text.includes('Your grade and certificate will be ready after'));
expect(earnedMsg).toBeInTheDocument();
});
it('is passing and not downloadable render empty', () => {
createWrapper({
grade: {
isPassing: true,
},
});
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
it('should use default values when courseData is empty or undefined', () => {
useCourseData.mockReturnValue({});
const lernearData = { data: { platformSettings: { supportEmail } } };
useInitializeLearnerHome.mockReturnValue(lernearData);
render(<IntlProvider locale="en"><CertificateBanner cardId="test-card" /></IntlProvider>);
const mockedUseMemo = jest.spyOn(React, 'useMemo');
const useMemoCall = mockedUseMemo.mock.calls.find(call => call[1].some(dep => dep === undefined || dep === null));
if (useMemoCall) {
const result = useMemoCall[0]();
expect(result.certificate).toEqual({});
expect(result.isVerified).toBe(false);
expect(result.isAudit).toBe(false);
expect(result.isPassing).toBe(false);
expect(result.isArchived).toBe(false);
expect(result.minPassingGrade).toBe(0);
expect(result.progressUrl).toBeDefined();
}
mockedUseMemo.mockRestore();
}); });
}); });

View File

@@ -1,22 +1,26 @@
/* eslint-disable max-len */ /* eslint-disable max-len */
import React from 'react'; import React, { useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Hyperlink } from '@edx/paragon'; import { Hyperlink } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { utilHooks, reduxHooks } from 'hooks'; import { utilHooks, useCourseData } from 'hooks';
import Banner from 'components/Banner'; import Banner from 'components/Banner';
import messages from './messages'; import messages from './messages';
export const CourseBanner = ({ cardId }) => { export const CourseBanner = ({ cardId }) => {
const {
isVerified,
isAuditAccessExpired,
canUpgrade,
coursewareAccess = {},
} = reduxHooks.useCardEnrollmentData(cardId);
const courseRun = reduxHooks.useCardCourseRunData(cardId);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const courseData = useCourseData(cardId);
const {
isVerified = false,
isAuditAccessExpired = false,
coursewareAccess = {},
} = useMemo(() => ({
isVerified: courseData.enrollment?.isVerified,
isAuditAccessExpired: courseData.enrollment?.isAuditAccessExpired,
coursewareAccess: courseData.enrollment?.coursewareAccess || {},
}), [courseData]);
const courseRun = courseData?.courseRun || {};
const formatDate = utilHooks.useFormatDate(); const formatDate = utilHooks.useFormatDate();
const { hasUnmetPrerequisites, isStaff, isTooEarly } = coursewareAccess; const { hasUnmetPrerequisites, isStaff, isTooEarly } = coursewareAccess;
@@ -26,13 +30,7 @@ export const CourseBanner = ({ cardId }) => {
return ( return (
<> <>
{isAuditAccessExpired {isAuditAccessExpired
&& (canUpgrade ? ( && (
<Banner>
{formatMessage(messages.auditAccessExpired)}
{' '}
{formatMessage(messages.upgradeToAccess)}
</Banner>
) : (
<Banner> <Banner>
{formatMessage(messages.auditAccessExpired)} {formatMessage(messages.auditAccessExpired)}
{' '} {' '}
@@ -40,17 +38,7 @@ export const CourseBanner = ({ cardId }) => {
{formatMessage(messages.findAnotherCourse)} {formatMessage(messages.findAnotherCourse)}
</Hyperlink> </Hyperlink>
</Banner> </Banner>
))} )}
{courseRun.isActive && !canUpgrade && (
<Banner>
{formatMessage(messages.upgradeDeadlinePassed)}
{' '}
<Hyperlink isInline destination={courseRun.marketingUrl || ''}>
{formatMessage(messages.exploreCourseDetails)}
</Hyperlink>
</Banner>
)}
{(!isStaff && isTooEarly && courseRun.startDate) && ( {(!isStaff && isTooEarly && courseRun.startDate) && (
<Banner> <Banner>
@@ -59,6 +47,7 @@ export const CourseBanner = ({ cardId }) => {
})} })}
</Banner> </Banner>
)} )}
{(!isStaff && hasUnmetPrerequisites) && ( {(!isStaff && hasUnmetPrerequisites) && (
<Banner>{formatMessage(messages.prerequisitesNotMet)}</Banner> <Banner>{formatMessage(messages.prerequisitesNotMet)}</Banner>
)} )}

View File

@@ -1,31 +1,23 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { reduxHooks } from 'hooks'; import { useCourseData } from 'hooks';
import { formatMessage } from 'testUtils'; import { formatMessage } from 'testUtils';
import { CourseBanner } from './CourseBanner'; import { CourseBanner } from './CourseBanner';
import messages from './messages'; import messages from './messages';
jest.mock('components/Banner', () => 'Banner');
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
useCourseData: jest.fn(),
utilHooks: { utilHooks: {
useFormatDate: () => date => date, useFormatDate: () => date => date,
}, },
reduxHooks: {
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
},
})); }));
const cardId = 'my-test-course-number'; const cardId = 'test-card-id';
let el;
const enrollmentData = { const enrollmentData = {
isVerified: false, isVerified: false,
canUpgrade: false,
isAuditAccessExpired: false, isAuditAccessExpired: false,
coursewareAccess: { coursewareAccess: {
hasUnmetPrerequisites: false, hasUnmetPrerequisites: false,
@@ -39,127 +31,80 @@ const courseRunData = {
marketingUrl: 'marketing-url', marketingUrl: 'marketing-url',
}; };
const render = (overrides = {}) => { const renderCourseBanner = (overrides = {}) => {
const { const {
courseRun = {}, courseRun = {},
enrollment = {}, enrollment = {},
} = overrides; } = overrides;
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ useCourseData.mockReturnValue({
...courseRunData, courseRun: {
...courseRun, ...courseRunData,
...courseRun,
},
enrollment: {
...enrollmentData,
...enrollment,
},
}); });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ return render(<IntlProvider locale="en"><CourseBanner cardId={cardId} /></IntlProvider>);
...enrollmentData,
...enrollment,
});
el = shallow(<CourseBanner cardId={cardId} />);
}; };
describe('CourseBanner', () => { describe('CourseBanner', () => {
test('initializes data with course number from enrollment, course and course run data', () => { it('initializes data with course number from enrollment, course and course run data', () => {
render(); renderCourseBanner();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId); expect(useCourseData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
}); });
test('no display if learner is verified', () => { it('no display if learner is verified', () => {
render({ enrollment: { isVerified: true } }); renderCourseBanner({ enrollment: { isVerified: true } });
expect(el.isEmptyRender()).toEqual(true); expect(screen.queryByRole('alert')).toBeNull();
}); });
describe('audit access expired, can upgrade', () => { it('should use default values when enrollment data is undefined', () => {
beforeEach(() => { renderCourseBanner({
render({ enrollment: { isAuditAccessExpired: true, canUpgrade: true } }); enrollment: undefined,
}); courseRun: {},
test('snapshot: (auditAccessExpired, upgradeToAccess)', () => {
expect(el).toMatchSnapshot();
});
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.text()).toContain(messages.upgradeToAccess.defaultMessage);
}); });
expect(useCourseData).toHaveBeenCalledWith('test-card-id');
}); });
describe('audit access expired, cannot upgrade', () => { describe('audit access expired', () => {
beforeEach(() => { it('should display correct message and link', () => {
render({ enrollment: { isAuditAccessExpired: true } }); renderCourseBanner({ enrollment: { isAuditAccessExpired: true } });
const auditAccessText = screen.getByText(formatMessage(messages.auditAccessExpired));
expect(auditAccessText).toBeInTheDocument();
const auditAccessLink = screen.getByText(formatMessage(messages.findAnotherCourse));
expect(auditAccessLink).toBeInTheDocument();
}); });
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
expect(el).toMatchSnapshot();
});
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.find(Hyperlink).text()).toEqual(messages.findAnotherCourse.defaultMessage);
});
});
describe('course run active and cannot upgrade', () => {
beforeEach(() => {
render({ courseRun: { isActive: true } });
});
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
expect(el).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);
});
});
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('');
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
expect(el.html()).toEqual('');
}); });
describe('unmet prerequisites', () => { describe('unmet prerequisites', () => {
beforeEach(() => { it('should display correct message', () => {
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } }); renderCourseBanner({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
}); const preReqText = screen.getByText(formatMessage(messages.prerequisitesNotMet));
test('snapshot: unmetPrerequisites', () => { expect(preReqText).toBeInTheDocument();
expect(el).toMatchSnapshot();
});
test('messages: prerequisitesNotMet', () => {
expect(el.text()).toContain(messages.prerequisitesNotMet.defaultMessage);
}); });
}); });
describe('too early', () => { describe('too early', () => {
describe('no start date', () => { describe('no start date', () => {
beforeEach(() => { it('should not display banner', () => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } }, courseRun: { startDate: null } }); renderCourseBanner({ enrollment: { coursewareAccess: { isTooEarly: true } }, courseRun: { startDate: null } });
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
}); });
test('snapshot', () => expect(el).toMatchSnapshot());
test('messages', () => expect(el.text()).toEqual(''));
}); });
describe('has start date', () => { describe('has start date', () => {
beforeEach(() => { it('should display messages courseHasNotStarted', () => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } } }); renderCourseBanner({ enrollment: { coursewareAccess: { isTooEarly: true } } });
}); const earlyMsg = screen.getByText(
test('snapshot', () => expect(el).toMatchSnapshot());
test('messages: courseHasNotStarted', () => {
expect(el.text()).toContain(
formatMessage(messages.courseHasNotStarted, { startDate: courseRunData.startDate }), formatMessage(messages.courseHasNotStarted, { startDate: courseRunData.startDate }),
); );
expect(earlyMsg).toBeInTheDocument();
}); });
}); });
}); });
describe('staff', () => { describe('staff', () => {
beforeEach(() => { it('should not display banner', () => {
render({ enrollment: { coursewareAccess: { isStaff: true } } }); renderCourseBanner({ enrollment: { coursewareAccess: { isStaff: true } } });
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
}); });
test('snapshot: isStaff', () => {
expect(el).toMatchSnapshot();
});
});
test('snapshot: stacking banners', () => {
render({
enrollment: {
coursewareAccess: {
isStaff: true,
isTooEarly: true,
hasUnmetPrerequisites: true,
},
},
});
expect(el).toMatchSnapshot();
}); });
}); });

View File

@@ -1,56 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditBanner component render with error state snapshot 1`] = `
<Banner
variant="danger"
>
<p
className="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": <MailtoLink
to="test-support-email"
>
test-support-email
</MailtoLink>,
}
}
/>
</p>
<ContentComponent
cardId="test-card-id"
/>
</Banner>
`;
exports[`CreditBanner component render with error state with no email snapshot 1`] = `
<Banner
variant="danger"
>
<p
className="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
cardId="test-card-id"
/>
</Banner>
`;

View File

@@ -1,6 +1,8 @@
import { useMemo } from 'react';
import { useInitializeLearnerHome } from 'data/hooks';
import { StrictDict } from 'utils'; import { StrictDict } from 'utils';
import { reduxHooks } from 'hooks'; import { useCourseData } from 'hooks';
import ApprovedContent from './views/ApprovedContent'; import ApprovedContent from './views/ApprovedContent';
import EligibleContent from './views/EligibleContent'; import EligibleContent from './views/EligibleContent';
@@ -15,9 +17,29 @@ export const statusComponents = StrictDict({
}); });
export const useCreditBannerData = (cardId) => { export const useCreditBannerData = (cardId) => {
const credit = reduxHooks.useCardCreditData(cardId); const courseData = useCourseData(cardId);
const { supportEmail } = reduxHooks.usePlatformSettingsData(); const { data: learnerHomeData } = useInitializeLearnerHome();
if (!credit.isEligible) { return null; } const supportEmail = useMemo(
() => (learnerHomeData?.platformSettings?.supportEmail),
[learnerHomeData],
);
const credit = useMemo(() => {
const creditData = courseData?.credit;
if (!creditData || Object.keys(creditData).length === 0) {
return { isEligible: false };
}
return {
isEligible: true,
providerStatusUrl: creditData.providerStatusUrl,
providerName: creditData.providerName,
providerId: creditData.providerId,
error: creditData.error,
purchased: creditData.purchased,
requestStatus: creditData.requestStatus,
};
}, [courseData]);
if (!credit.isEligible || !courseData?.credit?.isEligible) { return null; }
const { error, purchased, requestStatus } = credit; const { error, purchased, requestStatus } = credit;
let ContentComponent = EligibleContent; let ContentComponent = EligibleContent;

View File

@@ -1,5 +1,6 @@
import { keyStore } from 'utils'; import { keyStore } from 'utils';
import { reduxHooks } from 'hooks'; import { useCourseData } from 'hooks';
import { useInitializeLearnerHome } from 'data/hooks';
import ApprovedContent from './views/ApprovedContent'; import ApprovedContent from './views/ApprovedContent';
import EligibleContent from './views/EligibleContent'; import EligibleContent from './views/EligibleContent';
@@ -9,12 +10,19 @@ import RejectedContent from './views/RejectedContent';
import * as hooks from './hooks'; import * as hooks from './hooks';
jest.mock('hooks', () => ({ jest.mock('react', () => ({
reduxHooks: { ...jest.requireActual('react'),
useCardCreditData: jest.fn(), useMemo: (fn) => fn(),
usePlatformSettingsData: jest.fn(),
},
})); }));
jest.mock('hooks', () => ({
useCourseData: jest.fn(),
}));
jest.mock('data/hooks', () => ({
useInitializeLearnerHome: jest.fn(),
}));
jest.mock('./views/ApprovedContent', () => 'ApprovedContent'); jest.mock('./views/ApprovedContent', () => 'ApprovedContent');
jest.mock('./views/EligibleContent', () => 'EligibleContent'); jest.mock('./views/EligibleContent', () => 'EligibleContent');
jest.mock('./views/MustRequestContent', () => 'MustRequestContent'); jest.mock('./views/MustRequestContent', () => 'MustRequestContent');
@@ -34,18 +42,18 @@ const defaultProps = {
}; };
const loadHook = (creditData = {}) => { const loadHook = (creditData = {}) => {
reduxHooks.useCardCreditData.mockReturnValue({ ...defaultProps, ...creditData }); useCourseData.mockReturnValue({ credit: { ...defaultProps, ...creditData } });
out = hooks.useCreditBannerData(cardId); out = hooks.useCreditBannerData(cardId);
}; };
describe('useCreditBannerData hook', () => { describe('useCreditBannerData hook', () => {
beforeEach(() => { beforeEach(() => {
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail }); useInitializeLearnerHome.mockReturnValue({ data: { platformSettings: { supportEmail } } });
}); });
it('loads card credit data with cardID and loads platform settings data', () => { it('loads card credit data with cardID and loads platform settings data', () => {
loadHook({ isEligible: false }); loadHook({ isEligible: false });
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId); expect(useCourseData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.usePlatformSettingsData).toHaveBeenCalledWith(); expect(useInitializeLearnerHome).toHaveBeenCalledWith();
}); });
describe('non-credit-eligible learner', () => { describe('non-credit-eligible learner', () => {
it('returns null if the learner is not credit eligible', () => { it('returns null if the learner is not credit eligible', () => {

View File

@@ -5,7 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import Banner from 'components/Banner'; import Banner from 'components/Banner';
import { MailtoLink } from '@edx/paragon'; import { MailtoLink } from '@openedx/paragon';
import hooks from './hooks'; import hooks from './hooks';
import messages from './messages'; import messages from './messages';
@@ -21,7 +21,7 @@ export const CreditBanner = ({ cardId }) => {
return ( return (
<Banner {...(error && { variant: 'danger' })}> <Banner {...(error && { variant: 'danger' })}>
{error && ( {error && (
<p className="credit-error-msg"> <p className="credit-error-msg" data-testid="credit-error-msg">
{supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)} {supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)}
</p> </p>
)} )}

View File

@@ -1,99 +1,65 @@
import React from 'react'; import { screen, render } from '@testing-library/react';
import { shallow } from 'enzyme'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { MailtoLink } from '@edx/paragon';
import hooks from './hooks'; import hooks from './hooks';
import messages from './messages'; import { CreditBanner } from '.';
import CreditBanner from '.';
jest.mock('components/Banner', () => 'Banner');
jest.mock('./hooks', () => ({ jest.mock('./hooks', () => ({
useCreditBannerData: jest.fn(), useCreditBannerData: jest.fn(),
})); }));
let el; describe('CreditBanner', () => {
const cardId = 'test-card-id'; const mockCardId = 'test-card-id';
const mockContentComponent = () => <div data-testid="mock-content">Test Content</div>;
const mockSupportEmail = 'support@test.com';
const ContentComponent = () => 'ContentComponent'; const renderCreditBanner = () => render(
const supportEmail = 'test-support-email'; <IntlProvider locale="en">
<CreditBanner cardId={mockCardId} />
</IntlProvider>,
);
describe('CreditBanner component', () => { beforeEach(() => {
describe('behavior', () => { jest.clearAllMocks();
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue(null);
el = shallow(<CreditBanner cardId={cardId} />);
});
it('initializes hooks with cardId', () => {
expect(hooks.useCreditBannerData).toHaveBeenCalledWith(cardId);
});
it('returns null if hookData is null', () => {
expect(el.isEmptyRender()).toEqual(true);
});
}); });
describe('render', () => {
describe('with error state', () => {
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue({
error: true,
ContentComponent,
supportEmail,
});
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
it('passes danger variant to Banner parent', () => {
expect(el.find('Banner').props().variant).toEqual('danger');
});
it('includes credit-error-msg with support email link', () => {
expect(el.find('.credit-error-msg').containsMatchingElement(
formatMessage(messages.error, {
supportEmailLink: (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>),
}),
)).toEqual(true);
});
it('loads ContentComponent with cardId', () => {
expect(el.find('ContentComponent').props().cardId).toEqual(cardId);
});
});
describe('with error state with no email', () => { it('should return null if hook returns null', () => {
beforeEach(() => { hooks.useCreditBannerData.mockReturnValue(null);
hooks.useCreditBannerData.mockReturnValue({ renderCreditBanner();
error: true, const banner = screen.queryByRole('alert');
ContentComponent, expect(banner).toBeNull();
}); });
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
it('includes credit-error-msg without support email link', () => {
expect(el.find('.credit-error-msg').containsMatchingElement(
formatMessage(messages.errorNoEmail),
)).toEqual(true);
});
});
describe('with no error state', () => { it('should render content component without error', () => {
beforeEach(() => { hooks.useCreditBannerData.mockReturnValue({
hooks.useCreditBannerData.mockReturnValue({ ContentComponent: mockContentComponent,
error: false, error: false,
ContentComponent,
supportEmail,
});
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
it('loads ContentComponent with cardId', () => {
expect(el.find('ContentComponent').props().cardId).toEqual(cardId);
});
}); });
renderCreditBanner();
expect(screen.getByTestId('mock-content')).toBeInTheDocument();
expect(screen.queryByTestId('credit-error-msg')).not.toBeInTheDocument();
});
it('should render error message with support email', () => {
hooks.useCreditBannerData.mockReturnValue({
ContentComponent: mockContentComponent,
error: true,
supportEmail: mockSupportEmail,
});
renderCreditBanner();
expect(screen.getByTestId('credit-error-msg')).toBeInTheDocument();
expect(screen.getByText(mockSupportEmail)).toBeInTheDocument();
});
it('should render error message without support email', () => {
hooks.useCreditBannerData.mockReturnValue({
ContentComponent: mockContentComponent,
error: true,
});
renderCreditBanner();
expect(screen.getByTestId('credit-error-msg')).toBeInTheDocument();
expect(screen.queryByText(mockSupportEmail)).not.toBeInTheDocument();
}); });
}); });

View File

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

View File

@@ -1,17 +1,24 @@
import React from 'react'; import React, { useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { useCourseData, useIsMasquerading } from 'hooks';
import CreditContent from './components/CreditContent'; import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink'; import ProviderLink from './components/ProviderLink';
import messages from './messages'; import messages from './messages';
export const ApprovedContent = ({ cardId }) => { export const ApprovedContent = ({ cardId }) => {
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId); const courseData = useCourseData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData(); const { providerStatusUrl: href, providerName } = useMemo(() => {
const creditData = courseData?.credit;
return {
providerStatusUrl: creditData.providerStatusUrl,
providerName: creditData.providerName,
};
}, [courseData]);
const isMasquerading = useIsMasquerading();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<CreditContent <CreditContent

View File

@@ -1,63 +1,72 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils'; import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks'; import { useCourseData, useIsMasquerading } from 'hooks';
import messages from './messages'; import messages from './messages';
import ProviderLink from './components/ProviderLink';
import ApprovedContent from './ApprovedContent'; import ApprovedContent from './ApprovedContent';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { useCourseData: jest.fn(),
useCardCreditData: jest.fn(), useIsMasquerading: jest.fn(),
useMasqueradeData: jest.fn(),
},
})); }));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
let el;
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const credit = { const credit = {
providerStatusUrl: 'test-credit-provider-status-url', providerStatusUrl: 'test-credit-provider-status-url',
providerName: 'test-credit-provider-name', providerName: 'test-credit-provider-name',
}; };
reduxHooks.useCardCreditData.mockReturnValue(credit); useCourseData.mockReturnValue({ credit });
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false }); useIsMasquerading.mockReturnValue(false);
describe('ApprovedContent component', () => { describe('ApprovedContent component', () => {
beforeEach(() => { describe('hooks', () => {
el = shallow(<ApprovedContent cardId={cardId} />);
});
describe('behavior', () => {
it('initializes credit data with cardId', () => { it('initializes credit data with cardId', () => {
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId); render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
expect(useCourseData).toHaveBeenCalledWith(cardId);
}); });
}); });
describe('render', () => { describe('render', () => {
describe('rendered CreditContent component', () => { describe('rendered CreditContent component', () => {
let component; beforeEach(() => {
beforeAll(() => { jest.clearAllMocks();
component = el.find('CreditContent'); render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
}); });
test('action.href from credit.providerStatusUrl', () => { it('action.message is formatted viewCredit message', () => {
expect(component.props().action.href).toEqual(credit.providerStatusUrl); const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).toBeInTheDocument();
expect(actionButton).toHaveTextContent(formatMessage(messages.viewCredit));
}); });
test('action.message is formatted viewCredit message', () => { it('action.href from credit.providerStatusUrl', () => {
expect(component.props().action.message).toEqual(formatMessage(messages.viewCredit)); const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).toHaveAttribute('href', credit.providerStatusUrl);
}); });
test('action.disabled is false', () => { it('action.disabled is false', () => {
expect(component.props().action.disabled).toEqual(false); const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).not.toHaveAttribute('aria-disabled', 'true');
expect(actionButton).not.toHaveClass('disabled');
expect(actionButton).toBeEnabled();
}); });
test('message is formatted approved message', () => { it('message is formatted approved message', () => {
expect(component.props().message).toEqual(formatMessage( const creditMsg = screen.getByTestId('credit-msg');
messages.approved, expect(creditMsg).toBeInTheDocument();
{ expect(creditMsg.textContent).toContain(`${credit.providerName} has approved your request for course credit`);
congratulations: (<b>{formatMessage(messages.congratulations)}</b>), });
linkToProviderSite: <ProviderLink cardId={cardId} />, });
providerName: credit.providerName, describe('when masquerading', () => {
}, beforeEach(() => {
)); useIsMasquerading.mockReturnValue(true);
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
});
it('disables the action button', () => {
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).toHaveAttribute('aria-disabled', 'true');
expect(actionButton).toHaveClass('disabled');
});
it('still renders provider name and link correctly', () => {
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg.textContent).toContain(credit.providerName);
}); });
}); });
}); });

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { useCourseData } from 'hooks';
import track from 'tracking'; import track from 'tracking';
import CreditContent from './components/CreditContent'; import CreditContent from './components/CreditContent';
@@ -11,8 +11,9 @@ import messages from './messages';
export const EligibleContent = ({ cardId }) => { export const EligibleContent = ({ cardId }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { providerName } = reduxHooks.useCardCreditData(cardId); const courseData = useCourseData(cardId);
const { courseId } = reduxHooks.useCardCourseRunData(cardId); const providerName = courseData?.credit?.providerName;
const courseId = courseData?.courseRun?.courseId;
const onClick = track.credit.purchase(courseId); const onClick = track.credit.purchase(courseId);
const getCredit = formatMessage(messages.getCredit); const getCredit = formatMessage(messages.getCredit);

View File

@@ -1,81 +1,65 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { useCourseData } from 'hooks';
import { formatMessage } from 'testUtils';
import track from 'tracking'; import track from 'tracking';
import messages from './messages'; import messages from './messages';
import EligibleContent from './EligibleContent'; import EligibleContent from './EligibleContent';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { useCourseData: jest.fn(),
useCardCreditData: jest.fn(),
useCardCourseRunData: jest.fn(),
},
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('tracking', () => ({
credit: {
purchase: (...args) => ({ trackCredit: args }),
},
})); }));
let el; jest.mock('tracking', () => ({
let component; credit: {
purchase: jest.fn(),
},
}));
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const courseId = 'test-course-id'; const courseId = 'test-course-id';
const credit = { const credit = {
providerName: 'test-credit-provider-name', providerName: 'test-credit-provider-name',
}; };
reduxHooks.useCardCreditData.mockReturnValue(credit); useCourseData.mockReturnValue({ credit, courseRun: { courseId } });
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
const renderEligibleContent = () => render(<IntlProvider locale="en" messages={{}}><EligibleContent cardId={cardId} /></IntlProvider>);
const render = () => {
el = shallow(<EligibleContent cardId={cardId} />);
};
const loadComponent = () => {
component = el.find('CreditContent');
};
describe('EligibleContent component', () => { describe('EligibleContent component', () => {
beforeEach(() => { describe('hooks', () => {
render(); it('initializes credit data with cardId', () => {
renderEligibleContent();
expect(useCourseData).toHaveBeenCalledWith(cardId);
});
}); });
describe('behavior', () => { describe('behavior', () => {
it('initializes credit data with cardId', () => {
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
it('initializes course run data with cardId', () => {
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('rendered CreditContent component', () => { describe('rendered CreditContent component', () => {
beforeEach(() => { it('action message is formatted getCredit message', () => {
loadComponent(); renderEligibleContent();
const button = screen.getByRole('button', { name: messages.getCredit.defaultMessage });
expect(button).toBeInTheDocument();
}); });
test('action.onClick sends credit purchase track event', () => { it('onClick sends credit purchase track event', async () => {
expect(component.props().action.onClick).toEqual( const user = userEvent.setup();
track.credit.purchase(courseId), renderEligibleContent();
); const button = screen.getByRole('button', { name: messages.getCredit.defaultMessage });
await user.click(button);
expect(track.credit.purchase).toHaveBeenCalledWith(courseId);
}); });
test('action.message is formatted getCredit message', () => { it('message is formatted eligible message if provider', () => {
expect(component.props().action.message).toEqual(formatMessage(messages.getCredit)); renderEligibleContent();
const eligibleMessage = screen.getByTestId('credit-msg');
expect(eligibleMessage).toBeInTheDocument();
expect(eligibleMessage).toHaveTextContent(credit.providerName);
}); });
test('message is formatted eligible message if no provider', () => { it('message is formatted eligible message if no provider', () => {
reduxHooks.useCardCreditData.mockReturnValueOnce({}); useCourseData.mockReturnValue({ credit: {}, courseRun: { courseId } });
render(); renderEligibleContent();
loadComponent(); const eligibleMessage = screen.getByTestId('credit-msg');
expect(component.props().message).toEqual(formatMessage( expect(eligibleMessage).toBeInTheDocument();
messages.eligible, expect(eligibleMessage).toHaveTextContent(messages.getCredit.defaultMessage);
{ getCredit: (<b>{formatMessage(messages.getCredit)}</b>) },
));
});
test('message is formatted eligible message if provider', () => {
expect(component.props().message).toEqual(
formatMessage(messages.eligibleFromProvider, { providerName: credit.providerName }),
);
}); });
}); });
}); });

View File

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

View File

@@ -1,73 +1,102 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import userEvent from '@testing-library/user-event';
import { formatMessage } from 'testUtils'; import { useCourseData, useIsMasquerading } from 'hooks';
import { reduxHooks } from 'hooks';
import messages from './messages'; import messages from './messages';
import hooks from './hooks'; import hooks from './hooks';
import ProviderLink from './components/ProviderLink';
import MustRequestContent from './MustRequestContent'; import MustRequestContent from './MustRequestContent';
jest.mock('./hooks', () => ({ jest.mock('./hooks', () => ({
useCreditRequestData: jest.fn(), useCreditRequestData: jest.fn(),
})); }));
jest.mock('hooks', () => ({
reduxHooks: { useMasqueradeData: jest.fn() },
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
let el; jest.mock('hooks', () => ({
let component; useCourseData: jest.fn(),
useIsMasquerading: jest.fn(),
}));
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const requestData = { test: 'requestData' }; const requestData = {
const createCreditRequest = jest.fn().mockName('createCreditRequest'); url: 'test-request-data-url',
hooks.useCreditRequestData.mockReturnValue({ parameters: {
requestData, key1: 'val1',
createCreditRequest, key2: 'val2',
}); key3: 'val3',
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false }); },
const render = () => {
el = shallow(<MustRequestContent cardId={cardId} />);
}; };
const providerName = 'test-credit-provider-name';
const providerStatusUrl = 'test-credit-provider-status-url';
const createCreditRequest = jest.fn().mockName('createCreditRequest');
const renderMustRequestContent = () => render(
<IntlProvider locale="en" messages={messages}>
<MustRequestContent cardId={cardId} />
</IntlProvider>,
);
describe('MustRequestContent component', () => { describe('MustRequestContent component', () => {
beforeEach(() => { beforeEach(() => {
render(); jest.clearAllMocks();
hooks.useCreditRequestData.mockReturnValue({
requestData,
createCreditRequest,
});
useIsMasquerading.mockReturnValue(false);
useCourseData.mockReturnValue({
credit: {
providerName,
providerStatusUrl,
},
});
}); });
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit request data with cardId', () => { it('initializes credit request data with cardId', () => {
renderMustRequestContent();
expect(hooks.useCreditRequestData).toHaveBeenCalledWith(cardId); expect(hooks.useCreditRequestData).toHaveBeenCalledWith(cardId);
}); });
}); });
describe('render', () => {
describe('rendered CreditContent component', () => { describe('behavior', () => {
describe('rendered content', () => {
beforeEach(() => { beforeEach(() => {
component = el.find('CreditContent'); renderMustRequestContent();
}); });
test('action.onClick calls createCreditRequest from useCreditRequestData hook', () => {
expect(component.props().action.onClick).toEqual(createCreditRequest); it('calls createCreditRequest when request credit button is clicked', async () => {
const user = userEvent.setup();
const button = screen.getByRole('button', { name: /request credit/i });
await user.click(button);
expect(createCreditRequest).toHaveBeenCalled();
}); });
test('action.message is formatted requestCredit message', () => {
expect(component.props().action.message).toEqual( it('shows request credit button that is enabled', () => {
formatMessage(messages.requestCredit), const button = screen.getByRole('button', { name: /request credit/i });
); expect(button).toBeEnabled();
}); });
test('action.disabled is false', () => {
expect(component.props().action.disabled).toEqual(false); it('displays must request message with provider link', () => {
expect(screen.getByTestId('credit-msg')).toHaveTextContent(/request credit/i);
}); });
test('message is formatted mustRequest message', () => {
expect(component.props().message).toEqual( it('renders credit request form with correct data', () => {
formatMessage(messages.mustRequest, { const { container } = renderMustRequestContent();
linkToProviderSite: <ProviderLink cardId={cardId} />, const form = container.querySelector('form');
requestCredit: <b>{formatMessage(messages.requestCredit)}</b>, expect(form).toBeInTheDocument();
}), expect(form).toHaveAttribute('action', requestData.url);
);
}); });
test('requestData drawn from useCreditRequestData hook', () => { });
expect(component.props().requestData).toEqual(requestData);
describe('when masquerading', () => {
beforeEach(() => {
useIsMasquerading.mockReturnValue(true);
renderMustRequestContent();
});
it('disables the request credit button', () => {
const button = screen.getByRole('button', { name: /request credit/i });
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });
}); });

View File

@@ -3,13 +3,14 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { useCourseData, useIsMasquerading } from 'hooks';
import CreditContent from './components/CreditContent'; import CreditContent from './components/CreditContent';
import messages from './messages'; import messages from './messages';
export const PendingContent = ({ cardId }) => { export const PendingContent = ({ cardId }) => {
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId); const courseData = useCourseData(cardId);
const { isMasquerading } = reduxHooks.useMasqueradeData(); const { providerStatusUrl: href, providerName } = courseData?.credit || {};
const isMasquerading = useIsMasquerading();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<CreditContent <CreditContent

View File

@@ -1,62 +1,68 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useCourseData, useIsMasquerading } from 'hooks';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import messages from './messages'; import messages from './messages';
import PendingContent from './PendingContent'; import PendingContent from './PendingContent';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() }, useCourseData: jest.fn(),
useIsMasquerading: jest.fn(),
})); }));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
let el;
let component;
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const providerName = 'test-credit-provider-name'; const providerName = 'test-credit-provider-name';
const providerStatusUrl = 'test-credit-provider-status-url'; const providerStatusUrl = 'test-credit-provider-status-url';
reduxHooks.useCardCreditData.mockReturnValue({ useIsMasquerading.mockReturnValue(false);
providerName, useCourseData.mockReturnValue({
providerStatusUrl, credit: {
providerName,
providerStatusUrl,
},
}); });
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
const render = () => { const renderPendingContent = () => render(
el = shallow(<PendingContent cardId={cardId} />); <IntlProvider messages={{}} locale="en">
}; <PendingContent cardId={cardId} />
</IntlProvider>,
);
describe('PendingContent component', () => { describe('PendingContent component', () => {
beforeEach(() => { describe('hooks', () => {
render(); it('initializes card credit data with cardId', () => {
renderPendingContent();
expect(useCourseData).toHaveBeenCalledWith(cardId);
});
}); });
describe('behavior', () => { describe('behavior', () => {
it('initializes card credit data with cardId', () => {
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('rendered CreditContent component', () => { describe('rendered CreditContent component', () => {
beforeEach(() => { it('action message is formatted requestCredit message', () => {
component = el.find('CreditContent'); renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toBeInTheDocument();
}); });
test('action.href will go to provider status site', () => { it('action href will go to provider status site', () => {
expect(component.props().action.href).toEqual(providerStatusUrl); renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toHaveAttribute('href', providerStatusUrl);
}); });
test('action.message is formatted requestCredit message', () => { it('action.disabled is false', () => {
expect(component.props().action.message).toEqual( renderPendingContent();
formatMessage(messages.viewDetails), const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
); expect(button).not.toHaveClass('disabled');
}); });
test('action.disabled is false', () => { it('message is formatted pending message with provider name', () => {
expect(component.props().action.disabled).toEqual(false); renderPendingContent();
const component = screen.getByTestId('credit-msg');
expect(component).toBeInTheDocument();
expect(component).toHaveTextContent(`${providerName} has received`);
}); });
test('message is formatted pending message', () => { describe('when masqueradeData is true', () => {
expect(component.props().message).toEqual( it('disables the view details button', () => {
formatMessage(messages.received, { providerName }), useIsMasquerading.mockReturnValue(true);
); renderPendingContent();
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
expect(button).toHaveClass('disabled');
});
}); });
}); });
}); });

View File

@@ -3,18 +3,19 @@ import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks'; import { useCourseData } from 'hooks';
import CreditContent from './components/CreditContent'; import CreditContent from './components/CreditContent';
import ProviderLink from './components/ProviderLink'; import ProviderLink from './components/ProviderLink';
import messages from './messages'; import messages from './messages';
export const RejectedContent = ({ cardId }) => { export const RejectedContent = ({ cardId }) => {
const credit = reduxHooks.useCardCreditData(cardId); const courseData = useCourseData(cardId);
const credit = courseData?.credit;
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<CreditContent <CreditContent
message={formatMessage(messages.rejected, { message={formatMessage(messages.rejected, {
providerName: credit.providerName, providerName: credit?.providerName,
linkToProviderSite: (<ProviderLink cardId={cardId} />), linkToProviderSite: (<ProviderLink cardId={cardId} />),
})} })}
/> />

View File

@@ -1,53 +1,43 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils'; import { useCourseData } from 'hooks';
import { reduxHooks } from 'hooks';
import messages from './messages';
import ProviderLink from './components/ProviderLink';
import RejectedContent from './RejectedContent'; import RejectedContent from './RejectedContent';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { useCourseData: jest.fn(),
useCardCreditData: jest.fn(),
},
})); }));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
const cardId = 'test-card-id'; const cardId = 'test-card-id';
const credit = { const credit = {
providerStatusUrl: 'test-credit-provider-status-url', providerStatusUrl: 'test-credit-provider-status-url',
providerName: 'test-credit-provider-name', providerName: 'test-credit-provider-name',
}; };
reduxHooks.useCardCreditData.mockReturnValue(credit); useCourseData.mockReturnValue({
credit,
});
let el; const renderRejectedContent = () => render(<IntlProvider><RejectedContent cardId={cardId} /></IntlProvider>);
let component;
const render = () => { el = shallow(<RejectedContent cardId={cardId} />); };
const loadComponent = () => { component = el.find('CreditContent'); };
describe('RejectedContent component', () => { describe('RejectedContent component', () => {
beforeEach(render); describe('hooks', () => {
describe('behavior', () => {
it('initializes credit data with cardId', () => { it('initializes credit data with cardId', () => {
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId); renderRejectedContent();
expect(useCourseData).toHaveBeenCalledWith(cardId);
}); });
}); });
describe('render', () => { describe('render', () => {
describe('rendered CreditContent component', () => { describe('rendered CreditContent component', () => {
beforeAll(loadComponent); it('no action is passed', () => {
test('no action is passed', () => { renderRejectedContent();
expect(component.props().action).toEqual(undefined); const action = screen.queryByTestId('action-row-btn');
expect(action).not.toBeInTheDocument();
}); });
test('message is formatted rejected message', () => { it('message is formatted rejected message', () => {
expect(component.props().message).toEqual(formatMessage( renderRejectedContent();
messages.rejected, const message = screen.getByTestId('credit-msg');
{ expect(message).toBeInTheDocument();
linkToProviderSite: <ProviderLink cardId={cardId} />, expect(message).toHaveTextContent(`${credit.providerName} did not approve your request for course credit.`);
providerName: credit.providerName,
},
));
}); });
}); });
}); });

View File

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

View File

@@ -1,9 +1,7 @@
import React from 'react'; import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import CreditContent from './CreditContent'; import CreditContent from './CreditContent';
let el;
const action = { const action = {
href: 'test-action-href', href: 'test-action-href',
onClick: jest.fn().mockName('test-action-onClick'), onClick: jest.fn().mockName('test-action-onClick'),
@@ -15,45 +13,57 @@ const message = 'test-message';
const requestData = { url: 'test-request-data-url', parameters: { key1: 'val1' } }; const requestData = { url: 'test-request-data-url', parameters: { key1: 'val1' } };
const props = { action, message, requestData }; const props = { action, message, requestData };
const renderCreditContent = (data) => render(
<CreditContent {...data} />,
);
describe('CreditContent component', () => { describe('CreditContent component', () => {
describe('render', () => { describe('render', () => {
describe('with action', () => { describe('with action', () => {
beforeEach(() => { it('loads href and message into action row button', () => {
el = shallow(<CreditContent {...props} />); renderCreditContent(props);
}); const button = screen.getByRole('link', { name: action.message });
test('snapshot', () => { expect(button).toBeInTheDocument();
expect(el).toMatchSnapshot(); expect(button).toHaveAttribute('href', action.href);
}); expect(button).not.toHaveAttribute('disabled');
it('loads href, onClick, and message into action row button', () => {
const buttonEl = el.find('ActionRow Button');
expect(buttonEl.props().href).toEqual(action.href);
expect(buttonEl.props().onClick).toEqual(action.onClick);
expect(buttonEl.props().disabled).toEqual(action.disabled);
expect(buttonEl.text()).toEqual(action.message);
}); });
it('loads message into credit-msg div', () => { it('loads message into credit-msg div', () => {
expect(el.find('div.credit-msg').text()).toEqual(message); renderCreditContent(props);
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg).toBeInTheDocument();
expect(creditMsg.innerHTML).toEqual(message);
}); });
it('loads CreditRequestForm with passed requestData', () => { it('loads CreditRequestForm with passed requestData', () => {
expect(el.find('CreditRequestForm').props().requestData).toEqual(requestData); const { container } = renderCreditContent(props);
const creditForm = container.querySelector('form');
expect(creditForm).toBeInTheDocument();
expect(creditForm).toHaveAttribute('action', requestData.url);
}); });
test('disables action button when action.disabled is true', () => { it('disables action button when action.disabled is true', () => {
el.setProps({ action: { ...action, disabled: true } }); renderCreditContent({ ...props, action: { ...action, disabled: true } });
expect(el.find('ActionRow Button').props().disabled).toEqual(true); const button = screen.getByRole('link', { name: action.message });
expect(button).toBeInTheDocument();
expect(button).toHaveClass('disabled');
expect(button).toHaveAttribute('aria-disabled', 'true');
}); });
}); });
describe('without action', () => { describe('without action', () => {
test('snapshot', () => {
el = shallow(<CreditContent {...{ message, requestData }} />);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
it('loads message into credit-msg div', () => { it('loads message into credit-msg div', () => {
expect(el.find('div.credit-msg').text()).toEqual(message); renderCreditContent({ message, requestData });
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg).toBeInTheDocument();
expect(creditMsg.innerHTML).toEqual(message);
}); });
it('loads CreditRequestForm with passed requestData', () => { it('loads CreditRequestForm with passed requestData', () => {
expect(el.find('CreditRequestForm').props().requestData).toEqual(requestData); const { container } = renderCreditContent({ message, requestData });
const creditForm = container.querySelector('form');
expect(creditForm).toBeInTheDocument();
expect(creditForm).toHaveAttribute('action', requestData.url);
});
it('does not render action row button', () => {
renderCreditContent({ message, requestData });
const button = screen.queryByRole('link', { name: action.message });
expect(button).not.toBeInTheDocument();
}); });
}); });
}); });

View File

@@ -1,32 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditRequestForm component render output valid requestData snapshot 1`] = `
<Form
accept-method="UTF-8"
action="test-request-data-url"
className="hidden"
method="POST"
>
<FormControl
as="textarea"
key="key1"
name="key1"
value="val1"
/>
<FormControl
as="textarea"
key="key2"
name="key2"
value="val2"
/>
<FormControl
as="textarea"
key="key3"
name="key3"
value="val3"
/>
<Button
type="submit"
/>
</Form>
`;

View File

@@ -4,6 +4,12 @@ import useCreditRequestFormData from './hooks';
const requestData = 'test-request-data'; const requestData = 'test-request-data';
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn((val) => ({ current: val, useRef: true })),
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
}));
let out; let out;
const ref = { const ref = {
current: { click: jest.fn() }, current: { click: jest.fn() },

View File

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

View File

@@ -1,5 +1,4 @@
import React from 'react'; import { render } from '@testing-library/react';
import { shallow } from 'enzyme';
import { keyStore } from 'utils'; import { keyStore } from 'utils';
@@ -11,7 +10,8 @@ jest.mock('./hooks', () => ({
default: jest.fn(), default: jest.fn(),
})); }));
const ref = 'test-ref'; const ref = { current: { click: jest.fn() }, useRef: jest.fn() };
const requestData = { const requestData = {
url: 'test-request-data-url', url: 'test-request-data-url',
parameters: { parameters: {
@@ -25,40 +25,41 @@ const paramKeys = keyStore(requestData.parameters);
useCreditRequestFormData.mockReturnValue({ ref }); useCreditRequestFormData.mockReturnValue({ ref });
let el; const renderForm = (data) => render(<CreditRequestForm requestData={data} />);
const shallowRender = (data) => { el = shallow(<CreditRequestForm requestData={data} />); };
describe('CreditRequestForm component', () => { describe('CreditRequestForm component', () => {
describe('behavior', () => { beforeEach(() => {
jest.clearAllMocks();
});
describe('hooks', () => {
it('initializes ref from hook with requestData', () => { it('initializes ref from hook with requestData', () => {
shallowRender(requestData); renderForm(requestData);
expect(useCreditRequestFormData).toHaveBeenCalledWith(requestData); expect(useCreditRequestFormData).toHaveBeenCalledWith(requestData);
}); });
}); });
describe('render output', () => { describe('render output', () => {
describe('null requestData', () => { describe('null requestData', () => {
it('returns null', () => { it('returns null', () => {
shallowRender(null); const { container } = renderForm(null);
expect(el.isEmptyRender()).toEqual(true); expect(container.firstChild).toBeNull();
}); });
}); });
describe('valid requestData', () => { describe('valid requestData', () => {
beforeEach(() => {
shallowRender(requestData);
});
test('snapshot', () => {
expect(el).toMatchSnapshot();
});
it('loads Form with requestData url', () => { it('loads Form with requestData url', () => {
expect(el.find('Form').props().action).toEqual(requestData.url); const { container } = renderForm(requestData);
const creditForm = container.querySelector('form');
expect(creditForm).toBeInTheDocument();
expect(creditForm).toHaveAttribute('action', requestData.url);
}); });
it('loads a textarea form control for each requestData parameter', () => { it('loads a textarea form control for each requestData parameter', () => {
const controls = el.find('FormControl'); const { container } = renderForm(requestData);
expect(controls.at(0).props().name).toEqual(paramKeys.key1); const controls = container.querySelectorAll('textarea');
expect(controls.at(0).props().value).toEqual(requestData.parameters.key1); expect(controls.length).toEqual(Object.keys(requestData.parameters).length);
expect(controls.at(1).props().name).toEqual(paramKeys.key2); expect(controls[0]).toHaveAttribute('name', paramKeys.key1);
expect(controls.at(1).props().value).toEqual(requestData.parameters.key2); expect(controls[0]).toHaveValue(requestData.parameters.key1);
expect(controls.at(2).props().name).toEqual(paramKeys.key3); expect(controls[1]).toHaveAttribute('name', paramKeys.key2);
expect(controls.at(2).props().value).toEqual(requestData.parameters.key3); expect(controls[1]).toHaveValue(requestData.parameters.key2);
expect(controls[2]).toHaveAttribute('name', paramKeys.key3);
expect(controls[2]).toHaveValue(requestData.parameters.key3);
}); });
}); });
}); });

View File

@@ -4,9 +4,6 @@ import { render } from '@testing-library/react';
import useCreditRequestFormData from './hooks'; import useCreditRequestFormData from './hooks';
import CreditRequestForm from '.'; import CreditRequestForm from '.';
jest.unmock('@edx/paragon');
jest.unmock('react');
jest.mock('./hooks', () => ({ jest.mock('./hooks', () => ({
__esModule: true, __esModule: true,
default: jest.fn(), default: jest.fn(),

View File

@@ -2,11 +2,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { reduxHooks } from 'hooks'; import { useCourseData } from 'hooks';
import { Hyperlink } from '@edx/paragon'; import { Hyperlink } from '@openedx/paragon';
export const ProviderLink = ({ cardId }) => { export const ProviderLink = ({ cardId }) => {
const credit = reduxHooks.useCardCreditData(cardId); const courseData = useCourseData(cardId);
const credit = courseData?.credit || {};
return ( return (
<Hyperlink <Hyperlink
href={credit.providerStatusUrl} href={credit.providerStatusUrl}

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