Compare commits
485 Commits
aurora/use
...
release/ul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b1c56e850 | ||
|
|
0db621b134 | ||
|
|
aaf2e36fe9 | ||
|
|
b527fbcfba | ||
|
|
fa3f7b27cf | ||
|
|
3b0c58f376 | ||
|
|
f903392ca1 | ||
|
|
89032f09f4 | ||
|
|
03f146cce9 | ||
|
|
7dd2bda86e | ||
|
|
410f2f730f | ||
|
|
de13749443 | ||
|
|
0e66b2031d | ||
|
|
697c9ff2c8 | ||
|
|
fdb5d2f68c | ||
|
|
7cf79f8931 | ||
|
|
73b7b7f5d0 | ||
|
|
516544c7cc | ||
|
|
2134df5478 | ||
|
|
5345d2eef2 | ||
|
|
d94ac8dd62 | ||
|
|
ff925b06f1 | ||
|
|
2696486e5b | ||
|
|
5854b00d08 | ||
|
|
13e9b1a85f | ||
|
|
8116956a4b | ||
|
|
8d23e7585b | ||
|
|
b63a40006e | ||
|
|
92243063b9 | ||
|
|
a2673399aa | ||
|
|
cedcb4172d | ||
|
|
9e4faf1569 | ||
|
|
3a9acc981b | ||
|
|
e3c18698d8 | ||
|
|
eb38beedc3 | ||
|
|
01faf5a30a | ||
|
|
f4807614e2 | ||
|
|
ab448e52f2 | ||
|
|
3383016176 | ||
|
|
054cd57d4b | ||
|
|
0fc3cc4d53 | ||
|
|
e5c1244e59 | ||
|
|
3a389e14e1 | ||
|
|
6de409d7cc | ||
|
|
517b8424b3 | ||
|
|
49e527d810 | ||
|
|
fdc58a671a | ||
|
|
90aa652ca6 | ||
|
|
05a08101a2 | ||
|
|
cf94ea99b4 | ||
|
|
7f1e509ecf | ||
|
|
31c5445722 | ||
|
|
2acf7fbd73 | ||
|
|
cae7b1bba0 | ||
|
|
6ae8180f99 | ||
|
|
ab0f139d75 | ||
|
|
8a0b9dca5d | ||
|
|
2e59e24876 | ||
|
|
75865290bf | ||
|
|
57fe161b16 | ||
|
|
5c4dfd5de3 | ||
|
|
b0daefa2bf | ||
|
|
42a21180c7 | ||
|
|
c4205e9400 | ||
|
|
086c0638f1 | ||
|
|
9ef5840700 | ||
|
|
3c8c92ab92 | ||
|
|
7488fe55f0 | ||
|
|
fe6c726306 | ||
|
|
8c3d62c12b | ||
|
|
66794acf17 | ||
|
|
2f3f3bcd8b | ||
|
|
dcab4f1b75 | ||
|
|
66fdd79bdf | ||
|
|
0ea9f6d193 | ||
|
|
fee6a26366 | ||
|
|
1301fddbc9 | ||
|
|
5dcd596ac9 | ||
|
|
a5522faa42 | ||
|
|
3542c38472 | ||
|
|
14c03d8461 | ||
|
|
5562d8a339 | ||
|
|
a9194261c8 | ||
|
|
11a7512fea | ||
|
|
be620a80fa | ||
|
|
1d9eb08e59 | ||
|
|
05e9626e57 | ||
|
|
fe386e31ee | ||
|
|
cb1de82f0a | ||
|
|
2337843d54 | ||
|
|
70da0d38ed | ||
|
|
154a2583f6 | ||
|
|
633050739e | ||
|
|
61d24d29f1 | ||
|
|
a210f23c9f | ||
|
|
b16908842e | ||
|
|
b80cab7a66 | ||
|
|
6b8cd1f780 | ||
|
|
78c5d73900 | ||
|
|
eb3fc9412d | ||
|
|
3caf6fd67a | ||
|
|
d48cb3d9fc | ||
|
|
d3b4a7fc84 | ||
|
|
9e63777c5c | ||
|
|
cf2f3acc51 | ||
|
|
54f8bc86e3 | ||
|
|
10961010ba | ||
|
|
3c1b749395 | ||
|
|
845ee09bf2 | ||
|
|
1efec09f44 | ||
|
|
aa1cae5200 | ||
|
|
77ab48c59f | ||
|
|
5d2b33abd3 | ||
|
|
dd4f61eec3 | ||
|
|
8f7580ec30 | ||
|
|
fbf24e42d3 | ||
|
|
e764e9c502 | ||
|
|
13721f2770 | ||
|
|
960647ce9f | ||
|
|
44c797854f | ||
|
|
3ea088e411 | ||
|
|
4a18c890c3 | ||
|
|
e1c1c51704 | ||
|
|
f83f3a1850 | ||
|
|
b26d4632c9 | ||
|
|
76783133da | ||
|
|
57d09af61d | ||
|
|
86fd29309a | ||
|
|
6a43918b56 | ||
|
|
f110a0ade8 | ||
|
|
93bd883a01 | ||
|
|
61375c9e95 | ||
|
|
c2f4be5063 | ||
|
|
14bde7fc3f | ||
|
|
b5e2a94480 | ||
|
|
edcf2fd756 | ||
|
|
9228d017af | ||
|
|
1104c58611 | ||
|
|
3d7366ac1d | ||
|
|
0f19ff9a02 | ||
|
|
a21caead92 | ||
|
|
2b287c6332 | ||
|
|
8b67abd304 | ||
|
|
abae82b507 | ||
|
|
777d3aa45c | ||
|
|
ce595d0e62 | ||
|
|
0fd242eb74 | ||
|
|
d2215570da | ||
|
|
b6bef24ace | ||
|
|
bb5a2aa3fd | ||
|
|
77d1ba93c3 | ||
|
|
4aa786c595 | ||
|
|
a5ff2eceae | ||
|
|
84b281aa51 | ||
|
|
dc5c655314 | ||
|
|
2140d8821d | ||
|
|
63860e95ce | ||
|
|
1474c4c546 | ||
|
|
e2e51dc030 | ||
|
|
604298eaca | ||
|
|
f9d13c4058 | ||
|
|
e1db6807ef | ||
|
|
d8e1f82bdf | ||
|
|
c5a78e01f2 | ||
|
|
22e4b9facc | ||
|
|
1ae555eac9 | ||
|
|
a0e5f75f0b | ||
|
|
2e101d5c23 | ||
|
|
ce1848a5c3 | ||
|
|
ee515ad666 | ||
|
|
bc449a3c34 | ||
|
|
3012f64b4b | ||
|
|
e8886c9d9d | ||
|
|
a074459e03 | ||
|
|
b87e12d2cb | ||
|
|
bf2bc405d0 | ||
|
|
9fecc65680 | ||
|
|
486a0232e3 | ||
|
|
e68dc88d6c | ||
|
|
f777eaabff | ||
|
|
36080e7074 | ||
|
|
bdeb7e1381 | ||
|
|
ecf7b56acf | ||
|
|
92a2ec1fb0 | ||
|
|
892262a107 | ||
|
|
0e10a9b34b | ||
|
|
d872a57160 | ||
|
|
0d38f107bd | ||
|
|
1217e086c0 | ||
|
|
44e3d58e14 | ||
|
|
8b52cfc4d3 | ||
|
|
c93d94035a | ||
|
|
08d47dd9f1 | ||
|
|
f250efb660 | ||
|
|
c144c04aee | ||
|
|
0a52025a99 | ||
|
|
e4e02d4da2 | ||
|
|
0408a54372 | ||
|
|
134c741cf8 | ||
|
|
756e85f046 | ||
|
|
8b532aa49a | ||
|
|
cc544e4591 | ||
|
|
1bd6f71ac1 | ||
|
|
8914c7f4cc | ||
|
|
636216c5d3 | ||
|
|
a174abbc09 | ||
|
|
5134f8f85b | ||
|
|
1007dc40fb | ||
|
|
767596301a | ||
|
|
d76d13bcc2 | ||
|
|
bd495e98ee | ||
|
|
2f8ff3b517 | ||
|
|
629de04289 | ||
|
|
b4b3d0718d | ||
|
|
ed7a3ffdbc | ||
|
|
0cfebb6976 | ||
|
|
48e2c72180 | ||
|
|
3ce54cfc4a | ||
|
|
8969d011ff | ||
|
|
8fd6f2c7dc | ||
|
|
a2041bfc11 | ||
|
|
f836239ddb | ||
|
|
00129bcee0 | ||
|
|
c714abd656 | ||
|
|
e6baa0787c | ||
|
|
036e798637 | ||
|
|
db25a6c7e9 | ||
|
|
2d091895a8 | ||
|
|
5ea7c6cc0c | ||
|
|
72aa81f8dc | ||
|
|
afe386566b | ||
|
|
d5da8ba62f | ||
|
|
24a3a2de65 | ||
|
|
40f8b0e960 | ||
|
|
d1813d3dcd | ||
|
|
d5b02fbbb0 | ||
|
|
b3620a7832 | ||
|
|
5829e25fed | ||
|
|
aa041245af | ||
|
|
17aa646856 | ||
|
|
7ef5d5b034 | ||
|
|
68a46ac023 | ||
|
|
6310cc0452 | ||
|
|
9f52c61e4f | ||
|
|
6a62301c1c | ||
|
|
e61eaa8264 | ||
|
|
c906ce0d3a | ||
|
|
e9a1e3e40d | ||
|
|
f149f2c8cf | ||
|
|
3ee7c62119 | ||
|
|
a5d1cb380d | ||
|
|
e3b4e0956a | ||
|
|
eb427f3772 | ||
|
|
a53c167558 | ||
|
|
61476422bb | ||
|
|
5b6c4004c7 | ||
|
|
d7bd32aae3 | ||
|
|
c14496ade9 | ||
|
|
4b117c7882 | ||
|
|
0162a62c56 | ||
|
|
79ad701eca | ||
|
|
3150c110a1 | ||
|
|
9c3264c8a2 | ||
|
|
95a35e17dc | ||
|
|
aa7296c3cd | ||
|
|
89ae34c874 | ||
|
|
62a9cb0045 | ||
|
|
a96c8fc6ab | ||
|
|
c0ad27077f | ||
|
|
08ead35644 | ||
|
|
362bb8b3cf | ||
|
|
20eebf2f28 | ||
|
|
7e8dad41ec | ||
|
|
f6af646b80 | ||
|
|
3b25d04752 | ||
|
|
31eafb30ba | ||
|
|
1705926f52 | ||
|
|
f1128d63d7 | ||
|
|
c9866af227 | ||
|
|
a2ccda7b30 | ||
|
|
54cf4bb8fd | ||
|
|
229436cddf | ||
|
|
e94dd56fb1 | ||
|
|
3becef3468 | ||
|
|
3570ead725 | ||
|
|
17c5fd09f9 | ||
|
|
506f8f795f | ||
|
|
e73880b442 | ||
|
|
08050f458e | ||
|
|
75f19a28b7 | ||
|
|
ecc4c4c2e0 | ||
|
|
28da100ca2 | ||
|
|
0fbac0715d | ||
|
|
49f9e6e424 | ||
|
|
17eff1da7b | ||
|
|
0fcaa64a6e | ||
|
|
506d3f655c | ||
|
|
37bb54d28e | ||
|
|
1d53ef6153 | ||
|
|
59d90f5dc8 | ||
|
|
61d881b1cd | ||
|
|
77adb60167 | ||
|
|
b2b199e0e2 | ||
|
|
4c9008d141 | ||
|
|
81c44f1317 | ||
|
|
4d00fe924b | ||
|
|
4b8f8798c2 | ||
|
|
d3779adfde | ||
|
|
cf38f348b1 | ||
|
|
07730596ca | ||
|
|
233ea047e6 | ||
|
|
ba2000581a | ||
|
|
ac304ce66d | ||
|
|
be7d274479 | ||
|
|
1dd71f3aec | ||
|
|
a5730daa14 | ||
|
|
24a97445a3 | ||
|
|
d90339f5f5 | ||
|
|
92f712d670 | ||
|
|
150f28c374 | ||
|
|
fb145054a6 | ||
|
|
96050cb3f3 | ||
|
|
272870f769 | ||
|
|
f98e5994c4 | ||
|
|
57b6e57fc0 | ||
|
|
a0795b5ae0 | ||
|
|
5cd8b005b9 | ||
|
|
11bf6f2554 | ||
|
|
11b7e48080 | ||
|
|
a051d712dc | ||
|
|
8c76b5c689 | ||
|
|
b8e08d8a8f | ||
|
|
e045932e5f | ||
|
|
57d3b5a276 | ||
|
|
14a935556b | ||
|
|
731fbe2e2e | ||
|
|
ef4b7ecb5d | ||
|
|
ac8ede4b4f | ||
|
|
32822747ee | ||
|
|
523d531a38 | ||
|
|
86176ce34e | ||
|
|
8d6204a4a6 | ||
|
|
ec7069b02d | ||
|
|
78b30d8777 | ||
|
|
b01960e6a1 | ||
|
|
a7f2cec12b | ||
|
|
5083645eff | ||
|
|
e167df0082 | ||
|
|
ce9dd938b6 | ||
|
|
53abfb03e7 | ||
|
|
e8660b941f | ||
|
|
99815e8b60 | ||
|
|
3e116bf047 | ||
|
|
0e9d0ea3c2 | ||
|
|
75a4af95c5 | ||
|
|
152c2a4847 | ||
|
|
69c085f4cd | ||
|
|
769032fc92 | ||
|
|
858aff6fba | ||
|
|
e1a6293ebb | ||
|
|
3268165233 | ||
|
|
ad56b361be | ||
|
|
c73c0fe30d | ||
|
|
3a2d6db65f | ||
|
|
8edc7572e2 | ||
|
|
bc377b265a | ||
|
|
4730cf82e8 | ||
|
|
513d3fc4eb | ||
|
|
69e090e9a6 | ||
|
|
79314ead86 | ||
|
|
7fccd94d6b | ||
|
|
bed2c341b5 | ||
|
|
c405bc63ea | ||
|
|
9d334350f5 | ||
|
|
bd0c0c578c | ||
|
|
edc4afe4dd | ||
|
|
e191448bb8 | ||
|
|
ec4e9e8c60 | ||
|
|
d22dc31208 | ||
|
|
f041d35c27 | ||
|
|
5b15cef74a | ||
|
|
a4f14da17a | ||
|
|
81ce59eab7 | ||
|
|
d1bf6f9c91 | ||
|
|
5ca1e9dc1f | ||
|
|
b83f128f81 | ||
|
|
c2a20af9b8 | ||
|
|
8f2ed779ca | ||
|
|
0cedeb0809 | ||
|
|
1a51ac07a2 | ||
|
|
4b2d65c44c | ||
|
|
c44db75273 | ||
|
|
a98fd50788 | ||
|
|
fd57523b2e | ||
|
|
3cdcc1fe61 | ||
|
|
82ff0d7ddb | ||
|
|
7375c8f27b | ||
|
|
1478956e34 | ||
|
|
0cf98c9b78 | ||
|
|
f049712430 | ||
|
|
c977de2df9 | ||
|
|
4b20c5bbdd | ||
|
|
0c1fa2f030 | ||
|
|
91117cce6a | ||
|
|
e6dba8bdc2 | ||
|
|
1d67ac5f24 | ||
|
|
60d2f22c50 | ||
|
|
5dc89d7404 | ||
|
|
0f24d3a52d | ||
|
|
fc885d02dc | ||
|
|
2e09d3632e | ||
|
|
d8cb46da60 | ||
|
|
199d6e7c60 | ||
|
|
64563d58f9 | ||
|
|
1e9a0a87b6 | ||
|
|
d42d0cdc59 | ||
|
|
8fef92d94d | ||
|
|
b41eee47c9 | ||
|
|
909f3f1f47 | ||
|
|
ce269e8c8f | ||
|
|
86a4573405 | ||
|
|
be2258e409 | ||
|
|
be8cb85773 | ||
|
|
a2c003e542 | ||
|
|
f1cfe3de68 | ||
|
|
d43c17a663 | ||
|
|
c01042f1df | ||
|
|
ed2368222f | ||
|
|
103a67654c | ||
|
|
58c3720087 | ||
|
|
4e47018a81 | ||
|
|
e7d9255fe5 | ||
|
|
2c7e10ffc2 | ||
|
|
43aa5b088e | ||
|
|
86b1f5df1a | ||
|
|
5c52b6861e | ||
|
|
a358a6014f | ||
|
|
6ebc94506b | ||
|
|
59ab63807f | ||
|
|
322a79afaa | ||
|
|
c458f4942f | ||
|
|
93a4dfb4d9 | ||
|
|
f92bd9c8f9 | ||
|
|
5db95b0029 | ||
|
|
a479b7ead6 | ||
|
|
e43a49b431 | ||
|
|
4643e0b130 | ||
|
|
8c29abd0c8 | ||
|
|
d44b123815 | ||
|
|
8829f756d8 | ||
|
|
176a803f94 | ||
|
|
309a07ffa9 | ||
|
|
e3784d36f1 | ||
|
|
5048fffd04 | ||
|
|
5ca3036849 | ||
|
|
e57f44068b | ||
|
|
a4d10b6c72 | ||
|
|
5769629250 | ||
|
|
a59ff5e7e8 | ||
|
|
9a9c0583ca | ||
|
|
2f409e5168 | ||
|
|
cf35c7d611 | ||
|
|
4a2eee2a1d | ||
|
|
0ed2b10b13 | ||
|
|
01f67265f6 | ||
|
|
8a73043368 | ||
|
|
b09c36e13e | ||
|
|
14f7389900 | ||
|
|
895e867b91 | ||
|
|
6bc60bad33 | ||
|
|
5e716ece2d | ||
|
|
320f6acc21 | ||
|
|
af51373e2c | ||
|
|
5dd00e9f24 | ||
|
|
63eaa00ee1 | ||
|
|
e25610c66e | ||
|
|
5724d051b2 | ||
|
|
9a5ac5ddf7 | ||
|
|
145c18d9ed | ||
|
|
b4bb924659 | ||
|
|
45e8113553 | ||
|
|
cfb9bfdb6b | ||
|
|
6a73054a9c | ||
|
|
5d88e8d1ec | ||
|
|
19d7aa3e33 |
11
.env
11
.env
@@ -2,6 +2,7 @@ NODE_ENV='production'
|
||||
NODE_PATH=./src
|
||||
BASE_URL=''
|
||||
LMS_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
LOGIN_URL=''
|
||||
LOGOUT_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
@@ -31,7 +32,15 @@ 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=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
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={}
|
||||
|
||||
@@ -2,12 +2,12 @@ NODE_ENV='development'
|
||||
PORT=1996
|
||||
BASE_URL='localhost:1996'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
SUPPORT_URL=''
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
@@ -38,7 +38,15 @@ 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=''
|
||||
ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
13
.env.test
13
.env.test
@@ -2,12 +2,12 @@ NODE_ENV='test'
|
||||
PORT=1996
|
||||
BASE_URL='localhost:1996'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
@@ -20,7 +20,7 @@ LMS_CLIENT_ID='login-service-client-id'
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
SUPPORT_URL=''
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
OPEN_SOURCE_URL='http://localhost:18000/openedx'
|
||||
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
|
||||
@@ -37,7 +37,14 @@ 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=''
|
||||
ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
|
||||
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
|
||||
ENABLE_NOTICES=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
const config = createConfig('eslint', {
|
||||
rules: {
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'import/no-import-module-exports': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
},
|
||||
});
|
||||
|
||||
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@@ -1,8 +0,0 @@
|
||||
# Root app is developed and owned by Aurora
|
||||
* @edx/content-aurora
|
||||
|
||||
# WIDGETS and experiments are developed and owned by separate teams below
|
||||
|
||||
# Recommendations panel
|
||||
/src/widgets/RecommendationsPanel @edx/vanguards
|
||||
/src/widgets/LookingForChallengeWidget @edx/vanguards
|
||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Adding new check for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
33
.github/renovate.json
vendored
Normal file
33
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
"schedule:weekly",
|
||||
":automergeLinters",
|
||||
":automergeMinor",
|
||||
":automergeTesters",
|
||||
":enableVulnerabilityAlerts",
|
||||
":rebaseStalePrs",
|
||||
":semanticCommits",
|
||||
":updateNotScheduled"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDepTypes": [
|
||||
"devDependencies"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"lockFileMaintenance",
|
||||
"minor",
|
||||
"patch",
|
||||
"pin"
|
||||
],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -10,18 +10,16 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -31,6 +29,9 @@ jobs:
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Type check
|
||||
run: npm run types
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
@@ -39,19 +40,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
uses: dawidd6/action-send-mail@v3
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
server_address: email-smtp.us-east-1.amazonaws.com
|
||||
server_port: 465
|
||||
username: ${{ secrets.EDX_SMTP_USERNAME }}
|
||||
password: ${{ secrets.EDX_SMTP_PASSWORD }}
|
||||
subject: Upgrade python requirements workflow failed in ${{github.repository}}
|
||||
to: masters-grades@edx.org
|
||||
from: github-actions <github-actions@edx.org>
|
||||
body: Upgrade python requirements workflow in ${{github.repository}} failed!
|
||||
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
|
||||
}}"
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
32
.github/workflows/npm-publish.yml
vendored
32
.github/workflows/npm-publish.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: Release CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Create Build
|
||||
run: npm run build
|
||||
|
||||
- name: Release Package
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
|
||||
run: npm semantic-release
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
env.config.*
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
@@ -25,3 +26,4 @@ module.config.js
|
||||
### transifex ###
|
||||
src/i18n/transifex_input.json
|
||||
temp
|
||||
src/i18n/messages
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
27
.releaserc
27
.releaserc
@@ -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": []
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-learner-dashboard]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
32
Makefile
32
Makefile
@@ -2,17 +2,13 @@ npm-install-%: ## install specified % npm package
|
||||
npm install $* --save-dev
|
||||
git add package.json
|
||||
|
||||
transifex_resource = frontend-app-learner-dashboard
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
@@ -44,20 +40,18 @@ detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-app-learner-dashboard/src/i18n/messages:frontend-app-learner-dashboard
|
||||
|
||||
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-learner-dashboard
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
26
README.md
26
README.md
@@ -1,26 +0,0 @@
|
||||
# frontend-app-learner-dashboard
|
||||
|
||||
The Learner Home app is a microfrontend (MFE) course listing experience for the Open edX Learning Management System (LMS). This experience was designed to provide a clean and functional interface to allow learners to view all of their open enrollments, as well as take relevant actions on those enrollments. It also serves as host to a number of exposed "widget" containers to provide upsell and discovery widgets as sidebar/footer components.
|
||||
|
||||
## Quickstart
|
||||
|
||||
To start the MFE and enable the feature in LMS:
|
||||
|
||||
1. Start the MFE with `npm run start`. Take a note of the path/port (defaults to `http://localhost:1996`).
|
||||
|
||||
From there, simply load the configured address/port. You should be prompted to log into your LMS if you are not already, and then redirected to your home page.
|
||||
|
||||
## Contributing
|
||||
|
||||
A core goal of this app is to provide a clean experimentation interface. To promote this end, we have provided a silo'ed code directory at `src/widgets` in which contributors should add their custom widget components. In order to ensure our ability to maintain the code stability of the app, the code for these widgets should be strictly contained within the bounds of that directory.
|
||||
|
||||
Once written, the widgets can be configured into one of our widget containers at `src/containers/WidgetContainers`. This can include conditional logic, as well as Optimizely triggers. It is important to note that our integration tests will isolate and ignore these containers, and thus testing your widget is the response of the creator/maintainer of the widget itself.
|
||||
|
||||
Some guidelines for writing widgets:
|
||||
* Code for the widget should be strictly confined to the `src/widgets` directory.
|
||||
* You can load data from the redux store, but should not add or modify fields in that structure.
|
||||
* Network events should be managed in component hooks, though can use our `data/constants/requests:requestStates` for ease of tracking the request states.
|
||||
|
||||
## Resources
|
||||
|
||||
* [Learner Home project info at the Open edX Wiki](https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3575906333/Learner+Home)
|
||||
97
README.rst
Normal file
97
README.rst
Normal file
@@ -0,0 +1,97 @@
|
||||
|license-badge| |status-badge| |ci-badge| |codecov-badge|
|
||||
|
||||
.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-app-learner-dashboard.svg
|
||||
:target: https://github.com/openedx/frontend-app-learner-dashboard/blob/master/LICENSE
|
||||
:alt: License
|
||||
.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
|
||||
:alt: Maintained
|
||||
.. |ci-badge| image:: https://github.com/openedx/frontend-app-learner-dashboard/actions/workflows/ci.yml/badge.svg
|
||||
:target: https://github.com/openedx/frontend-app-learner-dashboard/actions/workflows/ci.yml
|
||||
:alt: Continuous Integration
|
||||
.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-app-learner-dashboard/coverage.svg?branch=master
|
||||
:target: https://app.codecov.io/github/openedx/frontend-app-learner-dashboard?branch=master
|
||||
:alt: Codecov
|
||||
|
||||
frontend-app-learner-dashboard
|
||||
==============================
|
||||
|
||||
The Learner Home app is a microfrontend (MFE) course listing experience for the Open edX Learning Management System
|
||||
(LMS). This experience was designed to provide a clean and functional interface to allow learners to view all of their
|
||||
open enrollments, as well as take relevant actions on those enrollments. It also serves as host to a number of exposed
|
||||
"widget" containers to provide upsell and discovery widgets as sidebar/footer components.
|
||||
|
||||
Quickstart
|
||||
----------
|
||||
|
||||
To start the MFE and enable the feature in LMS:
|
||||
|
||||
1. Start the MFE with ``npm run start``. Take a note of the path/port (defaults to ``http://localhost:1996``).
|
||||
|
||||
From there, simply load the configured address/port. You should be prompted to log into your LMS if you are not
|
||||
already, and then redirected to your home page.
|
||||
|
||||
Plugins
|
||||
-------
|
||||
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||
|
||||
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Contributions are very welcome. Please read `So you want to contribute to Open edX <https://docs.openedx.org/en/latest/developers/quickstarts/so_you_want_to_contribute.html>`_ for details on how to get started as an Open edX contributor.
|
||||
|
||||
This project is currently accepting all types of contributions — bug fixes, security fixes, maintenance work, or new features.
|
||||
However, if you intend to add a new feature, make sure it has gone through the `Product Review process <https://openedx.atlassian.net/wiki/spaces/COMM/pages/3875962884/How+to+submit+an+open+source+contribution+for+Product+Review>`_.
|
||||
|
||||
When proposing a change, create an issue in this repo to get the discussion started.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
The code in this repository is licensed under the AGPLv3 unless otherwise noted.
|
||||
|
||||
Please see the `license`_ for more info.
|
||||
|
||||
.. _license: https://github.com/openedx/frontend-app-learner-dashboard/blob/master/LICENSE
|
||||
|
||||
Getting Help
|
||||
------------
|
||||
|
||||
If you're having trouble, we have discussion forums at https://discuss.openedx.org where you can connect with others in
|
||||
the community.
|
||||
|
||||
Our real-time conversations are on Slack. You can request a `Slack invitation`_, then join our
|
||||
`community Slack workspace`_. Because this is a frontend repository, the best place to discuss it would be in the
|
||||
`#wg-frontend channel`_.
|
||||
|
||||
For anything non-trivial, the best path is to open an issue in this repository with as many details about the issue you
|
||||
are facing as you can provide.
|
||||
|
||||
https://github.com/openedx/frontend-app-learner-dashboard/issues
|
||||
|
||||
For more information about these options, see the `Getting Help`_ page.
|
||||
|
||||
.. _Slack invitation: https://openedx.org/slack
|
||||
.. _community Slack workspace: https://openedx.slack.com/
|
||||
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
||||
.. _Getting Help: https://openedx.org/community/connect
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
Additional info about the Learner Home MFE project can be found on the `Open edX Wiki`_.
|
||||
|
||||
.. _Open edX Wiki: https://openedx.atlassian.net/wiki/spaces/OEPM/pages/3575906333/Learner+Home
|
||||
|
||||
The Open edX Code of Conduct
|
||||
----------------------------
|
||||
|
||||
All community members are expected to follow the `Open edX Code of Conduct`_.
|
||||
|
||||
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
|
||||
|
||||
Reporting Security Issues
|
||||
-------------------------
|
||||
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
25
catalog-info.yaml
Normal file
25
catalog-info.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
# This file records information about this repo. Its use is described in OEP-55:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: 'frontend-app-learner-dashboard'
|
||||
description: 'The Microfrontend for the course listing interface allowing learners to view and act upon enrollments.'
|
||||
links:
|
||||
- url: 'https://github.com/openedx/frontend-app-learner-dashboard/blob/master/README.rst'
|
||||
title: 'Documentation'
|
||||
icon: 'Article'
|
||||
annotations:
|
||||
# (Optional) Annotation keys and values can be whatever you want.
|
||||
# We use it in Open edX repos to have a comma-separated list of GitHub user
|
||||
# names that might be interested in changes to the architecture of this
|
||||
# component.
|
||||
openedx.org/arch-interest-groups: ""
|
||||
# This can be multiple comma-separated projects.
|
||||
openedx.org/add-to-projects: "openedx:23"
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
type: 'service'
|
||||
lifecycle: 'production'
|
||||
owner: 2U-aperture
|
||||
# (Optional) An array of different components or resources.
|
||||
73
example.env.config.js
Normal file
73
example.env.config.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
Learner Dashboard is now able to handle JS-based configuration!
|
||||
|
||||
For the time being, the `.env.*` files are still made available when cloning down this repo or pulling from
|
||||
the master branch. To switch to using `env.config.js`, make a copy of `example.env.config.js` and configure as needed.
|
||||
|
||||
For testing with Jest Snapshot, there is a mock in `/src/setupTest.jsx` for `getConfig` that will need to be
|
||||
uncommented.
|
||||
|
||||
Note: having both .env and env.config.js files will follow a predictable order, in which non-empty values in the
|
||||
JS-based config will overwrite the .env environment variables.
|
||||
|
||||
frontend-platform's getConfig loads configuration in the following sequence:
|
||||
- .env file config
|
||||
- optional handlers (commonly used to merge MFE-specific config in via additional process.env variables)
|
||||
- env.config.js file config
|
||||
- runtime config
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
NODE_ENV: 'development',
|
||||
NODE_PATH: './src',
|
||||
PORT: 1996,
|
||||
BASE_URL: 'localhost:1996',
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
ECOMMERCE_BASE_URL: 'http://localhost:18130',
|
||||
CREDIT_PURCHASE_URL: 'http://localhost:8140',
|
||||
LOGIN_URL: 'http://localhost:18000/login',
|
||||
LOGOUT_URL: 'http://localhost:18000/logout',
|
||||
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
|
||||
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
|
||||
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
|
||||
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
|
||||
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
|
||||
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
|
||||
USER_INFO_COOKIE_NAME: 'edx-user-info',
|
||||
SITE_NAME: 'localhost',
|
||||
DATA_API_BASE_URL: 'http://localhost:8000',
|
||||
// LMS_CLIENT_ID should match the lms DOT client application in your LMS container
|
||||
LMS_CLIENT_ID: 'login-service-client-id',
|
||||
SEGMENT_KEY: '',
|
||||
FEATURE_FLAGS: {},
|
||||
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
|
||||
SUPPORT_URL: 'http://localhost:18000/support',
|
||||
CONTACT_URL: 'http://localhost:18000/contact',
|
||||
OPEN_SOURCE_URL: 'http://localhost:18000/openedx',
|
||||
TERMS_OF_SERVICE_URL: 'http://localhost:18000/terms-of-service',
|
||||
PRIVACY_POLICY_URL: 'http://localhost:18000/privacy-policy',
|
||||
FACEBOOK_URL: 'https://www.facebook.com',
|
||||
TWITTER_URL: 'https://twitter.com',
|
||||
YOU_TUBE_URL: 'https://www.youtube.com',
|
||||
LINKED_IN_URL: 'https://www.linkedin.com',
|
||||
REDDIT_URL: 'https://www.reddit.com',
|
||||
APPLE_APP_STORE_URL: 'https://www.apple.com/ios/app-store/',
|
||||
GOOGLE_PLAY_URL: 'https://play.google.com/store',
|
||||
ENTERPRISE_MARKETING_URL: 'http://example.com',
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE: 'example.com',
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN: 'example.com Referral',
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
|
||||
LEARNING_BASE_URL: 'http://localhost:2000',
|
||||
SESSION_COOKIE_DOMAIN: 'localhost',
|
||||
HOTJAR_APP_ID: '',
|
||||
HOTJAR_VERSION: 6,
|
||||
HOTJAR_DEBUG: '',
|
||||
NEW_RELIC_APP_ID: '',
|
||||
NEW_RELIC_LICENSE_KEY: '',
|
||||
ACCOUNT_SETTINGS_URL: 'http://localhost:1997',
|
||||
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
|
||||
ENABLE_NOTICES: '',
|
||||
CAREER_LINK_URL: '',
|
||||
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFilesAfterEnv: [
|
||||
@@ -6,9 +6,6 @@ module.exports = createConfig('jest', {
|
||||
'<rootDir>/src/setupTest.jsx',
|
||||
],
|
||||
modulePaths: ['<rootDir>/src/'],
|
||||
snapshotSerializers: [
|
||||
'enzyme-to-json/serializer',
|
||||
],
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
|
||||
@@ -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}
|
||||
62238
package-lock.json
generated
62238
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
94
package.json
94
package.json
@@ -6,17 +6,21 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"semantic-release": "semantic-release",
|
||||
"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",
|
||||
"prepare": "husky install"
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -25,70 +29,58 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
|
||||
"@edx/browserslist-config": "^1.1.0",
|
||||
"@edx/frontend-component-footer": "^11.4.1",
|
||||
"@edx/frontend-enterprise-hotjar": "^1.2.0",
|
||||
"@edx/frontend-platform": "^2.6.2",
|
||||
"@edx/paragon": "20.19.0",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.6.0",
|
||||
"@edx/frontend-enterprise-hotjar": "7.2.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.15",
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.21.4",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^23.4.5",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@reduxjs/toolkit": "^2.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.16.2",
|
||||
"dompurify": "^2.3.1",
|
||||
"email-prop-type": "^3.0.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "^8.0.6",
|
||||
"core-js": "3.46.0",
|
||||
"filesize": "^10.0.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "5.0.1",
|
||||
"html-react-parser": "^1.3.0",
|
||||
"jest": "^26.6.3",
|
||||
"history": "5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "7.0.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^5.20.9",
|
||||
"react-pdf": "^5.5.0",
|
||||
"react-intl": "6.8.9",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "5.3.3",
|
||||
"react-router-dom": "6.30.1",
|
||||
"react-share": "^4.4.0",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.1.1",
|
||||
"redux-beacon": "^2.1.0",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux": "4.2.1",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"reselect": "^4.0.0",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"util": "^0.12.4",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
"util": "^0.12.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "11.0.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"enzyme-adapter-react-16": "^1.15.6",
|
||||
"fetch-mock": "^9.11.0",
|
||||
"husky": "^7.0.0",
|
||||
"@edx/browserslist-config": "^1.3.0",
|
||||
"@edx/typescript-config": "^1.1.0",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest-expect-message": "^1.0.2",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
"react-test-renderer": "^16.14.0",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"semantic-release": "^17.4.5"
|
||||
"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": "^18.3.1",
|
||||
"redux-mock-store": "^1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us" dir="ltr">
|
||||
<head>
|
||||
<title>Learner Dashboard | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
52
src/App.jsx
52
src/App.jsx
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -7,8 +6,8 @@ import { logError } from '@edx/frontend-platform/logging';
|
||||
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
||||
|
||||
import { ErrorPage, AppContext } from '@edx/frontend-platform/react';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
import store from 'data/store';
|
||||
@@ -18,15 +17,16 @@ import {
|
||||
} from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import ZendeskFab from 'components/ZendeskFab';
|
||||
|
||||
import track from 'tracking';
|
||||
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
import LearnerDashboardHeaderVariant from './containers/LearnerDashboardHeaderVariant';
|
||||
|
||||
import AppWrapper from 'containers/AppWrapper';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
import './App.scss';
|
||||
|
||||
export const App = () => {
|
||||
@@ -41,7 +41,7 @@ export const App = () => {
|
||||
const loadData = reduxHooks.useLoadData();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') {
|
||||
if (authenticatedUser?.administrator || getConfig().NODE_ENV === 'development') {
|
||||
window.loadEmptyData = () => {
|
||||
loadData({ ...fakeData.globalData, courses: [] });
|
||||
};
|
||||
@@ -59,12 +59,12 @@ export const App = () => {
|
||||
window.actions = actions;
|
||||
window.track = track;
|
||||
}
|
||||
if (process.env.HOTJAR_APP_ID) {
|
||||
if (getConfig().HOTJAR_APP_ID) {
|
||||
try {
|
||||
initializeHotjar({
|
||||
hotjarId: process.env.HOTJAR_APP_ID,
|
||||
hotjarVersion: process.env.HOTJAR_VERSION,
|
||||
hotjarDebug: !!process.env.HOTJAR_DEBUG,
|
||||
hotjarId: getConfig().HOTJAR_APP_ID,
|
||||
hotjarVersion: getConfig().HOTJAR_VERSION,
|
||||
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
@@ -72,24 +72,28 @@ export const App = () => {
|
||||
}
|
||||
}, [authenticatedUser, loadData]);
|
||||
return (
|
||||
<Router>
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages.pageTitle)}</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
) : (<Dashboard />)}
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
<ZendeskFab />
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main id="main">
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
) : (
|
||||
<Dashboard />
|
||||
)}
|
||||
</main>
|
||||
</AppWrapper>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
</Router>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// frontend-app-*/src/index.scss
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/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";
|
||||
|
||||
.text-ellipsis {
|
||||
|
||||
130
src/App.test.jsx
130
src/App.test.jsx
@@ -1,26 +1,19 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { 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 LearnerDashboardHeaderVariant from 'containers/LearnerDashboardHeaderVariant';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeaderVariant', () => 'LearnerDashboardHeaderVariant');
|
||||
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
|
||||
jest.mock('@edx/frontend-component-footer', () => ({
|
||||
FooterSlot: jest.fn(() => <div>FooterSlot</div>),
|
||||
}));
|
||||
jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
|
||||
jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => <div>LearnerDashboardHeader</div>));
|
||||
jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: 'redux.selectors',
|
||||
actions: 'redux.actions',
|
||||
@@ -35,76 +28,107 @@ jest.mock('hooks', () => ({
|
||||
}));
|
||||
jest.mock('data/store', () => 'data/store');
|
||||
|
||||
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);
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
|
||||
const supportEmail = 'test-support-url';
|
||||
const supportEmail = 'test@support.com';
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValue({ supportEmail });
|
||||
|
||||
describe('App router component', () => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
const { formatMessage } = useIntl();
|
||||
describe('component', () => {
|
||||
const runBasicTests = () => {
|
||||
test('snapshot', () => { expect(el).toMatchSnapshot(); });
|
||||
it('displays title in helmet component', () => {
|
||||
expect(el.find(Helmet).find('title').text()).toEqual(useIntl().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.find(LearnerDashboardHeaderVariant).length).toEqual(1);
|
||||
const learnerDashboardHeader = screen.getByText('LearnerDashboardHeader');
|
||||
expect(learnerDashboardHeader).toBeInTheDocument();
|
||||
});
|
||||
it('wraps the page in a browser router', () => {
|
||||
expect(el.find(Router)).toMatchObject(el);
|
||||
it('wraps the header and main components in an AppWrapper widget container', () => {
|
||||
const appWrapper = screen.getByText('LearnerDashboardHeader').parentElement;
|
||||
expect(appWrapper).toHaveClass('AppWrapper');
|
||||
expect(appWrapper.children[1].id).toEqual('main');
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(el.find(Footer).props().logo).toEqual(logo);
|
||||
it('displays footer slot', () => {
|
||||
const footerSlot = screen.getByText('FooterSlot');
|
||||
expect(footerSlot).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
describe('no network failure', () => {
|
||||
beforeAll(() => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
el = shallow(<App />);
|
||||
getConfig.mockReturnValue({});
|
||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
expect(el.find('main')).toMatchObject(shallow(
|
||||
<main><Dashboard /></main>,
|
||||
));
|
||||
const dashboard = screen.getByText('Dashboard');
|
||||
expect(dashboard).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('no network failure with optimizely url', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ OPTIMIZELY_URL: 'fake.url' });
|
||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
const dashboard = screen.getByText('Dashboard');
|
||||
expect(dashboard).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('no network failure with optimizely project id', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
reduxHooks.useRequestIsFailed.mockReturnValue(false);
|
||||
getConfig.mockReturnValue({ OPTIMIZELY_PROJECT_ID: 'fakeId' });
|
||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
const dashboard = screen.getByText('Dashboard');
|
||||
expect(dashboard).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('initialize failure', () => {
|
||||
beforeAll(() => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
reduxHooks.useRequestIsFailed.mockImplementation((key) => key === RequestKeys.initialize);
|
||||
el = shallow(<App />);
|
||||
getConfig.mockReturnValue({});
|
||||
render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads error page', () => {
|
||||
expect(el.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
</main>,
|
||||
));
|
||||
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);
|
||||
el = shallow(<App />);
|
||||
getConfig.mockReturnValue({});
|
||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads error page', () => {
|
||||
expect(el.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Alert variant="danger">
|
||||
<ErrorPage message={formatMessage(messages.errorMessage, { supportEmail })} />
|
||||
</Alert>
|
||||
</main>,
|
||||
));
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
const errorPage = screen.getByText('ErrorPage');
|
||||
expect(errorPage).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component component initialize failure snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
<ErrorPage
|
||||
message="If you experience repeated failures, please email support at test-support-url"
|
||||
/>
|
||||
</Alert>
|
||||
</main>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`App router component component no network failure snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
<Dashboard />
|
||||
</main>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`App router component component refresh failure snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<title>
|
||||
Learner Home
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
<ErrorPage
|
||||
message="If you experience repeated failures, please email support at test-support-url"
|
||||
/>
|
||||
</Alert>
|
||||
</main>
|
||||
<Footer
|
||||
logo="fakeLogo.png"
|
||||
/>
|
||||
<ZendeskFab />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
@@ -1,36 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
>
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
</IntlProvider>
|
||||
`;
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
>
|
||||
<AppProvider
|
||||
store={
|
||||
Object {
|
||||
"redux": "store",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Switch>
|
||||
<PageRoute
|
||||
path="/"
|
||||
>
|
||||
<App />
|
||||
</PageRoute>
|
||||
<Redirect
|
||||
to="/"
|
||||
/>
|
||||
</Switch>
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
`;
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
|
||||
export const Banner = ({
|
||||
children, variant, icon, className,
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Alert } from '@edx/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).toMatchSnapshot();
|
||||
});
|
||||
test('renders with variants', () => {
|
||||
const wrapper = shallow(<Banner {...props} variant="success" />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
describe('Banner component', () => {
|
||||
it('renders children content', () => {
|
||||
render(<Banner>Test content</Banner>);
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(wrapper.find(Alert).prop('variant')).toEqual('success');
|
||||
});
|
||||
test('renders with custom class', () => {
|
||||
const wrapper = shallow(<Banner {...props} className="custom-class" />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MailtoLink } from '@edx/paragon';
|
||||
|
||||
export const EmailLink = ({ address }) => {
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MailtoLink to={address}>{address}</MailtoLink>
|
||||
);
|
||||
};
|
||||
EmailLink.defaultProps = { address: null };
|
||||
EmailLink.propTypes = { address: PropTypes.string };
|
||||
|
||||
export default EmailLink;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import EmailLink from './EmailLink';
|
||||
|
||||
describe('EmailLink', () => {
|
||||
it('renders null when no address is provided', () => {
|
||||
const wrapper = shallow(<EmailLink />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
it('renders a MailtoLink when an address is provided', () => {
|
||||
const wrapper = shallow(<EmailLink address="test@email.com" />);
|
||||
expect(wrapper.find('MailtoLink').length).toEqual(1);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
25
src/components/NoticesWrapper/api.js
Normal file
25
src/components/NoticesWrapper/api.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
export const noticesUrl = `${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`;
|
||||
|
||||
export const 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}. ${notFoundMessage}`);
|
||||
} else {
|
||||
logError(e);
|
||||
}
|
||||
};
|
||||
if (authenticatedUser) {
|
||||
return getAuthenticatedHttpClient().get(noticesUrl, {}).then(onLoad).catch(handleError);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default { getNotices };
|
||||
65
src/components/NoticesWrapper/api.test.js
Normal file
65
src/components/NoticesWrapper/api.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
||||
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({
|
||||
LMS_BASE_URL: 'test-lms-url',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
getAuthenticatedUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
logError: jest.fn(),
|
||||
logInfo: jest.fn(),
|
||||
}));
|
||||
|
||||
const testData = 'test-data';
|
||||
const successfulGet = () => Promise.resolve(testData);
|
||||
const error404 = { customAttributes: { httpErrorStatus: 404 }, test: 'error' };
|
||||
const error404Get = () => Promise.reject(error404);
|
||||
const error500 = { customAttributes: { httpErrorStatus: 500 }, test: 'error' };
|
||||
const error500Get = () => Promise.reject(error500);
|
||||
|
||||
const get = jest.fn().mockImplementation(successfulGet);
|
||||
getAuthenticatedHttpClient.mockReturnValue({ get });
|
||||
const authenticatedUser = { fake: 'user' };
|
||||
getAuthenticatedUser.mockReturnValue(authenticatedUser);
|
||||
|
||||
const onLoad = jest.fn();
|
||||
describe('getNotices api method', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('not authenticated', () => {
|
||||
it('does not fetch anything', () => {
|
||||
getAuthenticatedUser.mockReturnValueOnce(null);
|
||||
api.getNotices({ onLoad });
|
||||
expect(get).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('authenticated', () => {
|
||||
it('fetches noticesUrl with onLoad behavior', async () => {
|
||||
await api.getNotices({ onLoad });
|
||||
expect(get).toHaveBeenCalledWith(api.noticesUrl, {});
|
||||
expect(onLoad).toHaveBeenCalledWith(testData);
|
||||
});
|
||||
it('calls logInfo if fetch fails with 404', async () => {
|
||||
get.mockImplementation(error404Get);
|
||||
await api.getNotices({ onLoad });
|
||||
expect(logInfo).toHaveBeenCalledWith(`${error404}. ${api.error404Message}`);
|
||||
});
|
||||
it('calls logError if fetch fails with non-404 error', async () => {
|
||||
get.mockImplementation(error500Get);
|
||||
await api.getNotices({ onLoad });
|
||||
expect(logError).toHaveBeenCalledWith(error500);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
40
src/components/NoticesWrapper/hooks.js
Normal file
40
src/components/NoticesWrapper/hooks.js
Normal file
@@ -0,0 +1,40 @@
|
||||
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.
|
||||
* If the user has an unacknowledged notice, they will be rerouted off
|
||||
* course home and onto a full-screen notice page. If the plugin is not
|
||||
* installed, or there are no notices, we just passthrough this component.
|
||||
*/
|
||||
export const state = StrictDict({
|
||||
isRedirected: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useNoticesWrapperData = () => {
|
||||
const [isRedirected, setIsRedirected] = module.state.isRedirected();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (getConfig().ENABLE_NOTICES) {
|
||||
getNotices({
|
||||
onLoad: (data) => {
|
||||
if (data?.data?.results?.length > 0) {
|
||||
setIsRedirected(true);
|
||||
window.location.replace(`${data.data.results[0]}?next=${window.location.href}`);
|
||||
}
|
||||
},
|
||||
notFoundMessage: formatMessage(messages.error404Message),
|
||||
});
|
||||
}
|
||||
}, [setIsRedirected, formatMessage]);
|
||||
return { isRedirected };
|
||||
};
|
||||
|
||||
export default useNoticesWrapperData;
|
||||
99
src/components/NoticesWrapper/hooks.test.js
Normal file
99
src/components/NoticesWrapper/hooks.test.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MockUseState, formatMessage } from 'testUtils';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getNotices } from './api';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({ getConfig: jest.fn() }));
|
||||
jest.mock('./api', () => ({ getNotices: jest.fn() }));
|
||||
|
||||
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);
|
||||
|
||||
let hook;
|
||||
describe('NoticesWrapper hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.isRedirected);
|
||||
});
|
||||
describe('useNoticesWrapperData', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes state hooks', () => {
|
||||
hooks.useNoticesWrapperData();
|
||||
expect(hooks.state.isRedirected).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('effects', () => {
|
||||
it('does not call notices if not enabled', () => {
|
||||
getConfig.mockReturnValueOnce({ ENABLE_NOTICES: false });
|
||||
hooks.useNoticesWrapperData();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
|
||||
cb();
|
||||
expect(getNotices).not.toHaveBeenCalled();
|
||||
});
|
||||
describe('getNotices call (if enabled) onLoad behavior', () => {
|
||||
it('does not redirect if there are no results', () => {
|
||||
hooks.useNoticesWrapperData();
|
||||
expect(React.useEffect).toHaveBeenCalled();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
|
||||
cb();
|
||||
expect(getNotices).toHaveBeenCalled();
|
||||
const { onLoad } = getNotices.mock.calls[0][0];
|
||||
onLoad({});
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
onLoad({ data: {} });
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
onLoad({ data: { results: [] } });
|
||||
expect(state.setState.isRedirected).not.toHaveBeenCalled();
|
||||
});
|
||||
it('redirects and set isRedirected if results are returned', () => {
|
||||
delete window.location;
|
||||
window.location = { replace: jest.fn(), href: 'test-old-href' };
|
||||
hooks.useNoticesWrapperData();
|
||||
const [cb, prereqs] = React.useEffect.mock.calls[0];
|
||||
expect(prereqs).toEqual([state.setState.isRedirected, formatMessage]);
|
||||
cb();
|
||||
expect(getNotices).toHaveBeenCalled();
|
||||
const { onLoad } = getNotices.mock.calls[0][0];
|
||||
const target = 'url-target';
|
||||
onLoad({ data: { results: [target] } });
|
||||
expect(state.setState.isRedirected).toHaveBeenCalledWith(true);
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`${target}?next=${window.location.href}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('forwards isRedirected from state call', () => {
|
||||
hook = hooks.useNoticesWrapperData();
|
||||
expect(hook.isRedirected).toEqual(state.stateVals.isRedirected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
25
src/components/NoticesWrapper/index.jsx
Normal file
25
src/components/NoticesWrapper/index.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
|
||||
/**
|
||||
* This component uses the platform-plugin-notices plugin to function.
|
||||
* If the user has an unacknowledged notice, they will be rerouted off
|
||||
* course home and onto a full-screen notice page. If the plugin is not
|
||||
* installed, or there are no notices, we just passthrough this component.
|
||||
*/
|
||||
const NoticesWrapper = ({ children }) => {
|
||||
const { isRedirected } = useNoticesWrapperData();
|
||||
return (
|
||||
<div>
|
||||
{isRedirected === true ? null : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NoticesWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default NoticesWrapper;
|
||||
36
src/components/NoticesWrapper/index.test.jsx
Normal file
36
src/components/NoticesWrapper/index.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import useNoticesWrapperData from './hooks';
|
||||
import NoticesWrapper from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = { isRedirected: false };
|
||||
|
||||
const children = [<b key={1}>some</b>, <i key={2}>children</i>];
|
||||
describe('NoticesWrapper component', () => {
|
||||
beforeEach(() => {
|
||||
useNoticesWrapperData.mockClear();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes hooks', () => {
|
||||
useNoticesWrapperData.mockReturnValue(hookProps);
|
||||
render(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(useNoticesWrapperData).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('does not show children if redirected', () => {
|
||||
useNoticesWrapperData.mockReturnValueOnce({ isRedirected: true });
|
||||
render(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(screen.queryByText('some')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('children')).not.toBeInTheDocument();
|
||||
});
|
||||
it('shows children if not redirected', () => {
|
||||
useNoticesWrapperData.mockReturnValue(hookProps);
|
||||
render(<NoticesWrapper>{children}</NoticesWrapper>);
|
||||
expect(screen.getByText('some')).toBeInTheDocument();
|
||||
expect(screen.getByText('children')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
11
src/components/NoticesWrapper/messages.js
Normal file
11
src/components/NoticesWrapper/messages.js
Normal 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;
|
||||
@@ -1,54 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ZendeskFab snapshot 1`] = `
|
||||
<Zendesk
|
||||
cookies={true}
|
||||
defer={true}
|
||||
webWidget={
|
||||
Object {
|
||||
"answerBot": Object {
|
||||
"avatar": Object {
|
||||
"name": Object {
|
||||
"*": "edX Support",
|
||||
},
|
||||
"url": "https://edx-cdn.org/v3/prod/favicon.ico",
|
||||
},
|
||||
"contactOnlyAfterQuery": true,
|
||||
"suppress": false,
|
||||
"title": Object {
|
||||
"*": "edX Support",
|
||||
},
|
||||
},
|
||||
"chat": Object {
|
||||
"suppress": false,
|
||||
},
|
||||
"contactForm": Object {
|
||||
"attachments": true,
|
||||
"selectTicketForm": Object {
|
||||
"*": "Please choose your request type:",
|
||||
},
|
||||
"ticketForms": Array [
|
||||
Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"id": "description",
|
||||
"prefill": Object {
|
||||
"*": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": 360003368814,
|
||||
"subject": false,
|
||||
},
|
||||
],
|
||||
},
|
||||
"contactOptions": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"helpCenter": Object {
|
||||
"originalArticleButton": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -1,53 +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,
|
||||
},
|
||||
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;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import ZendeskFab from '.';
|
||||
|
||||
jest.mock('react-zendesk', () => 'Zendesk');
|
||||
|
||||
describe('ZendeskFab', () => {
|
||||
test('snapshot', () => {
|
||||
const wrapper = shallow(<ZendeskFab />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export const messages = StrictDict({
|
||||
supportTitle: {
|
||||
id: 'zendesk.supportTitle',
|
||||
description: 'Title for the support button',
|
||||
defaultMessage: 'edX Support',
|
||||
},
|
||||
selectTicketForm: {
|
||||
id: 'zendesk.selectTicketForm',
|
||||
description: 'Select ticket form',
|
||||
defaultMessage: 'Please choose your request type:',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -1,11 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmailLink renders a MailtoLink when an address is provided 1`] = `
|
||||
<MailtoLink
|
||||
to="test@email.com"
|
||||
>
|
||||
test@email.com
|
||||
</MailtoLink>
|
||||
`;
|
||||
|
||||
exports[`EmailLink renders null when no address is provided 1`] = `""`;
|
||||
@@ -1,6 +1,8 @@
|
||||
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,
|
||||
@@ -11,8 +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 = {};
|
||||
|
||||
13
src/containers/AppWrapper/index.jsx
Normal file
13
src/containers/AppWrapper/index.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const AppWrapper = ({
|
||||
children,
|
||||
}) => children;
|
||||
AppWrapper.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
export default AppWrapper;
|
||||
@@ -1,10 +1,8 @@
|
||||
@import "@edx/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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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"
|
||||
/>
|
||||
`;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||
|
||||
export const useIsCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
import useIsCollapsed from './hooks';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ActionButton from '.';
|
||||
|
||||
import useIsCollapsed from './hooks';
|
||||
@@ -8,18 +7,22 @@ jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
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).toMatchSnapshot();
|
||||
});
|
||||
test('is not collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
const wrapper = shallow(<ActionButton {...props} />);
|
||||
expect(wrapper).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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,22 +5,24 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const BeginCourseButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||
const { disableBeginCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
homeUrl + execEdTrackingParam,
|
||||
);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={isMasquerading || !hasAccess}
|
||||
disabled={disableBeginCourse}
|
||||
as="a"
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
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';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
@@ -13,58 +14,75 @@ jest.mock('tracking', () => ({
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'home-url' })),
|
||||
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
||||
|
||||
let wrapper;
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData();
|
||||
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, 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('snapshot', () => {
|
||||
test('renders default button when learner has access to the course', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
describe('initiliaze hooks', () => {
|
||||
it('initializes course run data with cardId', () => {
|
||||
renderComponent();
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads exec education path param', () => {
|
||||
renderComponent();
|
||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads disabled states for begin action from action hooks', () => {
|
||||
renderComponent();
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data with cardId', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('initializes enrollment data with cardId', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('learner does not have access', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
describe('disabled', () => {
|
||||
it('should be disabled', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableBeginCourse: true });
|
||||
renderComponent();
|
||||
const button = screen.getByRole('button', { name: 'Begin Course' });
|
||||
expect(button).toHaveClass('disabled');
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
test('masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
describe('enabled', () => {
|
||||
it('should be enabled', () => {
|
||||
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', 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,22 +5,24 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const ResumeButton = ({ cardId }) => {
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess, isAudit, isAuditAccessExpired } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const execEdTrackingParam = reduxHooks.useCardExecEdTrackingParam(cardId);
|
||||
const { disableResumeCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
resumeUrl,
|
||||
resumeUrl + execEdTrackingParam,
|
||||
);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired)}
|
||||
disabled={disableResumeCourse}
|
||||
as="a"
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -1,82 +1,83 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ResumeButton from './ResumeButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ resumeUrl: 'resumeUrl' })),
|
||||
useCardEnrollmentData: jest.fn(() => ({
|
||||
hasAccess: true,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: false,
|
||||
})),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useTrackCourseEvent: (eventName, cardId, url) => jest
|
||||
.fn()
|
||||
.mockName(`useTrackCourseEvent('${eventName}', '${cardId}', '${url}')`),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardExecEdTrackingParam: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: 'enterCourseClicked',
|
||||
},
|
||||
}));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
||||
|
||||
const { resumeUrl } = reduxHooks.useCardCourseRunData();
|
||||
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, url) => ({ trackCourseEvent: { eventName, cardId, url } }),
|
||||
);
|
||||
|
||||
describe('ResumeButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('renders default button when learner has access to the course', () => {
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick).getMockName()).toContain(
|
||||
'useTrackCourseEvent',
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
resumeUrl,
|
||||
);
|
||||
describe('initialize hooks', () => {
|
||||
beforeEach(() => render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>));
|
||||
it('initializes course run data with cardId', () => {
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads exec education path param', () => {
|
||||
expect(reduxHooks.useCardExecEdTrackingParam).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('loads disabled states for resume action from action hooks', () => {
|
||||
expect(useActionDisabledState).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes course run data based on cardId', () => {
|
||||
shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
it('initializes course enrollment data based on cardId', () => {
|
||||
shallow(<ResumeButton {...props} />);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
describe('disabled', () => {
|
||||
beforeEach(() => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
|
||||
});
|
||||
test('learner does not have access', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
hasAccess: false,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: false,
|
||||
});
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
it('should be disabled', () => {
|
||||
render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: 'Resume' });
|
||||
expect(button).toHaveClass('disabled');
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
test('audit access expired', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
hasAccess: true,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: true,
|
||||
});
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
describe('enabled', () => {
|
||||
it('should be enabled', () => {
|
||||
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', 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,18 +4,17 @@ import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const SelectSessionButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { hasAccess } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { canChange, hasSessions } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const { disableSelectSession } = useActionDisabledState(cardId);
|
||||
const openSessionModal = reduxHooks.useUpdateSelectSessionModalCallback(cardId);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={isMasquerading || !hasAccess || (!canChange || !hasSessions)}
|
||||
disabled={disableSelectSession}
|
||||
onClick={openSessionModal}
|
||||
>
|
||||
{formatMessage(messages.selectSession)}
|
||||
|
||||
@@ -1,67 +1,43 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
|
||||
useCardEntitlementData: jest.fn(() => ({ canChange: true, hasSessions: true })),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useUpdateSelectSessionModalCallback: () => jest.fn().mockName('mockOpenSessionModal'),
|
||||
useUpdateSelectSessionModalCallback: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableSelectSession: false })));
|
||||
|
||||
let wrapper;
|
||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||
|
||||
describe('SelectSessionButton', () => {
|
||||
const props = { cardId: 'cardId' };
|
||||
describe('snapshot', () => {
|
||||
test('renders default button', () => {
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('renders disabled button when user does not have access to the course', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('renders disabled button if masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
it('default render', () => {
|
||||
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: 'Select Session' });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
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('behavior', () => {
|
||||
it('default render', () => {
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick).getMockName())
|
||||
.toEqual(reduxHooks.useUpdateSelectSessionModalCallback().getMockName());
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('learner does not have access', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner cannot change sessions', () => {
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ canChange: false, hasSessions: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('entitlement does not have available sessions', () => {
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ canChange: true, hasSessions: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('user is masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Locked } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const UpgradeButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { upgradeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { canUpgrade } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const trackUpgradeClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.upgradeClicked,
|
||||
cardId,
|
||||
upgradeUrl,
|
||||
);
|
||||
|
||||
const isEnabled = (!isMasquerading && canUpgrade);
|
||||
const enabledProps = {
|
||||
as: 'a',
|
||||
href: upgradeUrl,
|
||||
onClick: trackUpgradeClick,
|
||||
};
|
||||
return (
|
||||
<ActionButton
|
||||
iconBefore={Locked}
|
||||
variant="outline-primary"
|
||||
disabled={!isEnabled}
|
||||
{...isEnabled && enabledProps}
|
||||
>
|
||||
{formatMessage(messages.upgrade)}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
UpgradeButton.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default UpgradeButton;
|
||||
@@ -1,57 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
upgradeClicked: jest.fn().mockName('segment.trackUpgradeClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
|
||||
describe('UpgradeButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
const upgradeUrl = 'upgradeUrl';
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ upgradeUrl });
|
||||
describe('snapshot', () => {
|
||||
test('can upgrade', () => {
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.upgradeClicked,
|
||||
props.cardId,
|
||||
upgradeUrl,
|
||||
));
|
||||
});
|
||||
test('cannot upgrade', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('masquerading', () => {
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,23 +5,23 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const ViewCourseButton = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { homeUrl } = reduxHooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess, isAudit, isAuditAccessExpired } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { disableViewCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
);
|
||||
// disabled on no access or (is audit track but audit access was expired)
|
||||
const disabledViewCourseButton = !hasAccess || (isAudit && isAuditAccessExpired);
|
||||
return (
|
||||
<ActionButton
|
||||
disabled={disabledViewCourseButton}
|
||||
disabled={disableViewCourse}
|
||||
as="a"
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import track from 'tracking';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
@@ -13,73 +15,41 @@ jest.mock('tracking', () => ({
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useTrackCourseEvent: jest.fn(
|
||||
(eventName, cardId, upgradeUrl) => ({ trackCourseEvent: { eventName, cardId, upgradeUrl } }),
|
||||
),
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'homeUrl' })),
|
||||
useTrackCourseEvent: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./ActionButton', () => 'ActionButton');
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
|
||||
|
||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||
|
||||
let wrapper;
|
||||
const defaultProps = { cardId: 'cardId' };
|
||||
const homeUrl = 'homeUrl';
|
||||
|
||||
const createWrapper = ({
|
||||
hasAccess = false,
|
||||
isAudit = false,
|
||||
isAuditAccessExpired = false,
|
||||
isEntitlement = false,
|
||||
isExpired = false,
|
||||
propsOveride = {},
|
||||
}) => {
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess, isAudit, isAuditAccessExpired });
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
|
||||
return shallow(<ViewCourseButton {...defaultProps} {...propsOveride} />);
|
||||
};
|
||||
|
||||
describe('ViewCourseButton', () => {
|
||||
describe('learner has access to course', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ hasAccess: true });
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('links to home URL', () => {
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
defaultProps.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
});
|
||||
test('link is enabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
test('link is disabled when audit access is expired', () => {
|
||||
wrapper = createWrapper({ hasAccess: true, isAudit: true, isAuditAccessExpired: true });
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
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');
|
||||
});
|
||||
describe('learner does not have access to course', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ hasAccess: false });
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('links to home URL', () => {
|
||||
expect(wrapper.prop(htmlProps.onClick)).toEqual(reduxHooks.useTrackCourseEvent(
|
||||
track.course.enterCourseClicked,
|
||||
defaultProps.cardId,
|
||||
homeUrl,
|
||||
));
|
||||
});
|
||||
test('link is disabled', () => {
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
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,
|
||||
);
|
||||
});
|
||||
it('learner cannot view course', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableViewCourse: 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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BeginCourseButton snapshot renders default button when learner has access to the course 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "home-url",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Begin Course
|
||||
</ActionButton>
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResumeButton snapshot renders default button when learner has access to the course 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="#"
|
||||
onClick={[MockFunction useTrackCourseEvent('enterCourseClicked', 'cardId', 'resumeUrl')]}
|
||||
>
|
||||
Resume
|
||||
</ActionButton>
|
||||
`;
|
||||
@@ -1,28 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectSessionButton snapshot renders default button 1`] = `
|
||||
<ActionButton
|
||||
disabled={false}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Select Session
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionButton snapshot renders disabled button if masquerading 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Select Session
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionButton snapshot renders disabled button when user does not have access to the course 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Select Session
|
||||
</ActionButton>
|
||||
`;
|
||||
@@ -1,42 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UpgradeButton snapshot can upgrade 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="upgradeUrl"
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.trackUpgradeClicked],
|
||||
"upgradeUrl": "upgradeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`UpgradeButton snapshot cannot upgrade 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`UpgradeButton snapshot masquerading 1`] = `
|
||||
<ActionButton
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</ActionButton>
|
||||
`;
|
||||
@@ -1,39 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ViewCourseButton learner does not have access to course snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={true}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
View Course
|
||||
</ActionButton>
|
||||
`;
|
||||
|
||||
exports[`ViewCourseButton learner has access to course snapshot 1`] = `
|
||||
<ActionButton
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="#"
|
||||
onClick={
|
||||
Object {
|
||||
"trackCourseEvent": Object {
|
||||
"cardId": "cardId",
|
||||
"eventName": [MockFunction segment.enterCourseClicked],
|
||||
"upgradeUrl": "homeUrl",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
View Course
|
||||
</ActionButton>
|
||||
`;
|
||||
@@ -1,54 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardActions snapshot show begin course button when verified and not entitlement and has started 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<BeginCourseButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show resume button when verified and not entitlement and has started 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<ResumeButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show select session button when not verified and entitlement 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<SelectSessionButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show upgrade button when not verified and not entitlement 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<UpgradeButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
<BeginCourseButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCardActions snapshot show view course button when not verified and entitlement and fulfilled 1`] = `
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<ViewCourseButton
|
||||
cardId="cardId"
|
||||
/>
|
||||
</ActionRow>
|
||||
`;
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ActionRow } from '@edx/paragon';
|
||||
import { ActionRow } from '@openedx/paragon';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
@@ -13,21 +13,25 @@ import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
export const CourseCardActions = ({ cardId }) => {
|
||||
const { isEntitlement, isFulfilled } = reduxHooks.useCardEntitlementData(cardId);
|
||||
const { isVerified, hasStarted } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const {
|
||||
hasStarted,
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const { isArchived } = reduxHooks.useCardCourseRunData(cardId);
|
||||
let PrimaryButton;
|
||||
if (isEntitlement) {
|
||||
PrimaryButton = isFulfilled ? ViewCourseButton : SelectSessionButton;
|
||||
} else if (isArchived) {
|
||||
PrimaryButton = ViewCourseButton;
|
||||
} else {
|
||||
PrimaryButton = hasStarted ? ResumeButton : BeginCourseButton;
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionRow data-test-id="CourseCardActions">
|
||||
{!(isEntitlement || isVerified) && <UpgradeButton cardId={cardId} />}
|
||||
<PrimaryButton cardId={cardId} />
|
||||
<CourseCardActionSlot cardId={cardId} />
|
||||
{isEntitlement && (isFulfilled
|
||||
? <ViewCourseButton cardId={cardId} />
|
||||
: <SelectSessionButton cardId={cardId} />
|
||||
)}
|
||||
{(isArchived && !isEntitlement) && (
|
||||
<ViewCourseButton cardId={cardId} />
|
||||
)}
|
||||
{!(isArchived || isEntitlement) && (hasStarted
|
||||
? <ResumeButton cardId={cardId} />
|
||||
: <BeginCourseButton cardId={cardId} />
|
||||
)}
|
||||
</ActionRow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import CourseCardActions from '.';
|
||||
@@ -9,96 +8,88 @@ jest.mock('hooks', () => ({
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
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>));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const props = { cardId };
|
||||
|
||||
describe('CourseCardActions', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
const createWrapper = ({
|
||||
isEntitlement, isFulfilled, isArchived, isVerified, hasStarted,
|
||||
}) => {
|
||||
const mockHooks = ({
|
||||
isEntitlement = false,
|
||||
isExecEd2UCourse = false,
|
||||
isFulfilled = false,
|
||||
isArchived = false,
|
||||
isVerified = false,
|
||||
hasStarted = false,
|
||||
isMasquerading = false,
|
||||
} = {}) => {
|
||||
reduxHooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isVerified, hasStarted });
|
||||
return shallow(<CourseCardActions {...props} />);
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isExecEd2UCourse, isVerified, hasStarted });
|
||||
reduxHooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading });
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('show upgrade button when not verified and not entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show select session button when not verified and entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show begin course button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show resume button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: true,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('show view course button when not verified and entitlement and fulfilled', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: true, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const renderComponent = () => render(<CourseCardActions {...props} />);
|
||||
describe('hooks', () => {
|
||||
it('initializes redux hooks', () => {
|
||||
mockHooks();
|
||||
renderComponent();
|
||||
expect(reduxHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
||||
expect(reduxHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('show upgrade button when not verified and not entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
describe('output', () => {
|
||||
describe('entitlement course', () => {
|
||||
it('renders ViewCourseButton if fulfilled', () => {
|
||||
mockHooks({ isEntitlement: true, isFulfilled: true });
|
||||
renderComponent();
|
||||
const ViewCourseButton = screen.getByText('ViewCourseButton');
|
||||
expect(ViewCourseButton).toBeInTheDocument();
|
||||
});
|
||||
it('renders SelectSessionButton if not fulfilled', () => {
|
||||
mockHooks({ isEntitlement: true });
|
||||
renderComponent();
|
||||
const SelectSessionButton = screen.getByText('SelectSessionButton');
|
||||
expect(SelectSessionButton).toBeInTheDocument();
|
||||
});
|
||||
expect(wrapper.find('UpgradeButton')).toHaveLength(1);
|
||||
});
|
||||
it('show select session button when not verified and entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
describe('not entitlement, verified, or exec ed', () => {
|
||||
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
|
||||
mockHooks({ isArchived: true });
|
||||
renderComponent();
|
||||
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
||||
expect(CourseCardActionSlot).toBeInTheDocument();
|
||||
const ViewCourseButton = screen.getByText('ViewCourseButton');
|
||||
expect(ViewCourseButton).toBeInTheDocument();
|
||||
});
|
||||
expect(wrapper.find('SelectSessionButton')).toHaveLength(1);
|
||||
});
|
||||
it('show begin course button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: false,
|
||||
describe('unstarted courses', () => {
|
||||
it('renders CourseCardActionSlot and BeginCourseButton', () => {
|
||||
mockHooks();
|
||||
renderComponent();
|
||||
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
||||
expect(CourseCardActionSlot).toBeInTheDocument();
|
||||
const BeginCourseButton = screen.getByText('BeginCourseButton');
|
||||
expect(BeginCourseButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(wrapper.find('BeginCourseButton')).toHaveLength(1);
|
||||
});
|
||||
it('show resume button when verified and not entitlement and has started', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: false, isFulfilled: false, isArchived: false, isVerified: true, hasStarted: true,
|
||||
describe('active courses (started, and not archived)', () => {
|
||||
it('renders CourseCardActionSlot and ResumeButton', () => {
|
||||
mockHooks({ hasStarted: true });
|
||||
renderComponent();
|
||||
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
||||
expect(CourseCardActionSlot).toBeInTheDocument();
|
||||
const ResumeButton = screen.getByText('ResumeButton');
|
||||
expect(ResumeButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(wrapper.find('ResumeButton')).toHaveLength(1);
|
||||
});
|
||||
it('show view course button when not verified and entitlement and fulfilled', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: true, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper.find('ViewCourseButton')).toHaveLength(1);
|
||||
});
|
||||
it('show view course button when not verified and entitlement and fulfilled and archived', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: true, isArchived: true, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper.find('ViewCourseButton')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export const messages = StrictDict({
|
||||
upgrade: {
|
||||
id: 'learner-dash.courseCard.actions.upgrade',
|
||||
description: 'Course card upgrade button text',
|
||||
defaultMessage: 'Upgrade',
|
||||
},
|
||||
const messages = defineMessages({
|
||||
beginCourse: {
|
||||
id: 'learner-dash.courseCard.actions.beginCourse',
|
||||
description: 'Course card begin-course button text',
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MailtoLink, Hyperlink } from '@edx/paragon';
|
||||
import { CheckCircle } from '@edx/paragon/icons';
|
||||
import { MailtoLink, Hyperlink } from '@openedx/paragon';
|
||||
import { CheckCircle } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
@@ -26,16 +26,28 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
const emailLink = address => address && <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
const emailLink = address => <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
|
||||
if (certificate.isRestricted) {
|
||||
return (
|
||||
<Banner variant="danger">
|
||||
{formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) })}
|
||||
{ supportEmail ? formatMessage(messages.certRestricted, { supportEmail: emailLink(supportEmail) }) : formatMessage(messages.certRestrictedNoEmail)}
|
||||
{isVerified && ' '}
|
||||
{isVerified && formatMessage(
|
||||
messages.certRefundContactBilling,
|
||||
{ billingEmail: emailLink(billingEmail) },
|
||||
{isVerified && (billingEmail ? formatMessage(messages.certRefundContactBilling, { billingEmail: emailLink(billingEmail) }) : formatMessage(messages.certRefundContactBillingNoEmail))}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isDownloadable) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
{formatMessage(messages.certReady)}
|
||||
{certificate.certPreviewUrl && (
|
||||
<>
|
||||
{' '}
|
||||
<Hyperlink isInline destination={certificate.certPreviewUrl}>
|
||||
{formatMessage(messages.viewCertificate)}
|
||||
</Hyperlink>
|
||||
</>
|
||||
)}
|
||||
</Banner>
|
||||
);
|
||||
@@ -63,17 +75,6 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isDownloadable) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
{formatMessage(messages.certReady)}
|
||||
{' '}
|
||||
<Hyperlink isInline destination={certificate.certPreviewUrl}>
|
||||
{formatMessage(messages.viewCertificate)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isEarnedButUnavailable) {
|
||||
return (
|
||||
<Banner>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { shallow } from 'enzyme';
|
||||
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,135 +17,226 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
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.usePlatformSettingsData.mockReturnValue({
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
});
|
||||
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 createWrapper = ({
|
||||
certificate = {},
|
||||
enrollment = {},
|
||||
grade = {},
|
||||
courseRun = {},
|
||||
platformSettings = {},
|
||||
}) => {
|
||||
reduxHooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
|
||||
reduxHooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
|
||||
reduxHooks.useCardCourseRunData.mockReturnValueOnce({ ...defaultCourseRun, ...courseRun });
|
||||
return shallow(<CertificateBanner {...props} />);
|
||||
reduxHooks.usePlatformSettingsData.mockReturnValueOnce({ ...defaultPlatformSettings, ...platformSettings });
|
||||
return render(<IntlProvider locale="en"><CertificateBanner {...props} /></IntlProvider>);
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('is restricted', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is restricted and verified', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
enrollment: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('not passing and audit', () => {
|
||||
const wrapper = createWrapper({
|
||||
enrollment: {
|
||||
isAudit: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('not passing and has finished', () => {
|
||||
const wrapper = createWrapper({
|
||||
courseRun: { isArchived: true },
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('not passing and not audit and not finished', () => {
|
||||
const wrapper = createWrapper({});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is passing and is downloadable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: {
|
||||
isPassing: true,
|
||||
},
|
||||
certificate: {
|
||||
isDownloadable: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is passing and is earned but unavailable', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: {
|
||||
isPassing: true,
|
||||
},
|
||||
certificate: {
|
||||
isEarnedButUnavailable: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('is passing and not downloadable render empty', () => {
|
||||
const wrapper = createWrapper({
|
||||
grade: {
|
||||
isPassing: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('is restricted', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
});
|
||||
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
|
||||
expect(bannerMessage).toEqual(messages.certRestricted.defaultMessage);
|
||||
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
|
||||
it('is restricted', () => {
|
||||
createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
});
|
||||
it('is restricted and verified', () => {
|
||||
const wrapper = createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
enrollment: {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
const bannerMessage = wrapper.find('format-message-function').map(el => el.prop('message').defaultMessage).join('\n');
|
||||
expect(bannerMessage).toContain(messages.certRestricted.defaultMessage);
|
||||
expect(bannerMessage).toContain(messages.certRefundContactBilling.defaultMessage);
|
||||
const banner = screen.getByRole('alert');
|
||||
expect(banner).toBeInTheDocument();
|
||||
const msg = screen.getByText((text) => text.includes('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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { utilHooks, reduxHooks } from 'hooks';
|
||||
@@ -12,7 +12,6 @@ export const CourseBanner = ({ cardId }) => {
|
||||
const {
|
||||
isVerified,
|
||||
isAuditAccessExpired,
|
||||
canUpgrade,
|
||||
coursewareAccess = {},
|
||||
} = reduxHooks.useCardEnrollmentData(cardId);
|
||||
const courseRun = reduxHooks.useCardCourseRunData(cardId);
|
||||
@@ -26,33 +25,15 @@ export const CourseBanner = ({ cardId }) => {
|
||||
return (
|
||||
<>
|
||||
{isAuditAccessExpired
|
||||
&& (canUpgrade ? (
|
||||
&& (
|
||||
<Banner>
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
{formatMessage(messages.upgradeToAccess)}
|
||||
<Hyperlink isInline destination="">
|
||||
{formatMessage(messages.findAnotherCourse)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
) : (
|
||||
<Banner>
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
{
|
||||
<Hyperlink isInline destination="">
|
||||
{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>
|
||||
@@ -61,6 +42,7 @@ export const CourseBanner = ({ cardId }) => {
|
||||
})}
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
{(!isStaff && hasUnmetPrerequisites) && (
|
||||
<Banner>{formatMessage(messages.prerequisitesNotMet)}</Banner>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Hyperlink } from '@edx/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,10 @@ jest.mock('hooks', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const cardId = 'my-test-course-number';
|
||||
|
||||
let el;
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
const enrollmentData = {
|
||||
isVerified: false,
|
||||
canUpgrade: false,
|
||||
isAuditAccessExpired: false,
|
||||
coursewareAccess: {
|
||||
hasUnmetPrerequisites: false,
|
||||
@@ -39,7 +34,7 @@ const courseRunData = {
|
||||
marketingUrl: 'marketing-url',
|
||||
};
|
||||
|
||||
const render = (overrides = {}) => {
|
||||
const renderCourseBanner = (overrides = {}) => {
|
||||
const {
|
||||
courseRun = {},
|
||||
enrollment = {},
|
||||
@@ -52,114 +47,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).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
|
||||
expect(el.text()).toContain(messages.upgradeToAccess.defaultMessage);
|
||||
});
|
||||
});
|
||||
describe('audit access expired, cannot upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render({ enrollment: { isAuditAccessExpired: true } });
|
||||
});
|
||||
test('snapshot: (auditAccessExpired, findAnotherCourse hyperlink)', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
|
||||
expect(el.find(Hyperlink).text()).toEqual(messages.findAnotherCourse.defaultMessage);
|
||||
});
|
||||
});
|
||||
describe('course run active and cannot upgrade', () => {
|
||||
beforeEach(() => {
|
||||
render({ courseRun: { isActive: true } });
|
||||
});
|
||||
test('snapshot: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('messages: (upgradseDeadlinePassed, exploreCourseDetails hyperlink)', () => {
|
||||
expect(el.text()).toContain(messages.upgradeDeadlinePassed.defaultMessage);
|
||||
const link = el.find(Hyperlink);
|
||||
expect(link.text()).toEqual(messages.exploreCourseDetails.defaultMessage);
|
||||
expect(link.props().destination).toEqual(courseRunData.marketingUrl);
|
||||
});
|
||||
});
|
||||
test('no display if audit access not expired and (course is not active or can upgrade)', () => {
|
||||
render();
|
||||
// isEmptyRender() isn't true because the minimal is <Fragment />
|
||||
expect(el.html()).toEqual('');
|
||||
render({ enrollment: { canUpgrade: true }, courseRun: { isActive: true } });
|
||||
expect(el.html()).toEqual('');
|
||||
});
|
||||
describe('unmet prerequisites', () => {
|
||||
beforeEach(() => {
|
||||
render({ enrollment: { coursewareAccess: { hasUnmetPrerequisites: true } } });
|
||||
});
|
||||
test('snapshot: unmetPrerequisites', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('messages: prerequisitesNotMet', () => {
|
||||
expect(el.text()).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).toMatchSnapshot());
|
||||
test('messages', () => expect(el.text()).toEqual(''));
|
||||
});
|
||||
describe('has start date', () => {
|
||||
beforeEach(() => {
|
||||
render({ enrollment: { coursewareAccess: { isTooEarly: true } } });
|
||||
});
|
||||
test('snapshot', () => expect(el).toMatchSnapshot());
|
||||
|
||||
test('messages: courseHasNotStarted', () => {
|
||||
expect(el.text()).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).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
test('snapshot: stacking banners', () => {
|
||||
render({
|
||||
enrollment: {
|
||||
coursewareAccess: {
|
||||
isStaff: true,
|
||||
isTooEarly: true,
|
||||
hasUnmetPrerequisites: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CreditBanner component render with error state snapshot 1`] = `
|
||||
<Banner
|
||||
variant="danger"
|
||||
>
|
||||
<p
|
||||
className="credit-error-msg"
|
||||
>
|
||||
<format-message-function
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "An error occurred with this transaction. For help, contact {supportEmailLink}.",
|
||||
"description": "",
|
||||
"id": "learner-dash.courseCard.banners.credit.error",
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"supportEmailLink": <EmailLink
|
||||
address="test-support-email"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<ContentComponent
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
|
||||
exports[`CreditBanner component render with no error state snapshot 1`] = `
|
||||
<Banner>
|
||||
<ContentComponent
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
@@ -4,8 +4,8 @@ import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
import EmailLink from 'components/EmailLink';
|
||||
|
||||
import { MailtoLink } from '@openedx/paragon';
|
||||
import hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -15,13 +15,14 @@ export const CreditBanner = ({ cardId }) => {
|
||||
if (hookData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ContentComponent, error, supportEmail } = hookData;
|
||||
const supportEmailLink = (<EmailLink address={supportEmail} />);
|
||||
const supportEmailLink = (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>);
|
||||
return (
|
||||
<Banner {...(error && { variant: 'danger' })}>
|
||||
{error && (
|
||||
<p className="credit-error-msg">
|
||||
{formatMessage(messages.error, { supportEmailLink })}
|
||||
<p className="credit-error-msg" data-testid="credit-error-msg">
|
||||
{supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)}
|
||||
</p>
|
||||
)}
|
||||
<ContentComponent cardId={cardId} />
|
||||
|
||||
@@ -1,82 +1,65 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import EmailLink from 'components/EmailLink';
|
||||
|
||||
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');
|
||||
jest.mock('components/EmailLink', () => 'EmailLink');
|
||||
import { CreditBanner } from '.';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useCreditBannerData: jest.fn(),
|
||||
}));
|
||||
|
||||
let el;
|
||||
const cardId = 'test-card-id';
|
||||
describe('CreditBanner', () => {
|
||||
const mockCardId = 'test-card-id';
|
||||
const mockContentComponent = () => <div data-testid="mock-content">Test Content</div>;
|
||||
const mockSupportEmail = 'support@test.com';
|
||||
|
||||
const ContentComponent = () => 'ContentComponent';
|
||||
const supportEmail = 'test-support-email';
|
||||
const renderCreditBanner = () => render(
|
||||
<IntlProvider locale="en">
|
||||
<CreditBanner cardId={mockCardId} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
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).toMatchSnapshot();
|
||||
});
|
||||
it('passes danger variant to Banner parent', () => {
|
||||
expect(el.find('Banner').props().variant).toEqual('danger');
|
||||
});
|
||||
it('includes credit-error-msg with support email link', () => {
|
||||
expect(el.find('.credit-error-msg').containsMatchingElement(
|
||||
formatMessage(messages.error, {
|
||||
supportEmailLink: (<EmailLink address={supportEmail} />),
|
||||
}),
|
||||
)).toEqual(true);
|
||||
});
|
||||
it('loads ContentComponent with cardId', () => {
|
||||
expect(el.find('ContentComponent').props().cardId).toEqual(cardId);
|
||||
});
|
||||
|
||||
it('should return null if hook returns null', () => {
|
||||
hooks.useCreditBannerData.mockReturnValue(null);
|
||||
renderCreditBanner();
|
||||
const banner = screen.queryByRole('alert');
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
|
||||
it('should render content component without error', () => {
|
||||
hooks.useCreditBannerData.mockReturnValue({
|
||||
ContentComponent: mockContentComponent,
|
||||
error: false,
|
||||
});
|
||||
describe('with no error state', () => {
|
||||
beforeEach(() => {
|
||||
hooks.useCreditBannerData.mockReturnValue({
|
||||
error: false,
|
||||
ContentComponent,
|
||||
supportEmail,
|
||||
});
|
||||
el = shallow(<CreditBanner cardId={cardId} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('loads ContentComponent with cardId', () => {
|
||||
expect(el.find('ContentComponent').props().cardId).toEqual(cardId);
|
||||
});
|
||||
renderCreditBanner();
|
||||
|
||||
expect(screen.getByTestId('mock-content')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('credit-error-msg')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error message with support email', () => {
|
||||
hooks.useCreditBannerData.mockReturnValue({
|
||||
ContentComponent: mockContentComponent,
|
||||
error: true,
|
||||
supportEmail: mockSupportEmail,
|
||||
});
|
||||
renderCreditBanner();
|
||||
|
||||
expect(screen.getByTestId('credit-error-msg')).toBeInTheDocument();
|
||||
expect(screen.getByText(mockSupportEmail)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error message without support email', () => {
|
||||
hooks.useCreditBannerData.mockReturnValue({
|
||||
ContentComponent: mockContentComponent,
|
||||
error: true,
|
||||
});
|
||||
renderCreditBanner();
|
||||
|
||||
expect(screen.getByTestId('credit-error-msg')).toBeInTheDocument();
|
||||
expect(screen.queryByText(mockSupportEmail)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export const messages = StrictDict({
|
||||
const messages = defineMessages({
|
||||
error: {
|
||||
id: 'learner-dash.courseCard.banners.credit.error',
|
||||
description: '',
|
||||
defaultMessage: 'An error occurred with this transaction. For help, contact {supportEmailLink}.',
|
||||
},
|
||||
errorNoEmail: {
|
||||
id: 'learner-dash.courseCard.banners.credit.errorNoEmail',
|
||||
description: '',
|
||||
defaultMessage: 'An error occurred with this transaction.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -11,10 +11,11 @@ import messages from './messages';
|
||||
|
||||
export const ApprovedContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{ href, message: formatMessage(messages.viewCredit) }}
|
||||
action={{ href, message: formatMessage(messages.viewCredit), disabled: isMasquerading }}
|
||||
message={formatMessage(
|
||||
messages.approved,
|
||||
{
|
||||
|
||||
@@ -1,58 +1,74 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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', () => ({
|
||||
reduxHooks: {
|
||||
useCardCreditData: jest.fn(),
|
||||
useMasqueradeData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
let el;
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
providerStatusUrl: 'test-credit-provider-status-url',
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
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.find('CreditContent');
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||
});
|
||||
test('action.href from credit.providerStatusUrl', () => {
|
||||
expect(component.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.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('message is formatted approved message', () => {
|
||||
expect(component.props().message).toEqual(formatMessage(
|
||||
messages.approved,
|
||||
{
|
||||
congratulations: (<b>{formatMessage(messages.congratulations)}</b>),
|
||||
linkToProviderSite: <ProviderLink cardId={cardId} />,
|
||||
providerName: credit.providerName,
|
||||
},
|
||||
));
|
||||
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();
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import track from 'tracking';
|
||||
|
||||
import messages from './messages';
|
||||
@@ -14,16 +14,13 @@ 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;
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const courseId = 'test-course-id';
|
||||
const credit = {
|
||||
@@ -32,50 +29,45 @@ const credit = {
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
reduxHooks.useCardCourseRunData.mockReturnValue({ courseId });
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<EligibleContent cardId={cardId} />);
|
||||
};
|
||||
const loadComponent = () => {
|
||||
component = el.find('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.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.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.props().message).toEqual(formatMessage(
|
||||
messages.eligible,
|
||||
{ getCredit: (<b>{formatMessage(messages.getCredit)}</b>) },
|
||||
));
|
||||
});
|
||||
test('message is formatted eligible message if provider', () => {
|
||||
expect(component.props().message).toEqual(
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import hooks from './hooks';
|
||||
@@ -12,11 +13,13 @@ import messages from './messages';
|
||||
export const MustRequestContent = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
message: formatMessage(messages.requestCredit),
|
||||
onClick: createCreditRequest,
|
||||
disabled: isMasquerading,
|
||||
}}
|
||||
message={formatMessage(messages.mustRequest, {
|
||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||
|
||||
@@ -1,60 +1,103 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
let el;
|
||||
let component;
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useMasqueradeData: jest.fn(),
|
||||
useCardCreditData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const requestData = { test: 'requestData' };
|
||||
const createCreditRequest = jest.fn().mockName('createCreditRequest');
|
||||
hooks.useCreditRequestData.mockReturnValue({ requestData, createCreditRequest });
|
||||
|
||||
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.find('CreditContent');
|
||||
renderMustRequestContent();
|
||||
});
|
||||
test('action.onClick calls createCreditRequest from useCreditRequestData hook', () => {
|
||||
expect(component.props().action.onClick).toEqual(createCreditRequest);
|
||||
|
||||
it('calls createCreditRequest when request credit button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button', { name: /request credit/i });
|
||||
await user.click(button);
|
||||
expect(createCreditRequest).toHaveBeenCalled();
|
||||
});
|
||||
test('action.message is formatted requestCredit message', () => {
|
||||
expect(component.props().action.message).toEqual(formatMessage(messages.requestCredit));
|
||||
|
||||
it('shows request credit button that is enabled', () => {
|
||||
const button = screen.getByRole('button', { name: /request credit/i });
|
||||
expect(button).toBeEnabled();
|
||||
});
|
||||
test('message is formatted mustRequest message', () => {
|
||||
expect(component.props().message).toEqual(
|
||||
formatMessage(messages.mustRequest, {
|
||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||
requestCredit: (<b>{formatMessage(messages.requestCredit)}</b>),
|
||||
}),
|
||||
);
|
||||
|
||||
it('displays must request message with provider link', () => {
|
||||
expect(screen.getByTestId('credit-msg')).toHaveTextContent(/request credit/i);
|
||||
});
|
||||
test('requestData drawn from useCreditRequestData hook', () => {
|
||||
expect(component.props().requestData).toEqual(requestData);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,12 +9,14 @@ import messages from './messages';
|
||||
|
||||
export const PendingContent = ({ cardId }) => {
|
||||
const { providerStatusUrl: href, providerName } = reduxHooks.useCardCreditData(cardId);
|
||||
const { isMasquerading } = reduxHooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
href,
|
||||
message: formatMessage(messages.viewDetails),
|
||||
disabled: isMasquerading,
|
||||
}}
|
||||
message={formatMessage(messages.received, { providerName })}
|
||||
/>
|
||||
|
||||
@@ -1,51 +1,66 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
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 PendingContent from './PendingContent';
|
||||
|
||||
jest.mock('hooks', () => ({ reduxHooks: { useCardCreditData: jest.fn() } }));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
let el;
|
||||
let component;
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: { useCardCreditData: jest.fn(), useMasqueradeData: jest.fn() },
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const providerName = 'test-credit-provider-name';
|
||||
const providerStatusUrl = 'test-credit-provider-status-url';
|
||||
reduxHooks.useCardCreditData.mockReturnValue({ providerName, providerStatusUrl });
|
||||
reduxHooks.useCardCreditData.mockReturnValue({
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
});
|
||||
reduxHooks.useMasqueradeData.mockReturnValue({ isMasquerading: false });
|
||||
|
||||
const render = () => {
|
||||
el = shallow(<PendingContent cardId={cardId} />);
|
||||
};
|
||||
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.find('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.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.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('message is formatted pending message', () => {
|
||||
expect(component.props().message).toEqual(
|
||||
formatMessage(messages.received, { providerName }),
|
||||
);
|
||||
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`);
|
||||
});
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
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,6 @@ jest.mock('hooks', () => ({
|
||||
useCardCreditData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('./components/CreditContent', () => 'CreditContent');
|
||||
jest.mock('./components/ProviderLink', () => 'ProviderLink');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
@@ -22,32 +17,27 @@ const credit = {
|
||||
};
|
||||
reduxHooks.useCardCreditData.mockReturnValue(credit);
|
||||
|
||||
let el;
|
||||
let component;
|
||||
const render = () => { el = shallow(<RejectedContent cardId={cardId} />); };
|
||||
const loadComponent = () => { component = el.find('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.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.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.`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ActionRow, Button } from '@edx/paragon';
|
||||
import { ActionRow, Button } from '@openedx/paragon';
|
||||
import CreditRequestForm from './CreditRequestForm';
|
||||
|
||||
export const CreditContent = ({ action, message, requestData }) => (
|
||||
<>
|
||||
<div className="message-copy credit-msg">
|
||||
<div className="message-copy credit-msg" data-testid="credit-msg">
|
||||
{message}
|
||||
</div>
|
||||
{action && (
|
||||
<ActionRow className="mt-4">
|
||||
<Button
|
||||
as="a"
|
||||
href={action.href}
|
||||
disabled={!!action.disabled}
|
||||
// make sure href is not undefined. Paragon won't disable the button if href is undefined.
|
||||
href={action.href || '#'}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
variant="outline-primary"
|
||||
className="border-gray-400"
|
||||
onClick={action.onClick}
|
||||
data-testid="action-row-btn"
|
||||
>
|
||||
{action.message}
|
||||
</Button>
|
||||
@@ -36,6 +39,7 @@ CreditContent.propTypes = {
|
||||
href: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
message: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
}),
|
||||
message: PropTypes.node.isRequired,
|
||||
requestData: PropTypes.shape({
|
||||
|
||||
@@ -1,53 +1,69 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import CreditContent from './CreditContent';
|
||||
|
||||
let el;
|
||||
const action = {
|
||||
href: 'test-action-href',
|
||||
onClick: jest.fn().mockName('test-action-onClick'),
|
||||
message: 'test-action-message',
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
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).toMatchSnapshot();
|
||||
});
|
||||
it('loads href, onClick, and message into action row button', () => {
|
||||
const buttonEl = el.find('ActionRow Button');
|
||||
expect(buttonEl.props().href).toEqual(action.href);
|
||||
expect(buttonEl.props().onClick).toEqual(action.onClick);
|
||||
expect(buttonEl.text()).toEqual(action.message);
|
||||
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.find('div.credit-msg').text()).toEqual(message);
|
||||
renderCreditContent(props);
|
||||
const creditMsg = screen.getByTestId('credit-msg');
|
||||
expect(creditMsg).toBeInTheDocument();
|
||||
expect(creditMsg.innerHTML).toEqual(message);
|
||||
});
|
||||
it('loads CreditRequestForm with passed requestData', () => {
|
||||
expect(el.find('CreditRequestForm').props().requestData).toEqual(requestData);
|
||||
const { container } = renderCreditContent(props);
|
||||
const creditForm = container.querySelector('form');
|
||||
expect(creditForm).toBeInTheDocument();
|
||||
expect(creditForm).toHaveAttribute('action', requestData.url);
|
||||
});
|
||||
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).toMatchSnapshot();
|
||||
});
|
||||
it('loads message into credit-msg div', () => {
|
||||
expect(el.find('div.credit-msg').text()).toEqual(message);
|
||||
renderCreditContent({ message, requestData });
|
||||
const creditMsg = screen.getByTestId('credit-msg');
|
||||
expect(creditMsg).toBeInTheDocument();
|
||||
expect(creditMsg.innerHTML).toEqual(message);
|
||||
});
|
||||
it('loads CreditRequestForm with passed requestData', () => {
|
||||
expect(el.find('CreditRequestForm').props().requestData).toEqual(requestData);
|
||||
const { container } = renderCreditContent({ message, requestData });
|
||||
const creditForm = container.querySelector('form');
|
||||
expect(creditForm).toBeInTheDocument();
|
||||
expect(creditForm).toHaveAttribute('action', requestData.url);
|
||||
});
|
||||
it('does not render action row button', () => {
|
||||
renderCreditContent({ message, requestData });
|
||||
const button = screen.queryByRole('link', { name: action.message });
|
||||
expect(button).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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() },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Form, FormControl } from '@edx/paragon';
|
||||
import { Button, Form, FormControl } from '@openedx/paragon';
|
||||
|
||||
import useCreditRequestFormData from './hooks';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
|
||||
@@ -11,7 +10,8 @@ jest.mock('./hooks', () => ({
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const ref = 'test-ref';
|
||||
const ref = { current: { click: jest.fn() }, useRef: jest.fn() };
|
||||
|
||||
const requestData = {
|
||||
url: 'test-request-data-url',
|
||||
parameters: {
|
||||
@@ -25,40 +25,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).toMatchSnapshot();
|
||||
});
|
||||
it('loads Form with requestData url', () => {
|
||||
expect(el.find('Form').props().action).toEqual(requestData.url);
|
||||
const { container } = renderForm(requestData);
|
||||
const creditForm = container.querySelector('form');
|
||||
expect(creditForm).toBeInTheDocument();
|
||||
expect(creditForm).toHaveAttribute('action', requestData.url);
|
||||
});
|
||||
it('loads a textarea form control for each requestData parameter', () => {
|
||||
const controls = el.find('FormControl');
|
||||
expect(controls.at(0).props().name).toEqual(paramKeys.key1);
|
||||
expect(controls.at(0).props().value).toEqual(requestData.parameters.key1);
|
||||
expect(controls.at(1).props().name).toEqual(paramKeys.key2);
|
||||
expect(controls.at(1).props().value).toEqual(requestData.parameters.key2);
|
||||
expect(controls.at(2).props().name).toEqual(paramKeys.key3);
|
||||
expect(controls.at(2).props().value).toEqual(requestData.parameters.key3);
|
||||
const { 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,6 @@ import { render } from '@testing-library/react';
|
||||
import useCreditRequestFormData from './hooks';
|
||||
import CreditRequestForm from '.';
|
||||
|
||||
jest.unmock('@edx/paragon');
|
||||
jest.unmock('react');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
|
||||
export const ProviderLink = ({ cardId }) => {
|
||||
const credit = reduxHooks.useCardCreditData(cardId);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user