Compare commits

...

149 Commits

Author SHA1 Message Date
MaxFrank13
0cb73b4356 test: lint 2025-07-01 18:11:49 +00:00
MaxFrank13
3030140e17 test: testing mock configurations 2025-07-01 18:09:34 +00:00
diana-villalvazo-wgu
d542fb84b6 test: remove paragon, react and i18n mocks and added when needed 2025-06-30 16:15:52 -06: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
220 changed files with 9416 additions and 15113 deletions

5
.env
View File

@@ -32,7 +32,6 @@ ENTERPRISE_MARKETING_UTM_SOURCE=''
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
LEARNING_BASE_URL=''
ZENDESK_KEY=''
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
@@ -41,3 +40,7 @@ ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL='http://localhost:18000/support'
SUPPORT_URL=''
CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
@@ -38,7 +38,6 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
LEARNING_BASE_URL='http://localhost:2000'
SESSION_COOKIE_DOMAIN='localhost'
ZENDESK_KEY=''
HOTJAR_APP_ID=''
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
@@ -47,3 +46,7 @@ ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=false
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
SEGMENT_KEY=''
FEATURE_FLAGS={}
MARKETING_SITE_BASE_URL='http://localhost:18000'
SUPPORT_URL='http://localhost:18000/support'
SUPPORT_URL=''
CONTACT_URL='http://localhost:18000/contact'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
@@ -37,7 +37,6 @@ 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'
ZENDESK_KEY='test-zendesk-key'
HOTJAR_APP_ID='hot-jar-app-id'
HOTJAR_VERSION='6'
HOTJAR_DEBUG=''
@@ -46,3 +45,6 @@ ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''
ENABLE_EDX_PERSONAL_DASHBOARD=true
ENABLE_PROGRAMS=false
NON_BROWSABLE_COURSES=false
PARAGON_THEME_URLS={}

View File

@@ -11,9 +11,6 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
steps:
- name: Checkout
@@ -22,7 +19,7 @@ jobs:
- name: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci
@@ -40,24 +37,7 @@ jobs:
run: npm run build
- name: Run Coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Send failure notification
if: ${{ failure() }}
uses: dawidd6/action-send-mail@v3
with:
server_address: email-smtp.us-east-1.amazonaws.com
server_port: 465
username: ${{ secrets.EDX_SMTP_USERNAME }}
password: ${{ secrets.EDX_SMTP_PASSWORD }}
subject: CI workflow failed in ${{github.repository}}
to: masters-grades@edx.org,aperture@2u-internal.opsgenie.net
from: github-actions <github-actions@edx.org>
nodemailerlog: true
nodemailerdebug: true
body: CI workflow in ${{github.repository}} failed!
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
}}"

View File

@@ -1,35 +0,0 @@
name: Release CI
on:
push:
tags:
- "*"
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v4
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

View File

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

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

@@ -17,6 +17,7 @@ metadata:
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'

View File

@@ -24,6 +24,7 @@ module.exports = {
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',
@@ -59,7 +60,6 @@ module.exports = {
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
LEARNING_BASE_URL: 'http://localhost:2000',
SESSION_COOKIE_DOMAIN: 'localhost',
ZENDESK_KEY: '',
HOTJAR_APP_ID: '',
HOTJAR_VERSION: 6,
HOTJAR_DEBUG: '',

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}

15305
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,9 @@
"type": "git",
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
},
"browserslist": [
"extends @edx/browserslist-config"
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
@@ -13,11 +16,10 @@
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
"semantic-release": "semantic-release",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/learner-dashboard/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
"quality": "npm run lint-fix && npm run test",
"watch-tests": "jest --watch",
"snapshot": "fedx-scripts jest --updateSnapshot",
"prepare": "husky install"
"watch-tests": "jest --watch"
},
"author": "edX",
"license": "AGPL-3.0",
@@ -27,70 +29,58 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "^1.1.0",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-enterprise-hotjar": "3.0.0",
"@edx/frontend-platform": "8.1.2",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "3.0.0",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-enterprise-hotjar": "7.2.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.7.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.2.0",
"@openedx/frontend-plugin-framework": "^1.2.0",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.2.2",
"@redux-beacon/segment": "^1.1.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.5",
"@redux-devtools/extension": "3.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/user-event": "^13.5.0",
"@reduxjs/toolkit": "^2.0.0",
"classnames": "^2.3.1",
"core-js": "3.38.1",
"dompurify": "^2.3.1",
"email-prop-type": "^3.0.1",
"file-saver": "^2.0.5",
"filesize": "^8.0.6",
"core-js": "3.42.0",
"filesize": "^10.0.0",
"font-awesome": "4.7.0",
"history": "5.3.0",
"html-react-parser": "^1.3.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-when": "^3.6.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-intl": "6.8.0",
"react-pdf": "^7.0.0",
"react-intl": "6.8.9",
"react-redux": "^7.2.4",
"react-router-dom": "6.27.0",
"react-router-dom": "6.29.0",
"react-share": "^4.4.0",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux-beacon": "^2.1.0",
"redux-logger": "3.0.6",
"redux-thunk": "2.4.2",
"regenerator-runtime": "^0.14.0",
"reselect": "^4.0.0",
"universal-cookie": "^4.0.4",
"util": "^0.12.4",
"whatwg-fetch": "^3.6.2"
"util": "^0.12.4"
},
"devDependencies": {
"@edx/browserslist-config": "^1.3.0",
"@edx/reactifex": "^2.1.1",
"@openedx/frontend-build": "14.1.5",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"copy-webpack-plugin": "^12.0.0",
"husky": "^9.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-expect-message": "^1.1.3",
"jest-when": "^3.6.0",
"react-dev-utils": "^12.0.0",
"react-test-renderer": "^17.0.2",
"redux-mock-store": "^1.5.4",
"semantic-release": "^20.1.3"
"react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4"
}
}

View File

@@ -6,7 +6,7 @@ import { logError } from '@edx/frontend-platform/logging';
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
import FooterSlot from '@openedx/frontend-slot-footer';
import { FooterSlot } from '@edx/frontend-component-footer';
import { Alert } from '@openedx/paragon';
import { RequestKeys } from 'data/constants/requests';
@@ -80,7 +80,7 @@ export const App = () => {
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<main id="main">
{hasNetworkFailure
? (
<Alert variant="danger">

View File

@@ -1,13 +1,10 @@
// frontend-app-*/src/index.scss
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
$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";

View File

@@ -1,24 +1,19 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, waitFor } from '@testing-library/react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { RequestKeys } from 'data/constants/requests';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import AppWrapper from 'containers/WidgetContainers/AppWrapper';
import { App } from './App';
import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'Footer' }));
jest.mock('containers/Dashboard', () => 'Dashboard');
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('containers/WidgetContainers/AppWrapper', () => 'AppWrapper');
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/WidgetContainers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
actions: 'redux.actions',
@@ -37,119 +32,103 @@ jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({})),
}));
// jest.mock('@edx/frontend-platform/react', () => ({
// ...jest.requireActual('@edx/frontend-platform/react'),
// ErrorPage: () => 'ErrorPage',
// }));
const loadData = jest.fn();
reduxHooks.useLoadData.mockReturnValue(loadData);
let el;
const supportEmail = 'test-support-url';
const supportEmail = 'test@support.com';
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
describe('App router component', () => {
const { formatMessage } = useIntl();
describe('component', () => {
const runBasicTests = () => {
test('snapshot', () => { expect(el.snapshot).toMatchSnapshot(); });
it('displays title in helmet component', () => {
const control = el.instance
.findByType(Helmet)[0]
.findByType('title')[0];
expect(control.children[0].el).toEqual(formatMessage(messages.pageTitle));
it('displays title in helmet component', async () => {
await waitFor(() => expect(document.title).toEqual(messages.pageTitle.defaultMessage));
});
it('displays learner dashboard header', () => {
expect(el.instance.findByType(LearnerDashboardHeader).length).toEqual(1);
const learnerDashboardHeader = screen.getByText('LearnerDashboardHeader');
expect(learnerDashboardHeader).toBeInTheDocument();
});
it('wraps the header and main components in an AppWrapper widget container', () => {
const container = el.instance.findByType(AppWrapper)[0];
expect(container.children[0].type).toEqual('LearnerDashboardHeader');
expect(container.children[1].type).toEqual('main');
const appWrapper = screen.getByText('LearnerDashboardHeader').parentElement;
expect(appWrapper).toHaveClass('AppWrapper');
expect(appWrapper.children[1].id).toEqual('main');
});
it('displays footer slot', () => {
const footerSlot = screen.getByText('FooterSlot');
expect(footerSlot).toBeInTheDocument();
});
};
describe('no network failure', () => {
beforeAll(() => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({});
el = shallow(<App />);
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
});
});
describe('no network failure with optimizely url', () => {
beforeAll(() => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
el = shallow(<App />);
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
});
});
describe('no network failure with optimizely project id', () => {
beforeAll(() => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockReturnValue(false);
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
el = shallow(<App />);
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads dashboard', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const dashboard = main.children[0];
expect(dashboard.type).toEqual('Dashboard');
expect(
dashboard.matches(shallow(<Dashboard />)),
).toEqual(true);
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
});
});
describe('initialize failure', () => {
beforeAll(() => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
getConfig.mockReturnValue({});
el = shallow(<App />);
render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
});
runBasicTests();
it('loads error page', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const alert = main.children[0];
expect(alert.type).toEqual('Alert');
expect(alert.children.length).toEqual(1);
const errorPage = alert.children[0];
expect(errorPage.type).toEqual('ErrorPage');
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
const errorPage = screen.getByText('ErrorPage');
expect(errorPage).toBeInTheDocument();
});
});
describe('refresh failure', () => {
beforeAll(() => {
beforeEach(() => {
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.refreshList);
getConfig.mockReturnValue({});
el = shallow(<App />);
render(<IntlProvider locale="en"><App /></IntlProvider>);
});
runBasicTests();
it('loads error page', () => {
const main = el.instance.findByType('main')[0];
expect(main.children.length).toEqual(1);
const alert = main.children[0];
expect(alert.type).toEqual('Alert');
expect(alert.children.length).toEqual(1);
const errorPage = alert.children[0];
expect(errorPage.type).toEqual('ErrorPage');
expect(errorPage.props.message).toEqual(formatMessage(messages.errorMessage, { supportEmail }));
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
const errorPage = screen.getByText('ErrorPage');
expect(errorPage).toBeInTheDocument();
});
});
});

View File

@@ -1,143 +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>
<FooterSlot />
</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>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
</div>
</Fragment>
`;
exports[`App router component component no network failure with optimizely project id snapshot 1`] = `
<Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
</div>
</Fragment>
`;
exports[`App router component component no network failure with optimizely url snapshot 1`] = `
<Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<title>
Learner Home
</title>
<link
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<div>
<AppWrapper>
<LearnerDashboardHeader />
<main>
<Dashboard />
</main>
</AppWrapper>
<FooterSlot />
</div>
</Fragment>
`;
exports[`App router component component refresh failure snapshot 1`] = `
<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>
<FooterSlot />
</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={
{
"redux": "store",
}
}
wrapWithRouter={true}
>
<NoticesWrapper>
<Routes>
<Route
element={
<PageWrap>
<App />
</PageWrap>
}
path="/"
/>
<Route
element={
<Navigate
replace={true}
to="/"
/>
}
path="*"
/>
</Routes>
</NoticesWrapper>
</AppProvider>
`;

View File

@@ -1,27 +1,30 @@
import { shallow } from '@edx/react-unit-test-utils';
import { Alert } from '@openedx/paragon';
import { render, screen } from '@testing-library/react';
import Banner from './Banner';
describe('Banner', () => {
const props = {
children: 'Hello, world!',
};
describe('snapshot', () => {
test('renders default banner', () => {
const wrapper = shallow(<Banner {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('renders with variants', () => {
const wrapper = shallow(<Banner {...props} variant="success" />);
expect(wrapper.snapshot).toMatchSnapshot();
jest.unmock('@openedx/paragon');
jest.unmock('react');
expect(wrapper.instance.findByType(Alert)[0].props.variant).toEqual('success');
});
test('renders with custom class', () => {
const wrapper = shallow(<Banner {...props} className="custom-class" />);
expect(wrapper.snapshot).toMatchSnapshot();
});
describe('Banner component', () => {
it('renders children content', () => {
render(<Banner>Test content</Banner>);
expect(screen.getByText('Test content')).toBeInTheDocument();
});
it('uses default props correctly', () => {
render(<Banner>Test content</Banner>);
const banner = screen.getByRole('alert');
expect(banner).toHaveClass('mb-0');
});
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

@@ -3,16 +3,15 @@ import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-
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 }) => {
export const getNotices = ({ onLoad, notFoundMessage }) => {
const authenticatedUser = getAuthenticatedUser();
const handleError = async (e) => {
// Error probably means that notices is not installed, which is fine.
const { customAttributes: { httpErrorStatus } } = e;
if (httpErrorStatus === 404) {
logInfo(`${e}. ${error404Message}`);
logInfo(`${e}. ${notFoundMessage}`);
} else {
logError(e);
}

View File

@@ -1,9 +1,11 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { StrictDict } from 'utils';
import { getNotices } from './api';
import * as module from './hooks';
import messages from './messages';
/**
* This component uses the platform-plugin-notices plugin to function.
@@ -17,6 +19,8 @@ export const state = StrictDict({
export const useNoticesWrapperData = () => {
const [isRedirected, setIsRedirected] = module.state.isRedirected();
const { formatMessage } = useIntl();
React.useEffect(() => {
if (getConfig().ENABLE_NOTICES) {
getNotices({
@@ -26,9 +30,10 @@ export const useNoticesWrapperData = () => {
window.location.replace(`${data.data.results[0]}?next=${window.location.href}`);
}
},
notFoundMessage: formatMessage(messages.error404Message),
});
}
}, [setIsRedirected]);
}, [setIsRedirected, formatMessage]);
return { isRedirected };
};

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { MockUseState } from 'testUtils';
import { MockUseState, formatMessage } from 'testUtils';
import { getConfig } from '@edx/frontend-platform';
import { getNotices } from './api';
@@ -9,6 +9,22 @@ import * as hooks from './hooks';
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
jest.mock('./api', () => ({ getNotices: jest.fn() }));
// jest.mock('react', () => ({
// ...jest.requireActual('react'),
// useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
// useContext: jest.fn(context => context),
// }));
// jest.mock('@edx/frontend-platform/i18n', () => {
// const { formatMessage: fn } = jest.requireActual('testUtils');
// return {
// ...jest.requireActual('@edx/frontend-platform/i18n'),
// useIntl: () => ({
// formatMessage: fn,
// }),
// };
// });
getConfig.mockReturnValue({ ENABLE_NOTICES: true });
const state = new MockUseState(hooks);
@@ -34,7 +50,7 @@ describe('NoticesWrapper hooks', () => {
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
hooks.useNoticesWrapperData();
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected]);
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb();
expect(getNotices).not.toHaveBeenCalled();
});
@@ -43,7 +59,7 @@ describe('NoticesWrapper hooks', () => {
hooks.useNoticesWrapperData();
expect(React.useEffect).toHaveBeenCalled();
const [cb, prereqs] = React.useEffect.mock.calls[0];
expect(prereqs).toEqual([state.setState.isRedirected]);
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb();
expect(getNotices).toHaveBeenCalled();
const { onLoad } = getNotices.mock.calls[0][0];
@@ -59,7 +75,7 @@ describe('NoticesWrapper hooks', () => {
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]);
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
cb();
expect(getNotices).toHaveBeenCalled();
const { onLoad } = getNotices.mock.calls[0][0];

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import useNoticesWrapperData from './hooks';
import NoticesWrapper from '.';
@@ -7,30 +6,31 @@ 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', () => {
beforeEach(() => {
useNoticesWrapperData.mockClear();
});
describe('behavior', () => {
it('initializes hooks', () => {
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
useNoticesWrapperData.mockReturnValue(hookProps);
render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(useNoticesWrapperData).toHaveBeenCalledWith();
});
});
describe('output', () => {
it('does not show children if redirected', () => {
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
expect(el.instance.children.length).toEqual(0);
render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(screen.queryByText('some')).not.toBeInTheDocument();
expect(screen.queryByText('children')).not.toBeInTheDocument();
});
it('shows children if not redirected', () => {
el = shallow(<NoticesWrapper>{children}</NoticesWrapper>);
expect(el.instance.children.length).toEqual(2);
expect(el.instance.children[0].type).toEqual(shallow(children[0]).type);
expect(el.instance.props).toEqual(shallow(children[0]).props);
expect(el.instance.children[1].type).toEqual(shallow(children[1]).type);
expect(el.instance.props).toEqual(shallow(children[1]).props);
useNoticesWrapperData.mockReturnValue(hookProps);
render(<NoticesWrapper>{children}</NoticesWrapper>);
expect(screen.getByText('some')).toBeInTheDocument();
expect(screen.getByText('children')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
error404Message: {
id: 'learner-dash.notices.error404Message',
defaultMessage: 'This probably happened because the notices plugin is not installed on platform.',
description: 'Error message when notices API returns 404',
},
});
export default messages;

View File

@@ -1,65 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ZendeskFab snapshot 1`] = `
<Zendesk
cookies={true}
defer={true}
webWidget={
{
"answerBot": {
"avatar": {
"name": {
"*": "edX Support",
},
"url": "https://edx-cdn.org/v3/prod/favicon.ico",
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": {
"*": "edX Support",
},
},
"chat": {
"departments": {
"enabled": [
"account settings",
"billing and payments",
"certificates",
"deadlines",
"errors and technical issues",
"other",
"proctoring",
],
},
"suppress": false,
},
"contactForm": {
"attachments": true,
"selectTicketForm": {
"*": "Please choose your request type:",
},
"ticketForms": [
{
"fields": [
{
"id": "description",
"prefill": {
"*": "",
},
},
],
"id": 360003368814,
"subject": false,
},
],
},
"contactOptions": {
"enabled": false,
},
"helpCenter": {
"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 '@edx/react-unit-test-utils';
import ZendeskFab from '.';
jest.mock('react-zendesk', () => 'Zendesk');
describe('ZendeskFab', () => {
test('snapshot', () => {
const wrapper = shallow(<ZendeskFab />);
expect(wrapper.snapshot).toMatchSnapshot();
});
});

View File

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

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

View File

@@ -1,10 +1,8 @@
@import "@openedx/paragon/scss/core/core";
.course-card {
.card {
.pgn__card-wrapper-image-cap.vertical {
display: flex;
min-height: $card-image-vertical-max-height;
min-height: var(--pgn-size-card-image-vertical-max-height);
}
.pgn__card-image-cap {
border-bottom-left-radius: 0 !important;
@@ -53,11 +51,11 @@
> .alert {
border-radius: 0;
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 {
border-bottom-left-radius: $alert-border-radius;
border-bottom-right-radius: $alert-border-radius;
border-bottom-left-radius: var(--pgn-size-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,6 +1,33 @@
import { useWindowSize, breakpoints } from '@openedx/paragon';
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', () => {
it('returns true only when it is between medium and small', () => {
// make sure all three breakpoints gap is large enough for test

View File

@@ -1,25 +1,30 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import ActionButton from '.';
import useIsCollapsed from './hooks';
jest.mock('./hooks', () => jest.fn());
jest.unmock('@openedx/paragon');
describe('ActionButton', () => {
const props = {
arbitary: 'props',
className: 'custom-class',
children: 'Test',
};
describe('snapshot', () => {
test('is collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<ActionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is not collapsed', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<ActionButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
});
it('is collapsed', async () => {
useIsCollapsed.mockReturnValue(true);
render(<ActionButton {...props} />);
const button = screen.getByRole('button', { name: 'Test' });
expect(button).toHaveClass('btn-sm', 'custom-class');
});
it('is not collapsed', () => {
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,5 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import useActionDisabledState from '../hooks';
@@ -18,68 +19,70 @@ jest.mock('hooks', () => ({
useTrackCourseEvent: jest.fn(),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
let wrapper;
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const homeUrl = 'home-url';
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
reduxHooks.useTrackCourseEvent.mockImplementation(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
);
const props = {
cardId: 'cardId',
};
const renderComponent = () => render(<IntlProvider locale="en"><BeginCourseButton {...props} /></IntlProvider>);
describe('BeginCourseButton', () => {
const props = {
cardId: 'cardId',
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
describe('initiliaze hooks', () => {
it('initializes course run data with cardId', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
renderComponent();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
});
it('loads exec education path param', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
renderComponent();
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
});
it('loads disabled states for begin action from action hooks', () => {
wrapper = shallow(<BeginCourseButton {...props} />);
renderComponent();
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
});
});
describe('snapshot', () => {
describe('behavior', () => {
describe('disabled', () => {
beforeEach(() => {
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be disabled', () => {
expect(wrapper.instance.props.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', () => {
beforeEach(() => {
wrapper = shallow(<BeginCourseButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be enabled', () => {
expect(wrapper.instance.props.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', () => {
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
it('should track enter course clicked event on click, with exec ed param', async () => {
renderComponent();
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Begin Course' });
user.click(button);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked,
props.cardId,
homeUrl + execEdPath(props.cardId),
));
);
});
});
});

View File

@@ -1,4 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import track from 'tracking';
@@ -19,65 +21,63 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const resumeUrl = 'resume-url';
reduxHooks.useCardCourseRunData.mockReturnValue({ resumeUrl });
const execEdPath = (cardId) => `exec-ed-tracking-path=${cardId}`;
reduxHooks.useCardExecEdTrackingParam.mockImplementation(execEdPath);
reduxHooks.useTrackCourseEvent.mockImplementation(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
(eventName, cardId, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
);
let wrapper;
describe('ResumeButton', () => {
const props = {
cardId: 'cardId',
};
describe('behavior', () => {
describe('initialize hooks', () => {
beforeEach(() => render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>));
it('initializes course run data with cardId', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
});
it('loads exec education path param', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
});
it('loads disabled states for resume action from action hooks', () => {
wrapper = shallow(<ResumeButton {...props} />);
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
});
});
describe('snapshot', () => {
describe('behavior', () => {
describe('disabled', () => {
beforeEach(() => {
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be disabled', () => {
expect(wrapper.instance.props.disabled).toEqual(true);
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', () => {
beforeEach(() => {
wrapper = shallow(<ResumeButton {...props} />);
});
test('snapshot', () => {
expect(wrapper.snapshot).toMatchSnapshot();
});
it('should be enabled', () => {
expect(wrapper.instance.props.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', () => {
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
it('should track enter course clicked event on click, with exec ed param', async () => {
render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'Resume' });
user.click(button);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked,
props.cardId,
resumeUrl + execEdPath(props.cardId),
));
);
});
});
});

View File

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

View File

@@ -1,45 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Locked } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import useActionDisabledState from '../hooks';
import ActionButton from './ActionButton';
import messages from './messages';
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,49 +0,0 @@
import { shallow } from '@edx/react-unit-test-utils';
import track from 'tracking';
import { reduxHooks } from 'hooks';
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.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(false);
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
track.course.upgradeClicked,
props.cardId,
upgradeUrl,
));
});
test('cannot upgrade', () => {
useActionDisabledState.mockReturnValueOnce({ disableUpgradeCourse: true });
const wrapper = shallow(<UpgradeButton {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
});
});
});

View File

@@ -1,4 +1,6 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import track from 'tracking';
import { reduxHooks } from 'hooks';
@@ -14,32 +16,40 @@ jest.mock('tracking', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
useTrackCourseEvent: jest.fn(
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
),
useTrackCourseEvent: jest.fn(),
},
}));
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
jest.mock('./ActionButton', () => 'ActionButton');
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
const defaultProps = { cardId: 'cardId' };
const homeUrl = 'homeUrl';
describe('ViewCourseButton', () => {
test('learner can view course', () => {
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.onClick).toEqual(reduxHooks.useTrackCourseEvent(
it('learner can view course', async () => {
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'View Course' });
expect(button).toBeInTheDocument();
expect(button).not.toHaveClass('disabled');
expect(button).not.toHaveAttribute('aria-disabled', 'true');
});
it('calls trackCourseEvent on click', async () => {
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: 'View Course' });
await user.click(button);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.enterCourseClicked,
defaultProps.cardId,
homeUrl,
));
expect(wrapper.instance.props.disabled).toEqual(false);
);
});
test('learner cannot view course', () => {
it('learner cannot view course', () => {
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: true });
const wrapper = shallow(<ViewCourseButton {...defaultProps} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.props.disabled).toEqual(true);
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
const button = screen.getByRole('button', { name: 'View Course' });
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={
{
"trackCourseEvent": {
"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={
{
"trackCourseEvent": {
"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={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
},
}
}
>
Resume
</ActionButton>
`;
exports[`ResumeButton snapshot enabled snapshot 1`] = `
<ActionButton
as="a"
disabled={false}
href="#"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.enterCourseClicked],
"upgradeUrl": "resume-urlexec-ed-tracking-path=cardId",
},
}
}
>
Resume
</ActionButton>
`;

View File

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

View File

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

View File

@@ -1,11 +1,6 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
upgrade: {
id: 'learner-dash.courseCard.actions.upgrade',
description: 'Course card upgrade button text',
defaultMessage: 'Upgrade',
},
beginCourse: {
id: 'learner-dash.courseCard.actions.beginCourse',
description: 'Course card begin-course button text',

View File

@@ -1,8 +1,8 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import CertificateBanner from './CertificateBanner';
import messages from './messages';
jest.mock('hooks', () => ({
utilHooks: {
@@ -17,28 +17,33 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('components/Banner', () => 'Banner');
jest.unmock('@openedx/paragon');
jest.unmock('@openedx/paragon/icons');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('react');
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', () => {
const props = { cardId: 'cardId' };
reduxHooks.useCardCourseRunData.mockReturnValue({
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 = ({
certificate = {},
enrollment = {},
@@ -51,177 +56,192 @@ describe('CertificateBanner', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings });
return shallow(<CertificateBanner {...props} />);
return render(<IntlProvider locale="en"><CertificateBanner {...props} /></IntlProvider>);
};
/** TODO: Update tests to validate snapshots **/
describe('snapshot', () => {
test('is restricted', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted with support email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail: 'suport@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted with billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with support email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is restricted and verified with support and billing email', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: true },
certificate: { isDownloadable: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and is downloadable', () => {
const wrapper = createWrapper({
grade: { isPassing: false },
certificate: { isDownloadable: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and audit', () => {
const wrapper = createWrapper({
enrollment: {
isAudit: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and has finished', () => {
const wrapper = createWrapper({
courseRun: { isArchived: true },
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('not passing and not audit and not finished', () => {
const wrapper = createWrapper({});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and is earned but unavailable', () => {
const wrapper = createWrapper({
grade: {
isPassing: true,
},
certificate: {
isEarnedButUnavailable: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
test('is passing and not downloadable render empty', () => {
const wrapper = createWrapper({
grade: {
isPassing: true,
},
});
expect(wrapper.snapshot).toMatchSnapshot();
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
it('is restricted', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
const bannerMessage = wrapper.instance.findByType('format-message-function').map(el => el.props.message.defaultMessage).join('\n');
expect(bannerMessage).toEqual(messages.certRestricted.defaultMessage);
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
it('is restricted', () => {
createWrapper({
certificate: {
isRestricted: true,
},
});
it('is restricted and verified', () => {
const wrapper = createWrapper({
certificate: {
isRestricted: true,
},
enrollment: {
isVerified: true,
},
platformSettings: {
supportEmail: 'suport@email',
billingEmail: 'billing@email',
},
});
const bannerMessage = wrapper.instance.findByType('format-message-function').map(el => el.props.message.defaultMessage).join('\n');
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('please let us know.'));
expect(msg).toBeInTheDocument();
expect(msg).not.toContain(supportEmail);
});
it('is restricted with support email', () => {
createWrapper({
certificate: {
isRestricted: true,
},
platformSettings: {
supportEmail,
},
});
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: {
isEarnedButUnavailable: true,
},
});
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();
});
});

View File

@@ -12,7 +12,6 @@ export const CourseBanner = ({ cardId }) => {
const {
isVerified,
isAuditAccessExpired,
canUpgrade,
coursewareAccess = {},
} = reduxHooks.useCardEnrollmentData(cardId);
const courseRun = reduxHooks.useCardCourseRunData(cardId);
@@ -26,13 +25,7 @@ export const CourseBanner = ({ cardId }) => {
return (
<>
{isAuditAccessExpired
&& (canUpgrade ? (
<Banner>
{formatMessage(messages.auditAccessExpired)}
{' '}
{formatMessage(messages.upgradeToAccess)}
</Banner>
) : (
&& (
<Banner>
{formatMessage(messages.auditAccessExpired)}
{' '}
@@ -40,17 +33,7 @@ export const CourseBanner = ({ cardId }) => {
{formatMessage(messages.findAnotherCourse)}
</Hyperlink>
</Banner>
))}
{courseRun.isActive && !canUpgrade && (
<Banner>
{formatMessage(messages.upgradeDeadlinePassed)}
{' '}
<Hyperlink isInline destination={courseRun.marketingUrl || ''}>
{formatMessage(messages.exploreCourseDetails)}
</Hyperlink>
</Banner>
)}
)}
{(!isStaff && isTooEarly && courseRun.startDate) && (
<Banner>
@@ -59,6 +42,7 @@ export const CourseBanner = ({ cardId }) => {
})}
</Banner>
)}
{(!isStaff && hasUnmetPrerequisites) && (
<Banner>{formatMessage(messages.prerequisitesNotMet)}</Banner>
)}

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Hyperlink } from '@openedx/paragon';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils';
@@ -8,7 +7,6 @@ import { CourseBanner } from './CourseBanner';
import messages from './messages';
jest.mock('components/Banner', () => 'Banner');
jest.mock('hooks', () => ({
utilHooks: {
useFormatDate: () => date => date,
@@ -19,13 +17,14 @@ jest.mock('hooks', () => ({
},
}));
const cardId = 'my-test-course-number';
jest.unmock('@openedx/paragon');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('react');
let el;
const cardId = 'test-card-id';
const enrollmentData = {
isVerified: false,
canUpgrade: false,
isAuditAccessExpired: false,
coursewareAccess: {
hasUnmetPrerequisites: false,
@@ -39,7 +38,7 @@ const courseRunData = {
marketingUrl: 'marketing-url',
};
const render = (overrides = {}) => {
const renderCourseBanner = (overrides = {}) => {
const {
courseRun = {},
enrollment = {},
@@ -52,114 +51,58 @@ const render = (overrides = {}) => {
...enrollmentData,
...enrollment,
});
el = shallow(<CourseBanner cardId={cardId} />);
return render(<IntlProvider locale="en"><CourseBanner cardId={cardId} /></IntlProvider>);
};
describe('CourseBanner', () => {
test('initializes data with course number from enrollment, course and course run data', () => {
render();
it('initializes data with course number from enrollment, course and course run data', () => {
renderCourseBanner();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
});
test('no display if learner is verified', () => {
render({ enrollment: { isVerified: true } });
expect(el.isEmptyRender()).toEqual(true);
it('no display if learner is verified', () => {
renderCourseBanner({ enrollment: { isVerified: true } });
expect(screen.queryByRole('alert')).toBeNull();
});
describe('audit access expired, can upgrade', () => {
beforeEach(() => {
render({ enrollment: { isAuditAccessExpired: true, canUpgrade: true } });
describe('audit access expired', () => {
it('should display correct message and link', () => {
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, upgradeToAccess)', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.instance.children[0].children[2].el).toContain(messages.upgradeToAccess.defaultMessage);
});
});
describe('audit access expired, cannot upgrade', () => {
beforeEach(() => {
render({ enrollment: { isAuditAccessExpired: true } });
});
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.auditAccessExpired.defaultMessage);
expect(el.instance.findByType(Hyperlink)[0].children[0].el).toEqual(messages.findAnotherCourse.defaultMessage);
});
});
describe('course run active and cannot upgrade', () => {
beforeEach(() => {
render({ courseRun: { isActive: true } });
});
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('messages: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.upgradeDeadlinePassed.defaultMessage);
const link = el.instance.findByType(Hyperlink);
expect(link[0].children[0].el).toEqual(messages.exploreCourseDetails.defaultMessage);
expect(link[0].props.destination).toEqual(courseRunData.marketingUrl);
});
});
test('no display if audit access not expired and (course is not active or can upgrade)', () => {
render();
// isEmptyRender() isn't true because the minimal is <Fragment />
expect(el.instance.children).toEqual([]);
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
expect(el.instance.children).toEqual([]);
});
describe('unmet prerequisites', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
});
test('snapshot: unmetPrerequisites', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('messages: prerequisitesNotMet', () => {
expect(el.instance.children[0].children[0].el).toContain(messages.prerequisitesNotMet.defaultMessage);
it('should display correct message', () => {
renderCourseBanner({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
const preReqText = screen.getByText(formatMessage(messages.prerequisitesNotMet));
expect(preReqText).toBeInTheDocument();
});
});
describe('too early', () => {
describe('no start date', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } }, courseRun: { startDate: null } });
it('should not display banner', () => {
renderCourseBanner({ enrollment: { coursewareAccess: { isTooEarly: true } }, courseRun: { startDate: null } });
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
test('snapshot', () => expect(el.snapshot).toMatchSnapshot());
test('messages', () => expect(el.instance.children).toEqual([]));
});
describe('has start date', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { isTooEarly: true } } });
});
test('snapshot', () => expect(el.snapshot).toMatchSnapshot());
test('messages: courseHasNotStarted', () => {
expect(el.instance.children[0].children[0].el).toContain(
it('should display messages courseHasNotStarted', () => {
renderCourseBanner({ enrollment: { coursewareAccess: { isTooEarly: true } } });
const earlyMsg = screen.getByText(
formatMessage(messages.courseHasNotStarted, { startDate: courseRunData.startDate }),
);
expect(earlyMsg).toBeInTheDocument();
});
});
});
describe('staff', () => {
beforeEach(() => {
render({ enrollment: { coursewareAccess: { isStaff: true } } });
it('should not display banner', () => {
renderCourseBanner({ enrollment: { coursewareAccess: { isStaff: true } } });
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
test('snapshot: isStaff', () => {
expect(el.snapshot).toMatchSnapshot();
});
});
test('snapshot: stacking banners', () => {
render({
enrollment: {
coursewareAccess: {
isStaff: true,
isTooEarly: true,
hasUnmetPrerequisites: true,
},
},
});
expect(el.snapshot).toMatchSnapshot();
});
});

View File

@@ -1,58 +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"
data-testid="credit-error-msg"
>
<format-message-function
message={
{
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
"description": "",
"id": "learner-dash.courseCard.banners.credit.error",
}
}
values={
{
"supportEmailLink": <MailtoLink
to="test-support-email"
>
test-support-email
</MailtoLink>,
}
}
/>
</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"
data-testid="credit-error-msg"
>
An error occurred with this transaction.
</p>
<ContentComponent
cardId="test-card-id"
/>
</Banner>
`;
exports[`CreditBanner component render with no error state snapshot 1`] = `
<Banner>
<ContentComponent
cardId="test-card-id"
/>
</Banner>
`;

View File

@@ -1,95 +1,69 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from 'testUtils';
import { MailtoLink } from '@openedx/paragon';
import { screen, render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import hooks from './hooks';
import messages from './messages';
import CreditBanner from '.';
jest.mock('components/Banner', () => 'Banner');
import { CreditBanner } from '.';
jest.mock('./hooks', () => ({
useCreditBannerData: jest.fn(),
}));
let el;
const cardId = 'test-card-id';
jest.unmock('@openedx/paragon');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('react');
const ContentComponent = () => 'ContentComponent';
const supportEmail = 'test-support-email';
describe('CreditBanner', () => {
const mockCardId = 'test-card-id';
const mockContentComponent = () => <div data-testid="mock-content">Test Content</div>;
const mockSupportEmail = 'support@test.com';
describe('CreditBanner component', () => {
describe('behavior', () => {
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);
});
const renderCreditBanner = () => render(
<IntlProvider locale="en">
<CreditBanner cardId={mockCardId} />
</IntlProvider>,
);
beforeEach(() => {
jest.clearAllMocks();
});
describe('render', () => {
describe('with error state', () => {
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue({
error: true,
ContentComponent,
supportEmail,
});
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('passes danger variant to Banner parent', () => {
expect(el.instance.findByType('Banner')[0].props.variant).toEqual('danger');
});
it('includes credit-error-msg with support email link', () => {
expect(el.instance.findByTestId('credit-error-msg')[0].children[0].el).toEqual(shallow(formatMessage(messages.error, {
supportEmailLink: (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>),
})));
});
it('loads ContentComponent with cardId', () => {
expect(el.instance.findByType('ContentComponent')[0].props.cardId).toEqual(cardId);
});
});
describe('with error state with no email', () => {
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue({
error: true,
ContentComponent,
});
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('includes credit-error-msg without support email link', () => {
expect(el.instance.findByTestId('credit-error-msg')[0].children[0].el).toEqual(formatMessage(messages.errorNoEmail));
});
});
it('should return null if hook returns null', () => {
hooks.useCreditBannerData.mockReturnValue(null);
renderCreditBanner();
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
describe('with no error state', () => {
beforeEach(() => {
hooks.useCreditBannerData.mockReturnValue({
error: false,
ContentComponent,
supportEmail,
});
el = shallow(<CreditBanner cardId={cardId} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads ContentComponent with cardId', () => {
expect(el.instance.findByType('ContentComponent')[0].props.cardId).toEqual(cardId);
});
it('should render content component without error', () => {
hooks.useCreditBannerData.mockReturnValue({
ContentComponent: mockContentComponent,
error: false,
});
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,10 +1,8 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import messages from './messages';
import ProviderLink from './components/ProviderLink';
import ApprovedContent from './ApprovedContent';
jest.mock('hooks', () => ({
@@ -13,10 +11,11 @@ jest.mock('hooks', () => ({
useMasqueradeData: jest.fn(),
},
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
let el;
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.unmock('@edx/frontend-platform/i18n');
const cardId = 'test-card-id';
const credit = {
providerStatusUrl: 'test-credit-provider-status-url',
@@ -26,38 +25,54 @@ reduxHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
describe('ApprovedContent component', () => {
beforeEach(() => {
el = shallow(<ApprovedContent cardId={cardId} />);
});
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit data with cardId', () => {
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('rendered CreditContent component', () => {
let component;
beforeAll(() => {
component = el.instance.findByType('CreditContent');
beforeEach(() => {
jest.clearAllMocks();
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
});
test('action.href from credit.providerStatusUrl', () => {
expect(component[0].props.action.href).toEqual(credit.providerStatusUrl);
it('action.message is formatted viewCredit message', () => {
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', () => {
expect(component[0].props.action.message).toEqual(formatMessage(messages.viewCredit));
it('action.href from credit.providerStatusUrl', () => {
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
expect(actionButton).toHaveAttribute('href', credit.providerStatusUrl);
});
test('action.disabled is false', () => {
expect(component[0].props.action.disabled).toEqual(false);
it('action.disabled is 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', () => {
expect(component[0].props.message).toEqual(formatMessage(
messages.approved,
{
congratulations: (<b>{formatMessage(messages.congratulations)}</b>),
linkToProviderSite: <ProviderLink cardId={cardId} />,
providerName: credit.providerName,
},
));
it('message is formatted approved message', () => {
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg).toBeInTheDocument();
expect(creditMsg.textContent).toContain(`${credit.providerName} has approved your request for course credit`);
});
});
describe('when masquerading', () => {
beforeEach(() => {
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: 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

@@ -1,8 +1,8 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import { formatMessage } from 'testUtils';
import track from 'tracking';
import messages from './messages';
@@ -14,15 +14,16 @@ jest.mock('hooks', () => ({
useCardCourseRunData: jest.fn(),
},
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('tracking', () => ({
credit: {
purchase: (...args) => ({ trackCredit: args }),
purchase: jest.fn(),
},
}));
let el;
let component;
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@openedx/paragon');
jest.unmock('react');
const cardId = 'test-card-id';
const courseId = 'test-course-id';
@@ -32,50 +33,45 @@ const credit = {
reduxHooks.useCardCreditData.mockReturnValue(credit);
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
const render = () => {
el = shallow(<EligibleContent cardId={cardId} />);
};
const loadComponent = () => {
component = el.instance.findByType('CreditContent');
};
const renderEligibleContent = () => render(<IntlProvider locale="en" messages={{}}><EligibleContent cardId={cardId} /></IntlProvider>);
describe('EligibleContent component', () => {
beforeEach(() => {
render();
});
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit data with cardId', () => {
renderEligibleContent();
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
it('initializes course run data with cardId', () => {
renderEligibleContent();
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('behavior', () => {
describe('rendered CreditContent component', () => {
beforeEach(() => {
loadComponent();
it('action message is formatted getCredit message', () => {
renderEligibleContent();
const button = screen.getByRole('button', { name: messages.getCredit.defaultMessage });
expect(button).toBeInTheDocument();
});
test('action.onClick sends credit purchase track event', () => {
expect(component[0].props.action.onClick).toEqual(
track.credit.purchase(courseId),
);
it('onClick sends credit purchase track event', async () => {
const user = userEvent.setup();
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', () => {
expect(component[0].props.action.message).toEqual(formatMessage(messages.getCredit));
it('message is formatted eligible message if provider', () => {
renderEligibleContent();
const eligibleMessage = screen.getByTestId('credit-msg');
expect(eligibleMessage).toBeInTheDocument();
expect(eligibleMessage).toHaveTextContent(credit.providerName);
});
test('message is formatted eligible message if no provider', () => {
reduxHooks.useCardCreditData.mockReturnValueOnce({});
render();
loadComponent();
expect(component[0].props.message).toEqual(formatMessage(
messages.eligible,
{ getCredit: (<b>{formatMessage(messages.getCredit)}</b>) },
));
});
test('message is formatted eligible message if provider', () => {
expect(component[0].props.message).toEqual(
formatMessage(messages.eligibleFromProvider, { providerName: credit.providerName }),
);
it('message is formatted eligible message if no provider', () => {
reduxHooks.useCardCreditData.mockReturnValue({});
renderEligibleContent();
const eligibleMessage = screen.getByTestId('credit-msg');
expect(eligibleMessage).toBeInTheDocument();
expect(eligibleMessage).toHaveTextContent(messages.getCredit.defaultMessage);
});
});
});

View File

@@ -1,73 +1,107 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { formatMessage } from 'testUtils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import userEvent from '@testing-library/user-event';
import { reduxHooks } from 'hooks';
import messages from './messages';
import hooks from './hooks';
import ProviderLink from './components/ProviderLink';
import MustRequestContent from './MustRequestContent';
jest.mock('./hooks', () => ({
useCreditRequestData: jest.fn(),
}));
jest.mock('hooks', () => ({
reduxHooks: { useMasqueradeData: jest.fn() },
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
let el;
let component;
jest.mock('hooks', () => ({
reduxHooks: {
useMasqueradeData: jest.fn(),
useCardCreditData: jest.fn(),
},
}));
jest.unmock('@openedx/paragon');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('react');
const cardId = 'test-card-id';
const requestData = { test: 'requestData' };
const createCreditRequest = jest.fn().mockName('createCreditRequest');
hooks.useCreditRequestData.mockReturnValue({
requestData,
createCreditRequest,
});
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
const render = () => {
el = shallow(<MustRequestContent cardId={cardId} />);
const requestData = {
url: 'test-request-data-url',
parameters: {
key1: 'val1',
key2: 'val2',
key3: 'val3',
},
};
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', () => {
beforeEach(() => {
render();
jest.clearAllMocks();
hooks.useCreditRequestData.mockReturnValue({
requestData,
createCreditRequest,
});
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
reduxHooks.useCardCreditData.mockReturnValue({
providerName,
providerStatusUrl,
});
});
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit request data with cardId', () => {
renderMustRequestContent();
expect(hooks.useCreditRequestData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('rendered CreditContent component', () => {
describe('behavior', () => {
describe('rendered content', () => {
beforeEach(() => {
component = el.instance.findByType('CreditContent');
renderMustRequestContent();
});
test('action.onClick calls createCreditRequest from useCreditRequestData hook', () => {
expect(component[0].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[0].props.action.message).toEqual(
formatMessage(messages.requestCredit),
);
it('shows request credit button that is enabled', () => {
const button = screen.getByRole('button', { name: /request credit/i });
expect(button).toBeEnabled();
});
test('action.disabled is false', () => {
expect(component[0].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[0].props.message).toEqual(
formatMessage(messages.mustRequest, {
linkToProviderSite: <ProviderLink cardId={cardId} />,
requestCredit: <b>{formatMessage(messages.requestCredit)}</b>,
}),
);
it('renders credit request form with correct data', () => {
const { container } = renderMustRequestContent();
const form = container.querySelector('form');
expect(form).toBeInTheDocument();
expect(form).toHaveAttribute('action', requestData.url);
});
test('requestData drawn from useCreditRequestData hook', () => {
expect(component[0].props.requestData).toEqual(requestData);
});
describe('when masquerading', () => {
beforeEach(() => {
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: 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

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

View File

@@ -1,10 +1,7 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import messages from './messages';
import ProviderLink from './components/ProviderLink';
import RejectedContent from './RejectedContent';
jest.mock('hooks', () => ({
@@ -12,8 +9,10 @@ jest.mock('hooks', () => ({
useCardCreditData: jest.fn(),
},
}));
jest.mock('./components/CreditContent', () => 'CreditContent');
jest.mock('./components/ProviderLink', () => 'ProviderLink');
jest.unmock('@openedx/paragon');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('react');
const cardId = 'test-card-id';
const credit = {
@@ -22,32 +21,27 @@ const credit = {
};
reduxHooks.useCardCreditData.mockReturnValue(credit);
let el;
let component;
const render = () => { el = shallow(<RejectedContent cardId={cardId} />); };
const loadComponent = () => { component = el.instance.findByType('CreditContent'); };
const renderRejectedContent = () => render(<IntlProvider><RejectedContent cardId={cardId} /></IntlProvider>);
describe('RejectedContent component', () => {
beforeEach(render);
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit data with cardId', () => {
renderRejectedContent();
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
describe('rendered CreditContent component', () => {
beforeAll(loadComponent);
test('no action is passed', () => {
expect(component[0].props.action).toEqual(undefined);
it('no action is passed', () => {
renderRejectedContent();
const action = screen.queryByTestId('action-row-btn');
expect(action).not.toBeInTheDocument();
});
test('message is formatted rejected message', () => {
expect(component[0].props.message).toEqual(formatMessage(
messages.rejected,
{
linkToProviderSite: <ProviderLink cardId={cardId} />,
providerName: credit.providerName,
},
));
it('message is formatted rejected message', () => {
renderRejectedContent();
const message = screen.getByTestId('credit-msg');
expect(message).toBeInTheDocument();
expect(message).toHaveTextContent(`${credit.providerName} did not approve your request for course credit.`);
});
});
});

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import CreditContent from './CreditContent';
let el;
jest.unmock('@openedx/paragon');
jest.unmock('react');
const action = {
href: 'test-action-href',
onClick: jest.fn().mockName('test-action-onClick'),
@@ -15,45 +16,57 @@ const message = 'test-message';
const requestData = { url: 'test-request-data-url', parameters: { key1: 'val1' } };
const props = { action, message, requestData };
const renderCreditContent = (data) => render(
<CreditContent {...data} />,
);
describe('CreditContent component', () => {
describe('render', () => {
describe('with action', () => {
beforeEach(() => {
el = shallow(<CreditContent {...props} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads href, onClick, and message into action row button', () => {
const buttonEl = el.instance.findByTestId('action-row-btn')[0];
expect(buttonEl.props.href).toEqual(action.href);
expect(buttonEl.props.onClick).toEqual(action.onClick);
expect(buttonEl.props.disabled).toEqual(action.disabled);
expect(buttonEl.children[0].el).toEqual(action.message);
it('loads href and message into action row button', () => {
renderCreditContent(props);
const button = screen.getByRole('link', { name: action.message });
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('href', action.href);
expect(button).not.toHaveAttribute('disabled');
});
it('loads message into credit-msg div', () => {
expect(el.instance.findByTestId('credit-msg')[0].children[0].el).toEqual(message);
renderCreditContent(props);
const creditMsg = screen.getByTestId('credit-msg');
expect(creditMsg).toBeInTheDocument();
expect(creditMsg.innerHTML).toEqual(message);
});
it('loads CreditRequestForm with passed requestData', () => {
expect(el.instance.findByType('CreditRequestForm')[0].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', () => {
el = shallow(<CreditContent {...props} action={{ ...action, disabled: true }} />);
expect(el.instance.findByTestId('action-row-btn')[0].props.disabled).toEqual(true);
it('disables action button when action.disabled is true', () => {
renderCreditContent({ ...props, action: { ...action, disabled: 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', () => {
test('snapshot', () => {
el = shallow(<CreditContent {...{ message, requestData }} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads message into credit-msg div', () => {
expect(el.instance.findByTestId('credit-msg')[0].children[0].el).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', () => {
expect(el.instance.findByType('CreditRequestForm')[0].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';
// jest.mock('react', () => ({
// ...jest.requireActual('react'),
// useRef: jest.fn((val) => ({ current: val, useRef: true })),
// useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
// }));
let out;
const ref = {
current: { click: jest.fn() },

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render } from '@testing-library/react';
import { keyStore } from 'utils';
@@ -11,7 +10,11 @@ jest.mock('./hooks', () => ({
default: jest.fn(),
}));
const ref = 'test-ref';
jest.unmock('@openedx/paragon');
jest.unmock('react');
const ref = { current: { click: jest.fn() }, useRef: jest.fn() };
const requestData = {
url: 'test-request-data-url',
parameters: {
@@ -25,40 +28,41 @@ const paramKeys = keyStore(requestData.parameters);
useCreditRequestFormData.mockReturnValue({ ref });
let el;
const shallowRender = (data) => { el = shallow(<CreditRequestForm requestData={data} />); };
const renderForm = (data) => render(<CreditRequestForm requestData={data} />);
describe('CreditRequestForm component', () => {
describe('behavior', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('hooks', () => {
it('initializes ref from hook with requestData', () => {
shallowRender(requestData);
renderForm(requestData);
expect(useCreditRequestFormData).toHaveBeenCalledWith(requestData);
});
});
describe('render output', () => {
describe('null requestData', () => {
it('returns null', () => {
shallowRender(null);
expect(el.isEmptyRender()).toEqual(true);
const { container } = renderForm(null);
expect(container.firstChild).toBeNull();
});
});
describe('valid requestData', () => {
beforeEach(() => {
shallowRender(requestData);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('loads Form with requestData url', () => {
expect(el.instance.findByType('Form')[0].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', () => {
const controls = el.instance.findByType('FormControl');
expect(controls[0].props.name).toEqual(paramKeys.key1);
expect(controls[0].props.value).toEqual(requestData.parameters.key1);
expect(controls[1].props.name).toEqual(paramKeys.key2);
expect(controls[1].props.value).toEqual(requestData.parameters.key2);
expect(controls[2].props.name).toEqual(paramKeys.key3);
expect(controls[2].props.value).toEqual(requestData.parameters.key3);
const { container } = renderForm(requestData);
const controls = container.querySelectorAll('textarea');
expect(controls.length).toEqual(Object.keys(requestData.parameters).length);
expect(controls[0]).toHaveAttribute('name', paramKeys.key1);
expect(controls[0]).toHaveValue(requestData.parameters.key1);
expect(controls[1]).toHaveAttribute('name', paramKeys.key2);
expect(controls[1]).toHaveValue(requestData.parameters.key2);
expect(controls[2]).toHaveAttribute('name', paramKeys.key3);
expect(controls[2]).toHaveValue(requestData.parameters.key3);
});
});
});

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { reduxHooks } from 'hooks';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import ProviderLink from './ProviderLink';
@@ -11,32 +10,39 @@ jest.mock('hooks', () => ({
},
}));
jest.unmock('@openedx/paragon');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('react');
const cardId = 'test-card-id';
const credit = {
providerStatusUrl: 'test-credit-provider-status-url',
providerName: 'test-credit-provider-name',
};
let el;
const renderProviderLink = () => render(
<IntlProvider locale="en"><ProviderLink cardId={cardId} /></IntlProvider>,
);
describe('ProviderLink component', () => {
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useCardCreditData.mockReturnValue(credit);
el = shallow(<ProviderLink cardId={cardId} />);
renderProviderLink();
});
describe('behavior', () => {
describe('hooks', () => {
it('initializes credit hook with cardId', () => {
expect(reduxHooks.useCardCreditData).toHaveBeenCalledWith(cardId);
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('passes credit.providerStatusUrl to the hyperlink href', () => {
expect(el.instance.findByType('Hyperlink')[0].props.href).toEqual(credit.providerStatusUrl);
const providerLink = screen.getByRole('link', { href: credit.providerStatusUrl });
expect(providerLink).toBeInTheDocument();
});
it('passes providerName for the link message', () => {
expect(el.instance.findByType('Hyperlink')[0].children[0].el).toEqual(credit.providerName);
const providerLink = screen.getByRole('link', { href: credit.providerStatusUrl });
expect(providerLink).toHaveTextContent(credit.providerName);
});
});
});

View File

@@ -1,60 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreditContent component render with action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>
<ActionRow
className="mt-4"
>
<Button
as="a"
className="border-gray-400"
data-testid="action-row-btn"
disabled={false}
href="test-action-href"
onClick={[MockFunction test-action-onClick]}
rel="noopener"
target="_blank"
variant="outline-primary"
>
test-action-message
</Button>
</ActionRow>
<CreditRequestForm
requestData={
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",
}
}
/>
</Fragment>
`;
exports[`CreditContent component render without action snapshot 1`] = `
<Fragment>
<div
className="message-copy credit-msg"
data-testid="credit-msg"
>
test-message
</div>
<CreditRequestForm
requestData={
{
"parameters": {
"key1": "val1",
},
"url": "test-request-data-url",
}
}
/>
</Fragment>
`;

View File

@@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProviderLink component render snapshot 1`] = `
<Hyperlink
href="test-credit-provider-status-url"
rel="noopener"
target="_blank"
>
test-credit-provider-name
</Hyperlink>
`;

View File

@@ -1,10 +1,11 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import EntitlementBanner from './EntitlementBanner';
import messages from './messages';
jest.mock('components/Banner', () => 'Banner');
jest.mock('hooks', () => ({
utilHooks: {
useFormatDate: () => date => date,
@@ -18,9 +19,11 @@ jest.mock('hooks', () => ({
},
}));
const cardId = 'my-test-course-number';
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@openedx/paragon');
jest.unmock('react');
let el;
const cardId = 'test-card-id';
const entitlementData = {
isEntitlement: true,
@@ -31,33 +34,43 @@ const entitlementData = {
};
const platformData = { supportEmail: 'test-support-email' };
const render = (overrides = {}) => {
const renderComponent = (overrides = {}) => {
const { entitlement = {} } = overrides;
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
reduxHooks.usePlatformSettingsData.mockReturnValueOnce(platformData);
el = shallow(<EntitlementBanner cardId={cardId} />);
return render(<IntlProvider locale="en"><EntitlementBanner cardId={cardId} /></IntlProvider>);
};
describe('EntitlementBanner', () => {
test('initializes data with course number from entitlement', () => {
render();
it('initializes data with course number from entitlement', () => {
renderComponent();
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
expect(reduxHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(cardId);
});
test('no display if not an entitlement', () => {
render({ entitlement: { isEntitlement: false } });
expect(el.isEmptyRender()).toEqual(true);
it('no display if not an entitlement', () => {
renderComponent({ entitlement: { isEntitlement: false } });
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
test('snapshot: no sessions available', () => {
render({ entitlement: { isFulfilled: false, hasSessions: false } });
expect(el.snapshot).toMatchSnapshot();
it('renders when no sessions available', () => {
renderComponent({ entitlement: { isFulfilled: false, hasSessions: false } });
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-warning');
expect(banner.innerHTML).toContain(platformData.supportEmail);
});
test('snapshot: expiration warning', () => {
render({ entitlement: { showExpirationWarning: true } });
expect(el.snapshot).toMatchSnapshot();
it('renders when expiration warning', () => {
renderComponent({ entitlement: { showExpirationWarning: true } });
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass('alert-info');
const button = screen.getByRole('button', { name: formatMessage(messages.selectSession) });
expect(button).toBeInTheDocument();
});
test('no display if sessions available and not displaying warning', () => {
render();
expect(el.isEmptyRender()).toEqual(true);
it('renders expired banner', () => {
renderComponent({ entitlement: { isExpired: true } });
const banner = screen.getByRole('alert');
expect(banner).toBeInTheDocument();
expect(banner.innerHTML).toContain(formatMessage(messages.entitlementExpired));
});
});

View File

@@ -1,4 +1,4 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { ProgramsList } from './ProgramsList';
@@ -9,15 +9,23 @@ describe('ProgramsList', () => {
title: 'Example Program 1',
},
{
programUrl: 'http://example.com',
programUrl: 'http://example2.com',
title: 'Example Program 2',
},
];
it('renders correctly', () => {
const wrapper = shallow(<ProgramsList programs={programs} />);
expect(wrapper.snapshot).toMatchSnapshot();
render(<ProgramsList programs={programs} />);
const list = screen.getByRole('list');
expect(list).toBeInTheDocument();
expect(list.children.length).toEqual(programs.length);
});
expect(wrapper.instance.findByType('li').length).toEqual(programs.length);
it('add the links correctly', () => {
render(<ProgramsList programs={programs} />);
programs.forEach(program => {
const link = screen.getByRole('link', { name: program.title });
expect(link).toHaveAttribute('href', program.url);
});
});
});

View File

@@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProgramsList renders correctly 1`] = `
<ul
className="related-programs-list-container"
>
<li
className="my-2"
key="http://example.com"
>
<a
href="http://example.com"
>
Example Program 1
</a>
</li>
<li
className="my-2"
key="http://example.com"
>
<a
href="http://example.com"
>
Example Program 2
</a>
</li>
</ul>
`;

View File

@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RelatedProgramsBanner render with programs 1`] = `
<Banner
className="bg-white border-top border-bottom mb-0 related-programs-banner"
icon={[MockFunction icons.Program]}
variant="info"
>
<span
className="font-weight-bolder"
>
Related Programs:
</span>
<ProgramsList
programs={
[
{
"title": "Program 1",
"url": "http://example.com/program1",
},
{
"title": "Program 2",
"url": "http://example.com/program2",
},
]
}
/>
</Banner>
`;

View File

@@ -1,9 +1,13 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import RelatedProgramsBanner from '.';
jest.mock('./ProgramsList', () => 'ProgramsList');
jest.unmock('@openedx/paragon');
jest.unmock('@openedx/paragon/icons');
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('react');
jest.mock('hooks', () => ({
reduxHooks: {
@@ -12,31 +16,39 @@ jest.mock('hooks', () => ({
}));
const cardId = 'test-card-id';
const programData = {
list: [
{
title: 'Program 1',
url: 'http://example.com/program1',
},
{
title: 'Program 2',
url: 'http://example.com/program2',
},
],
length: 2,
};
describe('RelatedProgramsBanner', () => {
test('render empty', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue({
length: 0,
});
const el = shallow(<RelatedProgramsBanner cardId={cardId} />);
expect(el.isEmptyRender()).toEqual(true);
it('render empty', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue({});
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
const banner = screen.queryByRole('alert');
expect(banner).toBeNull();
});
test('render with programs', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue({
list: [
{
title: 'Program 1',
url: 'http://example.com/program1',
},
{
title: 'Program 2',
url: 'http://example.com/program2',
},
],
length: 2,
});
const el = shallow(<RelatedProgramsBanner cardId={cardId} />);
expect(el.snapshot).toMatchSnapshot();
it('render with programs', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue(programData);
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
const list = screen.getByRole('list');
expect(list.childElementCount).toBe(programData.list.length);
});
it('render related programs title', () => {
reduxHooks.useCardRelatedProgramsData.mockReturnValue(programData);
render(<IntlProvider locale="en"><RelatedProgramsBanner cardId={cardId} /></IntlProvider>);
const title = screen.getByText('Related Programs:');
expect(title).toBeInTheDocument();
});
});

View File

@@ -1,205 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CertificateBanner snapshot is passing and is downloadable 1`] = `
<Banner
icon={[MockFunction icons.CheckCircle]}
variant="success"
>
Congratulations. Your certificate is ready.
</Banner>
`;
exports[`CertificateBanner snapshot is passing and is earned but unavailable 1`] = `
<Banner>
Your grade and certificate will be ready after 10/20/3030.
</Banner>
`;
exports[`CertificateBanner snapshot is passing and not downloadable render empty 1`] = `null`;
exports[`CertificateBanner snapshot is restricted 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
If you would like a refund on your Certificate of Achievement, please contact us.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with billing email 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
<format-message-function
message={
{
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
{
"billingEmail": <MailtoLink
to="billing@email"
>
billing@email
</MailtoLink>,
}
}
/>
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with support and billing email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
<format-message-function
message={
{
"defaultMessage": "If you would like a refund on your Certificate of Achievement, please contact our billing address {billingEmail}",
"description": "Message to learners to contact billing for certificate refunds",
"id": "learner-dash.courseCard.banners.certificateRefundContactBilling",
}
}
values={
{
"billingEmail": <MailtoLink
to="billing@email"
>
billing@email
</MailtoLink>,
}
}
/>
</Banner>
`;
exports[`CertificateBanner snapshot is restricted and verified with support email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
If you would like a refund on your Certificate of Achievement, please contact us.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted with billing email 1`] = `
<Banner
variant="danger"
>
Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know.
</Banner>
`;
exports[`CertificateBanner snapshot is restricted with support email 1`] = `
<Banner
variant="danger"
>
<format-message-function
message={
{
"defaultMessage": "Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {supportEmail}.",
"description": "Restricted certificate warning message",
"id": "learner-dash.courseCard.banners.certificateRestricted",
}
}
values={
{
"supportEmail": <MailtoLink
to="suport@email"
>
suport@email
</MailtoLink>,
}
}
/>
</Banner>
`;
exports[`CertificateBanner snapshot not passing and audit 1`] = `
<Banner>
Grade required to pass the course: 0.8%
</Banner>
`;
exports[`CertificateBanner snapshot not passing and has finished 1`] = `
<Banner
variant="warning"
>
You are not eligible for a certificate.
<Hyperlink
destination="progressUrl"
isInline={true}
>
View grades.
</Hyperlink>
</Banner>
`;
exports[`CertificateBanner snapshot not passing and is downloadable 1`] = `
<Banner
icon={[MockFunction icons.CheckCircle]}
variant="success"
>
Congratulations. Your certificate is ready.
</Banner>
`;
exports[`CertificateBanner snapshot not passing and not audit and not finished 1`] = `
<Banner
variant="warning"
>
Grade required for a certificate: 0.8%
</Banner>
`;

View File

@@ -1,63 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseBanner audit access expired, can upgrade snapshot: (auditAccessExpired, upgradeToAccess) 1`] = `
<Fragment>
<Banner>
Your audit access to this course has expired.
Upgrade now to access your course again.
</Banner>
</Fragment>
`;
exports[`CourseBanner audit access expired, cannot upgrade snapshot: (auditAccessExpired, findAnotherCourse hyperlink) 1`] = `
<Fragment>
<Banner>
Your audit access to this course has expired.
<Hyperlink
destination=""
isInline={true}
>
Find another course
</Hyperlink>
</Banner>
</Fragment>
`;
exports[`CourseBanner course run active and cannot upgrade snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink) 1`] = `
<Fragment>
<Banner>
Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.
<Hyperlink
destination="marketing-url"
isInline={true}
>
Explore course details.
</Hyperlink>
</Banner>
</Fragment>
`;
exports[`CourseBanner snapshot: stacking banners 1`] = `<Fragment />`;
exports[`CourseBanner staff snapshot: isStaff 1`] = `<Fragment />`;
exports[`CourseBanner too early has start date snapshot 1`] = `
<Fragment>
<Banner>
You can't access this course just yet because the course hasn't started yet. The course will start on 11/11/3030.
</Banner>
</Fragment>
`;
exports[`CourseBanner too early no start date snapshot 1`] = `<Fragment />`;
exports[`CourseBanner unmet prerequisites snapshot: unmetPrerequisites 1`] = `
<Fragment>
<Banner>
You can't access this course just yet because you have not met the pre-requisites.
</Banner>
</Fragment>
`;

View File

@@ -1,53 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EntitlementBanner snapshot: expiration warning 1`] = `
<Banner>
<format-message-function
message={
{
"defaultMessage": "You must {selectSessionButton} by {changeDeadline} to access the course.",
"description": "Entitlement course message when the entitlement is expiring soon.",
"id": "learner-dash.courseCard.banners.entitlementExpiringSoon",
}
}
values={
{
"changeDeadline": "11/11/2022",
"selectSessionButton": <Button
className="m-0 p-0"
onClick={[MockFunction updateSelectSessionModalCallback(my-test-course-number)]}
size="inline"
variant="link"
>
select a session
</Button>,
}
}
/>
</Banner>
`;
exports[`EntitlementBanner snapshot: no sessions available 1`] = `
<Banner
variant="warning"
>
<format-message-function
message={
{
"defaultMessage": "There are no sessions available at the moment. The course team will create new sessions soon. If no sessions appear, please contact {emailLink} for information.",
"description": "Entitlement course message when no sessions are available",
"id": "learner-dash.courseCard.banners.entitlementUnavailable",
}
}
values={
{
"emailLink": <MailtoLink
to="test-support-email"
>
test-support-email
</MailtoLink>,
}
}
/>
</Banner>
`;

View File

@@ -1,41 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardBanners render with isEnrolled false 1`] = `
<div
className="course-card-banners"
data-testid="CourseCardBanners"
>
<RelatedProgramsBanner
cardId="test-card-id"
/>
<CourseBanner
cardId="test-card-id"
/>
<EntitlementBanner
cardId="test-card-id"
/>
</div>
`;
exports[`CourseCardBanners renders default CourseCardBanners 1`] = `
<div
className="course-card-banners"
data-testid="CourseCardBanners"
>
<RelatedProgramsBanner
cardId="test-card-id"
/>
<CourseBanner
cardId="test-card-id"
/>
<EntitlementBanner
cardId="test-card-id"
/>
<CertificateBanner
cardId="test-card-id"
/>
<CreditBanner
cardId="test-card-id"
/>
</div>
`;

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { reduxHooks } from 'hooks';
import CourseBanner from './CourseBanner';
import CourseBannerSlot from 'plugin-slots/CourseBannerSlot';
import CertificateBanner from './CertificateBanner';
import CreditBanner from './CreditBanner';
import EntitlementBanner from './EntitlementBanner';
@@ -14,7 +14,7 @@ export const CourseCardBanners = ({ cardId }) => {
return (
<div className="course-card-banners" data-testid="CourseCardBanners">
<RelatedProgramsBanner cardId={cardId} />
<CourseBanner cardId={cardId} />
<CourseBannerSlot cardId={cardId} />
<EntitlementBanner cardId={cardId} />
{isEnrolled && <CertificateBanner cardId={cardId} />}
{isEnrolled && <CreditBanner cardId={cardId} />}

View File

@@ -1,14 +1,23 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { reduxHooks } from 'hooks';
import CourseCardBanners from '.';
jest.mock('./CourseBanner', () => 'CourseBanner');
jest.mock('./CertificateBanner', () => 'CertificateBanner');
jest.mock('./CreditBanner', () => 'CreditBanner');
jest.mock('./EntitlementBanner', () => 'EntitlementBanner');
jest.mock('./RelatedProgramsBanner', () => 'RelatedProgramsBanner');
jest.mock('./CourseBanner', () => jest.fn(() => <div>CourseBanner</div>));
jest.mock('./CertificateBanner', () => jest.fn(() => <div>CertificateBanner</div>));
jest.mock('./CreditBanner', () => jest.fn(() => <div>CreditBanner</div>));
jest.mock('./EntitlementBanner', () => jest.fn(() => <div>EntitlementBanner</div>));
jest.mock('./RelatedProgramsBanner', () => jest.fn(() => <div>RelatedProgramsBanner</div>));
const mockedComponents = [
'CourseBanner',
'CertificateBanner',
'CreditBanner',
'EntitlementBanner',
'RelatedProgramsBanner',
];
jest.mock('hooks', () => ({
reduxHooks: {
@@ -20,13 +29,20 @@ describe('CourseCardBanners', () => {
const props = {
cardId: 'test-card-id',
};
test('renders default CourseCardBanners', () => {
const wrapper = shallow(<CourseCardBanners {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
it('renders default CourseCardBanners', () => {
render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
mockedComponents.map((componentName) => {
const mockedComponent = screen.getByText(componentName);
return expect(mockedComponent).toBeInTheDocument();
});
});
test('render with isEnrolled false', () => {
it('render with isEnrolled false', () => {
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
const wrapper = shallow(<CourseCardBanners {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
render(<IntlProvider locale="en"><CourseCardBanners {...props} /></IntlProvider>);
const mockedComponentsIfNotEnrolled = mockedComponents.slice(-2);
mockedComponentsIfNotEnrolled.map((componentName) => {
const mockedComponent = screen.getByText(componentName);
return expect(mockedComponent).toBeInTheDocument();
});
});
});

View File

@@ -6,26 +6,11 @@ const messages = defineMessages({
description: 'Audit access expiration banner message',
defaultMessage: 'Your audit access to this course has expired.',
},
upgradeToAccess: {
id: 'learner-dash.courseCard.banners.upgradeToAccess',
description: 'Upgrade prompt for audit-expired learners that can still upgrade',
defaultMessage: 'Upgrade now to access your course again.',
},
findAnotherCourse: {
id: 'learner-dash.courseCard.banners.findAnotherCourse',
description: 'Action prompt taking learners to course exploration',
defaultMessage: 'Find another course',
},
upgradeDeadlinePassed: {
id: 'learner-dash.courseCard.banners.upgradeDeadlinePassed',
description: 'Audit upgrade deadline passed banner message',
defaultMessage: 'Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.',
},
exploreCourseDetails: {
id: 'learner-dash.courseCard.banners.exploreCourseDetails',
description: 'Action prompt taking learners to course details page',
defaultMessage: 'Explore course details.',
},
certRestricted: {
id: 'learner-dash.courseCard.banners.certificateRestricted',
description: 'Restricted certificate warning message',

View File

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

View File

@@ -10,8 +10,8 @@ export const useAccessMessage = ({ cardId }) => {
const courseRun = reduxHooks.useCardCourseRunData(cardId);
const formatDate = utilHooks.useFormatDate();
if (!courseRun.isStarted) {
if (!courseRun.startDate) { return null; }
const startDate = formatDate(courseRun.startDate);
if (!courseRun.startDate && !courseRun.advertisedStart) { return null; }
const startDate = courseRun.advertisedStart ? courseRun.advertisedStart : formatDate(courseRun.startDate);
return formatMessage(messages.courseStarts, { startDate });
}
if (enrollment.isEnrolled) {

View File

@@ -20,6 +20,16 @@ jest.mock('hooks', () => ({
},
}));
// jest.mock('@edx/frontend-platform/i18n', () => {
// const { formatMessage } = jest.requireActual('testUtils');
// return {
// ...jest.requireActual('@edx/frontend-platform/i18n'),
// useIntl: () => ({
// formatMessage,
// }),
// };
// });
const cardId = 'my-test-card-id';
const courseNumber = 'test-course-number';
const useAccessMessage = 'test-access-message';

View File

@@ -1,7 +1,3 @@
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
a.course-card-title {
color: $black;
color: var(--pgn-color-black);
}

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import CourseCardDetails from '.';
@@ -48,23 +47,36 @@ describe('CourseCard Details component', () => {
return separatorsCount;
};
test('has change session button on entitlement course', () => {
it('has change session button on entitlement course', () => {
const wrapper = createWrapper();
expect(wrapper.container).toMatchSnapshot();
const sessionButton = screen.getByRole('button', { name: defaultHooks.changeOrLeaveSessionMessage });
expect(sessionButton).toBeInTheDocument();
const accessMessage = screen.getByText((text) => text.includes(defaultHooks.accessMessage));
expect(accessMessage).toBeInTheDocument();
// it has 3 separator, 4 column
expect(fetchSeparators(wrapper)).toBe(3);
});
test('has change session button on entitlement course but no access message', () => {
it('has change session button on entitlement course but no access message', () => {
const wrapper = createWrapper({ accessMessage: null });
expect(wrapper.container).toMatchSnapshot();
const sessionButton = screen.getByRole('button', { name: defaultHooks.changeOrLeaveSessionMessage });
expect(sessionButton).toBeInTheDocument();
const accessMessage = screen.queryByText((text) => text.includes(defaultHooks.accessMessage));
expect(accessMessage).toBeNull();
// it has 2 separator, 3 column
expect(fetchSeparators(wrapper)).toBe(2);
});
test('does not have change session button on regular course', () => {
it('does not have change session button on regular course', () => {
const wrapper = createWrapper({ isEntitlement: false });
expect(wrapper.container).toMatchSnapshot();
const sessionButton = screen.queryByRole('button', { name: defaultHooks.changeOrLeaveSessionMessage });
expect(sessionButton).toBeNull();
const accessMessage = screen.getByText((text) => text.includes(defaultHooks.accessMessage));
expect(accessMessage).toBeInTheDocument();
// it has 2 separator, 3 column
expect(fetchSeparators(wrapper)).toBe(2);
});

View File

@@ -20,11 +20,13 @@ export const CourseCardImage = ({ cardId, orientation }) => {
const { isVerified } = reduxHooks.useCardEnrollmentData(cardId);
const { disableCourseTitle } = useActionDisabledState(cardId);
const handleImageClicked = reduxHooks.useTrackCourseEvent(courseImageClicked, cardId, homeUrl);
const wrapperClassName = `pgn__card-wrapper-image-cap overflow-visible ${orientation}`;
const wrapperClassName = `pgn__card-wrapper-image-cap d-inline-block overflow-visible ${orientation}`;
const image = (
<>
<img
className="pgn__card-image-cap show"
// w-100 is necessary for images on Safari, otherwise stretches full height of the image
// https://stackoverflow.com/a/44250830
className="pgn__card-image-cap w-100 show"
src={bannerImgSrc}
alt={formatMessage(messages.bannerAlt)}
/>

View File

@@ -1,61 +1,72 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import useActionDisabledState from './hooks';
import CourseCardImage from './CourseCardImage';
import { CourseCardImage } from './CourseCardImage';
import messages from '../messages';
const homeUrl = 'home-url';
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@openedx/paragon');
jest.unmock('react');
jest.mock('tracking', () => ({
course: {
courseImageClicked: jest.fn().mockName('segment.courseImageClicked'),
},
}));
const homeUrl = 'https://example.com';
const bannerImgSrc = 'banner-img-src.jpg';
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(() => ({ bannerImgSrc: 'banner-img-src' })),
useCardCourseData: jest.fn(() => ({ bannerImgSrc })),
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
useCardEnrollmentData: jest.fn(() => ({ isVerified: true })),
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
trackCourseEvent: { eventName, cardId, upgradeUrl },
useCardEnrollmentData: jest.fn(),
useTrackCourseEvent: jest.fn((eventName, cardId, url) => ({
trackCourseEvent: { eventName, cardId, url },
})),
},
}));
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
jest.mock('./hooks', () => jest.fn());
describe('CourseCardImage', () => {
const props = {
cardId: 'cardId',
orientation: 'orientation',
cardId: 'test-card-id',
orientation: 'horizontal',
};
beforeEach(() => {
jest.clearAllMocks();
it('renders course image with correct attributes', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: true });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
const image = screen.getByRole('img', { name: formatMessage(messages.bannerAlt) });
expect(image).toBeInTheDocument();
expect(image.src).toContain(bannerImgSrc);
expect(image.parentElement).toHaveClass('horizontal');
});
describe('snapshot', () => {
test('renders clickable link course Image', () => {
const wrapper = shallow(<CourseCardImage {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.type).toBe('a');
expect(wrapper.instance.props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(
track.course.courseImageClicked,
props.cardId,
homeUrl,
),
);
});
test('renders disabled link', () => {
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
const wrapper = shallow(<CourseCardImage {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
expect(wrapper.instance.type).toBe('div');
});
it('isVerified, should render badge', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
const badge = screen.getByText(formatMessage(messages.verifiedBanner));
expect(badge).toBeInTheDocument();
const badgeImg = screen.getByRole('img', { name: formatMessage(messages.verifiedBannerRibbonAlt) });
expect(badgeImg).toBeInTheDocument();
});
describe('behavior', () => {
it('renders link with correct href if disableCourseTitle is false', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: false });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', homeUrl);
});
describe('hooks', () => {
it('initializes', () => {
shallow(<CourseCardImage {...props} />);
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
reduxHooks.useCardEnrollmentData.mockReturnValue({ isVerified: true });
render(<IntlProvider locale="en"><CourseCardImage {...props} /></IntlProvider>);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
props.cardId,

View File

@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import * as ReactShare from 'react-share';
import { StrictDict } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@openedx/paragon';
@@ -11,9 +10,9 @@ import { reduxHooks } from 'hooks';
import messages from './messages';
export const testIds = StrictDict({
export const testIds = {
emailSettingsModalToggle: 'emailSettingsModalToggle',
});
};
export const SocialShareMenu = ({ cardId, emailSettings }) => {
const { formatMessage } = useIntl();

View File

@@ -1,32 +1,19 @@
import { when } from 'jest-when';
import * as ReactShare from 'react-share';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage, shallow } from '@edx/react-unit-test-utils';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import { useEmailSettings } from './hooks';
import SocialShareMenu, { testIds } from './SocialShareMenu';
import SocialShareMenu from './SocialShareMenu';
import messages from './messages';
jest.mock('react-share', () => ({
FacebookShareButton: () => 'FacebookShareButton',
TwitterShareButton: () => 'TwitterShareButton',
}));
jest.mock('tracking', () => ({
socialShare: 'test-social-share-key',
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn().mockReturnValue({
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
}),
}));
jest.mock('hooks', () => ({
reduxHooks: {
useMasqueradeData: jest.fn(),
@@ -40,6 +27,10 @@ jest.mock('./hooks', () => ({
useEmailSettings: jest.fn(),
}));
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@openedx/paragon');
jest.unmock('react');
const props = {
cardId: 'test-card-id',
emailSettings: { show: jest.fn() },
@@ -47,9 +38,9 @@ const props = {
const mockHook = (fn, returnValue, options = {}) => {
if (options.isCardHook) {
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue);
when(fn).calledWith(props.cardId).mockReturnValue(returnValue);
} else {
when(fn).calledWith().mockReturnValueOnce(returnValue);
when(fn).calledWith().mockReturnValue(returnValue);
}
};
@@ -89,19 +80,13 @@ const mockHooks = (returnVals = {}) => {
);
};
let el;
const render = () => {
el = shallow(<SocialShareMenu {...props} />);
};
const renderComponent = () => render(<IntlProvider locale="en"><SocialShareMenu {...props} /></IntlProvider>);
describe('SocialShareMenu', () => {
describe('behavior', () => {
beforeEach(() => {
mockHooks();
render();
});
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalledWith();
renderComponent();
});
it('initializes local hooks', () => {
when(useEmailSettings).expectCalledWith();
@@ -118,53 +103,43 @@ describe('SocialShareMenu', () => {
describe('render', () => {
it('renders null if exec ed course', () => {
mockHooks({ isExecEd2UCourse: true });
render();
expect(el.isEmptyRender()).toEqual(true);
renderComponent();
const emailSettingsButton = screen.queryByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeNull();
const facebookShareButton = screen.queryByRole('button', { name: 'facebook' });
expect(facebookShareButton).toBeNull();
const twitterShareButton = screen.queryByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeNull();
});
const testEmailSettingsDropdown = (isMasquerading = false) => {
describe('email settings dropdown', () => {
const loadToggle = () => el.instance.findByTestId(testIds.emailSettingsModalToggle)[0];
it('renders', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(1);
const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeInTheDocument();
});
if (isMasquerading) {
it('is disabled', () => {
expect(loadToggle().props.disabled).toEqual(true);
const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toHaveAttribute('aria-disabled', 'true');
});
} else {
it('is enabled', () => {
expect(loadToggle().props.disabled).toEqual(false);
const emailSettingsButton = screen.getByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeEnabled();
});
}
test('show email settings modal on click', () => {
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
});
});
};
const testFacebookShareButton = () => {
test('renders facebook share button with courseName and brand', () => {
const button = el.instance.findByType(ReactShare.FacebookShareButton)[0];
expect(button.props.url).toEqual(socialShare.facebook.shareUrl);
expect(button.props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(track.socialShare, props.cardId, 'facebook'),
);
expect(button.props.title).toEqual(formatMessage(messages.shareQuote, {
courseName,
socialBrand: socialShare.facebook.socialBrand,
}));
it('renders facebook share button', () => {
const facebookShareButton = screen.getByRole('button', { name: 'facebook' });
expect(facebookShareButton).toBeInTheDocument();
});
};
const testTwitterShareButton = () => {
test('renders twitter share button with courseName and brand', () => {
const button = el.instance.findByType(ReactShare.TwitterShareButton)[0];
expect(button.props.url).toEqual(socialShare.twitter.shareUrl);
expect(button.props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(track.socialShare, props.cardId, 'twitter'),
);
expect(button.props.title).toEqual(formatMessage(messages.shareQuote, {
courseName,
socialBrand: socialShare.twitter.socialBrand,
}));
it('renders twitter share button', () => {
const twitterShareButton = screen.getByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeInTheDocument();
});
};
describe('all enabled', () => {
@@ -174,19 +149,7 @@ describe('SocialShareMenu', () => {
twitter: { isEnabled: true },
isEmailEnabled: true,
});
render();
});
describe('email settings dropdown', () => {
const loadToggle = () => el.instance.findByTestId(testIds.emailSettingsModalToggle)[0];
it('renders', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(1);
});
it('is enabled', () => {
expect(loadToggle().props.disabled).toEqual(false);
});
test('show email settings modal on click', () => {
expect(loadToggle().props.onClick).toEqual(props.emailSettings.show);
});
renderComponent();
});
testEmailSettingsDropdown();
testFacebookShareButton();
@@ -194,42 +157,49 @@ describe('SocialShareMenu', () => {
});
describe('only email enabled', () => {
beforeEach(() => {
jest.clearAllMocks();
mockHooks({ isEmailEnabled: true });
render();
renderComponent();
});
testEmailSettingsDropdown();
it('does not render facebook or twitter controls', () => {
expect(el.instance.findByType(ReactShare.FacebookShareButton).length).toEqual(0);
expect(el.instance.findByType(ReactShare.TwitterShareButton).length).toEqual(0);
const facebookShareButton = screen.queryByRole('button', { name: 'facebook' });
expect(facebookShareButton).toBeNull();
const twitterShareButton = screen.queryByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeNull();
});
describe('masquerading', () => {
beforeEach(() => {
mockHooks({ isEmailEnabled: true, isMasquerading: true });
render();
});
testEmailSettingsDropdown(true);
});
describe('masquerading', () => {
beforeEach(() => {
mockHooks({ isEmailEnabled: true, isMasquerading: true });
renderComponent();
});
testEmailSettingsDropdown(true);
});
describe('only facebook enabled', () => {
beforeEach(() => {
mockHooks({ facebook: { isEnabled: true } });
render();
renderComponent();
});
testFacebookShareButton();
it('does not render email or twitter controls', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(0);
expect(el.instance.findByType(ReactShare.TwitterShareButton).length).toEqual(0);
const emailSettingsButton = screen.queryByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeNull();
const twitterShareButton = screen.queryByRole('button', { name: 'twitter' });
expect(twitterShareButton).toBeNull();
});
});
describe('only twitter enabled', () => {
beforeEach(() => {
mockHooks({ twitter: { isEnabled: true } });
render();
renderComponent();
});
testTwitterShareButton();
it('does not render email or facebook controls', () => {
expect(el.instance.findByTestId(testIds.emailSettingsModalToggle).length).toEqual(0);
expect(el.instance.findByType(ReactShare.FacebookShareButton).length).toEqual(0);
const emailSettingsButton = screen.queryByRole('button', { name: messages.emailSettings.defaultMessage });
expect(emailSettingsButton).toBeNull();
const facebookShareButton = screen.queryByRole('button', { name: 'facebook' });
expect(facebookShareButton).toBeNull();
});
});
});

View File

@@ -1,81 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardMenu render show dropdown hide unenroll item and disable email snapshot 1`] = `
<Fragment>
<Dropdown
onToggle={[MockFunction hooks.handleToggleDropdown]}
>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
iconAs="Icon"
id="course-actions-dropdown-test-card-id"
src={[MockFunction icons.MoreVert]}
variant="primary"
/>
<Dropdown.Menu>
<SocialShareMenu
cardId="test-card-id"
emailSettings={
{
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
}
}
/>
</Dropdown.Menu>
</Dropdown>
<UnenrollConfirmModal
cardId="test-card-id"
closeModal={[MockFunction unenrollHide]}
show={false}
/>
</Fragment>
`;
exports[`CourseCardMenu render show dropdown show unenroll and enable email snapshot 1`] = `
<Fragment>
<Dropdown
onToggle={[MockFunction hooks.handleToggleDropdown]}
>
<Dropdown.Toggle
alt="Course actions dropdown"
as="IconButton"
iconAs="Icon"
id="course-actions-dropdown-test-card-id"
src={[MockFunction icons.MoreVert]}
variant="primary"
/>
<Dropdown.Menu>
<Dropdown.Item
data-testid="unenrollModalToggle"
disabled={false}
onClick={[MockFunction unenrollShow]}
>
Unenroll
</Dropdown.Item>
<SocialShareMenu
cardId="test-card-id"
emailSettings={
{
"hide": [MockFunction emailSettingHide],
"isVisible": false,
"show": [MockFunction emailSettingShow],
}
}
/>
</Dropdown.Menu>
</Dropdown>
<UnenrollConfirmModal
cardId="test-card-id"
closeModal={[MockFunction unenrollHide]}
show={false}
/>
<EmailSettingsModal
cardId="test-card-id"
closeModal={[MockFunction emailSettingHide]}
show={false}
/>
</Fragment>
`;

View File

@@ -1,15 +1,15 @@
import { useKeyedState, StrictDict } from '@edx/react-unit-test-utils';
import track from 'tracking';
import { reduxHooks } from 'hooks';
import { useState } from 'react';
import { StrictDict } from 'utils';
export const stateKeys = StrictDict({
isUnenrollConfirmVisible: 'isUnenrollConfirmVisible',
isEmailSettingsVisible: 'isEmailSettingsVisible',
export const state = StrictDict({
isUnenrollConfirmVisible: (val) => useState(val), // eslint-disable-line
isEmailSettingsVisible: (val) => useState(val), // eslint-disable-line
});
export const useUnenrollData = () => {
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isUnenrollConfirmVisible, false);
const [isVisible, setIsVisible] = state.isUnenrollConfirmVisible(false);
return {
show: () => setIsVisible(true),
hide: () => setIsVisible(false),
@@ -18,7 +18,7 @@ export const useUnenrollData = () => {
};
export const useEmailSettings = () => {
const [isVisible, setIsVisible] = useKeyedState(stateKeys.isEmailSettingsVisible, false);
const [isVisible, setIsVisible] = state.isEmailSettingsVisible(false);
return {
show: () => setIsVisible(true),
hide: () => setIsVisible(false),

View File

@@ -1,7 +1,6 @@
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import { MockUseState } from 'testUtils';
import * as hooks from './hooks';
@@ -19,7 +18,7 @@ reduxHooks.useTrackCourseEvent.mockReturnValue(trackCourseEvent);
const cardId = 'test-card-id';
let out;
const state = mockUseKeyedState(hooks.stateKeys);
const state = new MockUseState(hooks);
describe('CourseCardMenu hooks', () => {
beforeEach(() => {
@@ -28,7 +27,6 @@ describe('CourseCardMenu hooks', () => {
});
describe('useUnenrollData', () => {
beforeEach(() => {
state.mockVals({ isUnenrollConfirmVisible: true });
out = hooks.useUnenrollData();
});
describe('behavior', () => {
@@ -37,9 +35,6 @@ describe('CourseCardMenu hooks', () => {
});
});
describe('output', () => {
test('state is loaded from current state value', () => {
expect(out.isVisible).toEqual(true);
});
test('show sets state value to true', () => {
out.show();
expect(state.setState.isUnenrollConfirmVisible).toHaveBeenCalledWith(true);
@@ -53,7 +48,6 @@ describe('CourseCardMenu hooks', () => {
describe('useEmailSettings', () => {
beforeEach(() => {
state.mockVals({ isEmailSettingsVisible: true });
out = hooks.useEmailSettings();
});
describe('behavior', () => {
@@ -62,9 +56,6 @@ describe('CourseCardMenu hooks', () => {
});
});
describe('output', () => {
test('state is loaded from current state value', () => {
expect(out.isVisible).toEqual(state.values.isEmailSettingsVisible);
});
test('show sets state value to true', () => {
out.show();
expect(state.setState.isEmailSettingsVisible).toHaveBeenCalledWith(true);

View File

@@ -1,10 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown, Icon, IconButton } from '@openedx/paragon';
import { MoreVert } from '@openedx/paragon/icons';
import { StrictDict } from '@edx/react-unit-test-utils';
import EmailSettingsModal from 'containers/EmailSettingsModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
@@ -19,9 +17,9 @@ import {
import messages from './messages';
export const testIds = StrictDict({
export const testIds = {
unenrollModalToggle: 'unenrollModalToggle',
});
};
export const CourseCardMenu = ({ cardId }) => {
const { formatMessage } = useIntl();

View File

@@ -1,26 +1,23 @@
import { when } from 'jest-when';
import { Dropdown } from '@openedx/paragon';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import EmailSettingsModal from 'containers/EmailSettingsModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import { reduxHooks } from 'hooks';
import SocialShareMenu from './SocialShareMenu';
import * as hooks from './hooks';
import CourseCardMenu, { testIds } from '.';
import CourseCardMenu from '.';
import messages from './messages';
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn().mockReturnValue({
formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage,
}),
}));
jest.mock('hooks', () => ({
reduxHooks: { useMasqueradeData: jest.fn(), useCardEnrollmentData: jest.fn() },
reduxHooks: {
useMasqueradeData: jest.fn(),
useCardEnrollmentData: jest.fn(),
},
}));
jest.mock('./SocialShareMenu', () => 'SocialShareMenu');
jest.mock('./SocialShareMenu', () => jest.fn(() => <div>SocialShareMenu</div>));
jest.mock('containers/EmailSettingsModal', () => jest.fn(() => <div>EmailSettingsModal</div>));
jest.mock('containers/UnenrollConfirmModal', () => jest.fn(() => <div>UnenrollConfirmModal</div>));
jest.mock('./hooks', () => ({
useEmailSettings: jest.fn(),
useUnenrollData: jest.fn(),
@@ -28,6 +25,10 @@ jest.mock('./hooks', () => ({
useOptionVisibility: jest.fn(),
}));
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@openedx/paragon');
jest.unmock('react');
const props = {
cardId: 'test-card-id',
};
@@ -44,13 +45,11 @@ const unenrollData = {
hide: jest.fn().mockName('unenrollHide'),
};
let el;
const mockHook = (fn, returnValue, options = {}) => {
if (options.isCardHook) {
when(fn).calledWith(props.cardId).mockReturnValueOnce(returnValue);
when(fn).calledWith(props.cardId).mockReturnValue(returnValue);
} else {
when(fn).calledWith().mockReturnValueOnce(returnValue);
when(fn).calledWith().mockReturnValue(returnValue);
}
};
@@ -82,18 +81,13 @@ const mockHooks = (returnVals = {}) => {
);
};
const render = () => {
el = shallow(<CourseCardMenu {...props} />);
};
const renderComponent = () => render(<IntlProvider locale="en"><CourseCardMenu {...props} /></IntlProvider>);
describe('CourseCardMenu', () => {
describe('behavior', () => {
describe('hooks', () => {
beforeEach(() => {
mockHooks();
render();
});
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalledWith();
renderComponent();
});
it('initializes local hooks', () => {
when(hooks.useEmailSettings).expectCalledWith();
@@ -109,47 +103,30 @@ describe('CourseCardMenu', () => {
describe('render', () => {
it('renders null if showDropdown is false', () => {
mockHooks();
render();
expect(el.isEmptyRender()).toEqual(true);
renderComponent();
const dropdown = screen.queryByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeNull();
});
const testHandleToggle = () => {
it('displays Dropdown with onToggle=handleToggleDropdown', () => {
expect(el.instance.findByType(Dropdown)[0].props.onToggle).toEqual(handleToggleDropdown);
});
};
const testUnenrollConfirmModal = () => {
it('displays UnenrollConfirmModal with cardId and unenrollModal data', () => {
const modal = el.instance.findByType(UnenrollConfirmModal)[0];
expect(modal.props.show).toEqual(unenrollData.isVisible);
expect(modal.props.closeModal).toEqual(unenrollData.hide);
expect(modal.props.cardId).toEqual(props.cardId);
});
};
const testSocialShareMenu = () => {
it('displays SocialShareMenu with cardID and emailSettings', () => {
const menu = el.instance.findByType(SocialShareMenu)[0];
expect(menu.props.cardId).toEqual(props.cardId);
expect(menu.props.emailSettings).toEqual(emailSettings);
});
};
describe('show dropdown', () => {
describe('hide unenroll item and disable email', () => {
beforeEach(() => {
mockHooks({ shouldShowDropdown: true });
render();
it('displays Dropdown and renders SocialShareMenu and UnenrollConfirmModal', async () => {
mockHooks({
shouldShowDropdown: true,
});
renderComponent();
const user = userEvent.setup();
const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeInTheDocument();
await user.click(dropdown);
const unenrollOption = screen.queryByRole('button', { name: messages.unenroll.defaultMessage });
expect(unenrollOption).toBeNull();
const socialShareMenu = screen.getByText('SocialShareMenu');
expect(socialShareMenu).toBeInTheDocument();
const unenrollConfirmModal = screen.getByText('UnenrollConfirmModal');
expect(unenrollConfirmModal).toBeInTheDocument();
const emailSettingsModal = screen.queryByText('EmailSettingsModal');
expect(emailSettingsModal).toBeNull();
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
testHandleToggle();
testSocialShareMenu();
it('does not render unenroll modal toggle', () => {
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(0);
});
it('does not render EmailSettingsModal', () => {
expect(el.instance.findByType(EmailSettingsModal).length).toEqual(0);
});
testUnenrollConfirmModal();
});
describe('show unenroll and enable email', () => {
const hookProps = {
@@ -157,57 +134,49 @@ describe('CourseCardMenu', () => {
isEmailEnabled: true,
shouldShowUnenrollItem: true,
};
beforeEach(() => {
mockHooks(hookProps);
render();
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
testHandleToggle();
testSocialShareMenu();
describe('unenroll modal toggle', () => {
let toggle;
describe('not masquerading', () => {
beforeEach(() => {
it('renders all components', async () => {
mockHooks(hookProps);
render();
[toggle] = el.instance.findByTestId(testIds.unenrollModalToggle);
});
it('renders unenroll modal toggle', () => {
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(1);
});
test('onClick from unenroll modal hook', () => {
expect(toggle.props.onClick).toEqual(unenrollData.show);
});
test('disabled', () => {
expect(toggle.props.disabled).toEqual(false);
renderComponent();
const user = userEvent.setup();
const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeInTheDocument();
await user.click(dropdown);
const unenrollOption = screen.getByRole('button', { name: messages.unenroll.defaultMessage });
expect(unenrollOption).toBeInTheDocument();
const socialShareMenu = screen.getByText('SocialShareMenu');
expect(socialShareMenu).toBeInTheDocument();
const unenrollConfirmModal = screen.getByText('UnenrollConfirmModal');
expect(unenrollConfirmModal).toBeInTheDocument();
const emailSettingsModal = screen.getByText('EmailSettingsModal');
expect(emailSettingsModal).toBeInTheDocument();
});
});
describe('masquerading', () => {
beforeEach(() => {
it('renders but unenroll is disabled', async () => {
mockHooks({ ...hookProps, isMasquerading: true });
render();
[toggle] = el.instance.findByTestId(testIds.unenrollModalToggle);
});
it('renders', () => {
expect(el.instance.findByTestId(testIds.unenrollModalToggle).length).toEqual(1);
});
test('onClick from unenroll modal hook', () => {
expect(toggle.props.onClick).toEqual(unenrollData.show);
});
test('disabled', () => {
expect(toggle.props.disabled).toEqual(true);
renderComponent();
const user = userEvent.setup();
const dropdown = screen.getByRole('button', { name: messages.dropdownAlt.defaultMessage });
expect(dropdown).toBeInTheDocument();
await user.click(dropdown);
const unenrollOption = screen.getByRole('button', { name: messages.unenroll.defaultMessage });
expect(unenrollOption).toBeInTheDocument();
expect(unenrollOption).toHaveAttribute('aria-disabled', 'true');
const socialShareMenu = screen.getByText('SocialShareMenu');
expect(socialShareMenu).toBeInTheDocument();
const unenrollConfirmModal = screen.getByText('UnenrollConfirmModal');
expect(unenrollConfirmModal).toBeInTheDocument();
const emailSettingsModal = screen.getByText('EmailSettingsModal');
expect(emailSettingsModal).toBeInTheDocument();
});
});
});
testUnenrollConfirmModal();
it('displays EmaiSettingsModal with cardId and emailSettingsModal data', () => {
const modal = el.instance.findByType(EmailSettingsModal)[0];
expect(modal.props.show).toEqual(emailSettings.isVisible);
expect(modal.props.closeModal).toEqual(emailSettings.hide);
expect(modal.props.cardId).toEqual(props.cardId);
});
});
});
});

View File

@@ -1,12 +1,10 @@
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { reduxHooks } from 'hooks';
import track from 'tracking';
import useActionDisabledState from './hooks';
import CourseCardTitle from './CourseCardTitle';
const homeUrl = 'home-url';
jest.mock('tracking', () => ({
course: {
courseTitleClicked: jest.fn().mockName('segment.courseTitleClicked'),
@@ -15,53 +13,65 @@ jest.mock('tracking', () => ({
jest.mock('hooks', () => ({
reduxHooks: {
useCardCourseData: jest.fn(() => ({ courseName: 'course-name' })),
useCardCourseRunData: jest.fn(() => ({ homeUrl })),
useTrackCourseEvent: jest.fn((eventName, cardId, upgradeUrl) => ({
trackCourseEvent: { eventName, cardId, upgradeUrl },
})),
useCardCourseData: jest.fn(),
useCardCourseRunData: jest.fn(),
useTrackCourseEvent: jest.fn(),
},
}));
jest.mock('./hooks', () => jest.fn(() => ({ disableCourseTitle: false })));
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@openedx/paragon');
jest.unmock('react');
describe('CourseCardTitle', () => {
const props = {
cardId: 'cardId',
cardId: 'test-card-id',
};
const courseName = 'Test Course';
const homeUrl = 'http://test.com';
const handleTitleClick = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
reduxHooks.useCardCourseData.mockReturnValue({ courseName });
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
reduxHooks.useTrackCourseEvent.mockReturnValue(handleTitleClick);
});
describe('snapshot', () => {
test('renders clickable link course title', () => {
const wrapper = shallow(<CourseCardTitle {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
const title = wrapper.instance.findByTestId('CourseCardTitle');
expect(title[0].type).toBe('a');
expect(title[0].props.onClick).toEqual(
reduxHooks.useTrackCourseEvent(
track.course.courseTitleClicked,
props.cardId,
homeUrl,
),
);
});
test('renders disabled link', () => {
useActionDisabledState.mockReturnValueOnce({ disableCourseTitle: true });
const wrapper = shallow(<CourseCardTitle {...props} />);
expect(wrapper.snapshot).toMatchSnapshot();
const title = wrapper.instance.findByTestId('CourseCardTitle');
expect(title[0].type).toBe('span');
expect(title[0].props.onClick).toBeUndefined();
});
it('renders course name as link when not disabled', async () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
render(<CourseCardTitle {...props} />);
const user = userEvent.setup();
const link = screen.getByRole('link', { name: courseName });
expect(link).toHaveAttribute('href', homeUrl);
await user.click(link);
expect(handleTitleClick).toHaveBeenCalled();
});
describe('behavior', () => {
it('initializes', () => {
shallow(<CourseCardTitle {...props} />);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(
props.cardId,
);
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
});
it('renders course name as span when disabled', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: true });
render(<CourseCardTitle {...props} />);
const text = screen.getByText(courseName);
expect(text).toBeInTheDocument();
expect(text.tagName.toLowerCase()).toBe('span');
});
it('uses correct hooks with cardId', () => {
useActionDisabledState.mockReturnValue({ disableCourseTitle: false });
render(<CourseCardTitle {...props} />);
expect(reduxHooks.useCardCourseData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
expect(reduxHooks.useTrackCourseEvent).toHaveBeenCalledWith(
track.course.courseTitleClicked,
props.cardId,
homeUrl,
);
});
});

View File

@@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RelatedProgramsBadge component snapshot: 3 programs 1`] = `
<Fragment>
<Button
className="pl-0 mr-0 justify-content-start align-self-start flex-shrink-1"
data-testid="RelatedProgramsBadge"
onClick={[MockFunction useRelatedProgramsBadge.openModal]}
size="sm"
variant="tertiary"
>
<Icon
className="mr-2 pr-0"
src={[MockFunction icons.Program]}
/>
useRelatedProgramsBadge.programsMessage
</Button>
<RelatedProgramsModal
cardId="test-course-number"
closeModal={[MockFunction useRelatedProgramsBadge.closeModal]}
isOpen={true}
/>
</Fragment>
`;

View File

@@ -12,6 +12,16 @@ jest.mock('hooks', () => ({
},
}));
// jest.mock('@edx/frontend-platform/i18n', () => {
// const { formatMessage } = jest.requireActual('testUtils');
// return {
// ...jest.requireActual('@edx/frontend-platform/i18n'),
// useIntl: () => ({
// formatMessage,
// }),
// };
// });
const cardId = 'test-card-id';
const state = new MockUseState(hooks);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import useRelatedProgramsBadge from './hooks';
import RelatedProgramsBadge from '.';
@@ -7,6 +7,10 @@ import RelatedProgramsBadge from '.';
jest.mock('containers/RelatedProgramsModal', () => 'RelatedProgramsModal');
jest.mock('./hooks', () => jest.fn());
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@openedx/paragon');
jest.unmock('react');
const hookProps = {
isOpen: true,
openModal: jest.fn().mockName('useRelatedProgramsBadge.openModal'),
@@ -15,16 +19,21 @@ const hookProps = {
programsMessage: 'useRelatedProgramsBadge.programsMessage',
};
const cardId = 'test-course-number';
const cardId = 'test-card-id';
describe('RelatedProgramsBadge component', () => {
test('empty render: no programs', () => {
it('should not render if no programs', () => {
useRelatedProgramsBadge.mockReturnValueOnce({ ...hookProps, numPrograms: 0 });
const el = shallow(<RelatedProgramsBadge cardId={cardId} />);
expect(el.isEmptyRender()).toEqual(true);
render(<IntlProvider locale="en"><RelatedProgramsBadge cardId={cardId} /></IntlProvider>);
const button = screen.queryByRole('button', { name: hookProps.programsMessage });
expect(button).toBeNull();
const dialog = screen.queryByRole('dialog');
expect(dialog).toBeNull();
});
test('snapshot: 3 programs', () => {
useRelatedProgramsBadge.mockReturnValueOnce(hookProps);
expect(shallow(<RelatedProgramsBadge cardId={cardId} />).snapshot).toMatchSnapshot();
it('3 programs closed', () => {
useRelatedProgramsBadge.mockReturnValue({ ...hookProps, isOpen: false });
render(<IntlProvider locale="en"><RelatedProgramsBadge cardId={cardId} /></IntlProvider>);
const button = screen.getByRole('button', { name: hookProps.programsMessage });
expect(button).toBeInTheDocument();
});
});

View File

@@ -1,72 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardImage snapshot renders clickable link course Image 1`] = `
<a
className="pgn__card-wrapper-image-cap overflow-visible orientation"
href="home-url"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseImageClicked],
"upgradeUrl": "home-url",
},
}
}
tabIndex="-1"
>
<Fragment>
<img
alt="Course thumbnail"
className="pgn__card-image-cap show"
src="banner-img-src"
/>
<span
className="course-card-verify-ribbon-container"
title="You're enrolled as a verified student"
>
<Badge
as="div"
className="w-100"
variant="success"
>
Verified
</Badge>
<img
alt="ID Verified Ribbon/Badge"
src="test-file-stub"
/>
</span>
</Fragment>
</a>
`;
exports[`CourseCardImage snapshot renders disabled link 1`] = `
<div
className="pgn__card-wrapper-image-cap overflow-visible orientation"
>
<Fragment>
<img
alt="Course thumbnail"
className="pgn__card-image-cap show"
src="banner-img-src"
/>
<span
className="course-card-verify-ribbon-container"
title="You're enrolled as a verified student"
>
<Badge
as="div"
className="w-100"
variant="success"
>
Verified
</Badge>
<img
alt="ID Verified Ribbon/Badge"
src="test-file-stub"
/>
</span>
</Fragment>
</div>
`;

View File

@@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseCardTitle snapshot renders clickable link course title 1`] = `
<h3>
<a
className="course-card-title"
data-testid="CourseCardTitle"
href="home-url"
onClick={
{
"trackCourseEvent": {
"cardId": "cardId",
"eventName": [MockFunction segment.courseTitleClicked],
"upgradeUrl": "home-url",
},
}
}
>
course-name
</a>
</h3>
`;
exports[`CourseCardTitle snapshot renders disabled link 1`] = `
<h3>
<span
className="course-card-title"
data-testid="CourseCardTitle"
>
course-name
</span>
</h3>
`;

View File

@@ -3,19 +3,18 @@ import { reduxHooks } from 'hooks';
export const useActionDisabledState = (cardId) => {
const { isMasquerading } = reduxHooks.useMasqueradeData();
const {
canUpgrade, hasAccess, isAudit, isAuditAccessExpired,
hasAccess, isAudit, isAuditAccessExpired,
} = reduxHooks.useCardEnrollmentData(cardId);
const {
isEntitlement, isFulfilled, canChange, hasSessions,
} = reduxHooks.useCardEntitlementData(cardId);
const { resumeUrl, homeUrl, upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
const { resumeUrl, homeUrl } = reduxHooks.useCardCourseRunData(cardId);
const disableBeginCourse = !homeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
const disableResumeCourse = !resumeUrl || (isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired));
const disableViewCourse = !hasAccess || (isAudit && isAuditAccessExpired);
const disableSelectSession = !isEntitlement || isMasquerading || !hasAccess || (!canChange || !hasSessions);
const disableUpgradeCourse = !upgradeUrl || (isMasquerading && !canUpgrade);
const disableCourseTitle = (isEntitlement && !isFulfilled) || disableViewCourse;
@@ -23,7 +22,6 @@ export const useActionDisabledState = (cardId) => {
disableBeginCourse,
disableResumeCourse,
disableViewCourse,
disableUpgradeCourse,
disableSelectSession,
disableCourseTitle,
};

View File

@@ -16,7 +16,6 @@ const cardId = 'my-test-course-number';
describe('useActionDisabledState', () => {
const defaultData = {
isMasquerading: false,
canUpgrade: false,
isEntitlement: false,
isFulfilled: false,
canChange: false,
@@ -26,12 +25,10 @@ describe('useActionDisabledState', () => {
isAuditAccessExpired: false,
resumeUrl: 'resume.url',
homeUrl: 'home.url',
upgradeUrl: 'upgrade.url',
};
const mockHooksData = (args) => {
const {
isMasquerading,
canUpgrade,
isEntitlement,
isFulfilled,
canChange,
@@ -41,11 +38,9 @@ describe('useActionDisabledState', () => {
isAuditAccessExpired,
resumeUrl,
homeUrl,
upgradeUrl,
} = { ...defaultData, ...args };
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
canUpgrade,
hasAccess,
isAudit,
isAuditAccessExpired,
@@ -59,7 +54,6 @@ describe('useActionDisabledState', () => {
reduxHooks.useCardCourseRunData.mockReturnValueOnce({
resumeUrl,
homeUrl,
upgradeUrl,
});
};
@@ -121,21 +115,6 @@ describe('useActionDisabledState', () => {
testDisabled({ hasAccess: true }, false);
});
});
describe('disableUpgradeCourse', () => {
const testDisabled = (data, expected) => {
mockHooksData(data);
expect(runHook().disableUpgradeCourse).toBe(expected);
};
it('disable when upgradeUrl is invalid', () => {
testDisabled({ upgradeUrl: null }, true);
});
it('disable when isMasquerading is true and canUpgrade is false', () => {
testDisabled({ isMasquerading: true, canUpgrade: false }, true);
});
it('enable when all conditions are met', () => {
testDisabled({ canUpgrade: true }, false);
});
});
describe('disableSelectSession', () => {
const testDisabled = (data, expected) => {
mockHooksData(data);

View File

@@ -11,6 +11,16 @@ jest.mock('hooks', () => ({
},
}));
// jest.mock('@edx/frontend-platform/i18n', () => {
// const { formatMessage } = jest.requireActual('testUtils');
// return {
// ...jest.requireActual('@edx/frontend-platform/i18n'),
// useIntl: () => ({
// formatMessage,
// }),
// };
// });
const cardId = 'my-test-course-number';
describe('CourseCard hooks', () => {

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import CourseCard from '.';
import hooks from './hooks';
@@ -8,22 +8,47 @@ jest.mock('./hooks', () => ({
useIsCollapsed: jest.fn(),
}));
jest.mock('./components/CourseCardBanners', () => 'CourseCardBanners');
jest.mock('./components/CourseCardImage', () => 'CourseCardImage');
jest.mock('./components/CourseCardMenu', () => 'CourseCardMenu');
jest.mock('./components/CourseCardActions', () => 'CourseCardActions');
jest.mock('./components/CourseCardDetails', () => 'CourseCardDetails');
jest.mock('./components/CourseCardTitle', () => 'CourseCardTitle');
const namesMockComponents = [
'CourseCardBanners',
'CourseCardImage',
'CourseCardMenu',
'CourseCardActions',
'CourseCardDetails',
'CourseCardTitle',
];
jest.mock('./components/CourseCardBanners', () => jest.fn(() => <div>CourseCardBanners</div>));
jest.mock('./components/CourseCardImage', () => jest.fn(() => <div>CourseCardImage</div>));
jest.mock('./components/CourseCardMenu', () => jest.fn(() => <div>CourseCardMenu</div>));
jest.mock('./components/CourseCardActions', () => jest.fn(() => <div>CourseCardActions</div>));
jest.mock('./components/CourseCardDetails', () => jest.fn(() => <div>CourseCardDetails</div>));
jest.mock('./components/CourseCardTitle', () => jest.fn(() => <div>CourseCardTitle</div>));
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@openedx/paragon');
jest.unmock('react');
const cardId = 'test-card-id';
describe('CourseCard component', () => {
test('snapshot: collapsed', () => {
it('collapsed', () => {
hooks.useIsCollapsed.mockReturnValueOnce(true);
expect(shallow(<CourseCard cardId={cardId} />).snapshot).toMatchSnapshot();
render(<IntlProvider locale="en"><CourseCard cardId={cardId} /></IntlProvider>);
const cardImage = screen.getByText('CourseCardImage');
expect(cardImage.parentElement).not.toHaveClass('d-flex');
});
test('snapshot: not collapsed', () => {
it('not collapsed', () => {
hooks.useIsCollapsed.mockReturnValueOnce(false);
expect(shallow(<CourseCard cardId={cardId} />).snapshot).toMatchSnapshot();
render(<IntlProvider locale="en"><CourseCard cardId={cardId} /></IntlProvider>);
const cardImage = screen.getByText('CourseCardImage');
expect(cardImage.parentElement).toHaveClass('d-flex');
});
it('renders courseCard child components', () => {
hooks.useIsCollapsed.mockReturnValueOnce(false);
render(<IntlProvider locale="en"><CourseCard cardId={cardId} /></IntlProvider>);
namesMockComponents.map((courseCardName) => {
const courseCardComponent = screen.getByText(courseCardName);
return expect(courseCardComponent).toBeInTheDocument();
});
});
});

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