Compare commits
592 Commits
open-relea
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| faa45786f7 | |||
|
|
add424ac60 | ||
|
|
305960a029 | ||
|
|
a580d245c0 | ||
|
|
ca954e139d | ||
|
|
0d2eb96c86 | ||
|
|
f1180bffde | ||
|
|
f93598a9e4 | ||
|
|
35680b8b05 | ||
|
|
5d1000530d | ||
|
|
eef5d3f671 | ||
|
|
8fc839dc3d | ||
|
|
b21e6e553d | ||
|
|
cfc82975d8 | ||
|
|
50ea19d6af | ||
|
|
9e61ae677e | ||
|
|
247794b21d | ||
|
|
0a50937b4c | ||
|
|
22a1c658f1 | ||
|
|
75396f1dab | ||
|
|
62099a50eb | ||
|
|
19ccb8ab87 | ||
|
|
a3e2c80537 | ||
|
|
324cb525c6 | ||
|
|
f14ab8851d | ||
|
|
c277150716 | ||
|
|
2a0ed5714f | ||
|
|
1f0b758705 | ||
|
|
85a5a6e94e | ||
|
|
f59b5013c8 | ||
|
|
86b6574c60 | ||
|
|
c38e80505c | ||
|
|
b926e13c01 | ||
|
|
0a4285aad3 | ||
|
|
e70fa29261 | ||
|
|
b6ba8fb366 | ||
|
|
2221655950 | ||
|
|
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 | ||
|
|
3c7be4c65c | ||
|
|
7ccf049edb | ||
|
|
49d70dda93 | ||
|
|
6b225cbf86 | ||
|
|
07874fecf7 | ||
|
|
b79c0d1434 | ||
|
|
63ab012bcb | ||
|
|
f25a15d917 | ||
|
|
ff631a21c2 | ||
|
|
3f801caf38 | ||
|
|
38b0d5832f | ||
|
|
33c50082ef | ||
|
|
cba982a7a0 | ||
|
|
55de93ef59 | ||
|
|
0933d185af | ||
|
|
a13085a6a1 | ||
|
|
dc3141cc65 | ||
|
|
b2e8621e5c | ||
|
|
82268b4f37 | ||
|
|
044bf0f45a | ||
|
|
166c64a391 | ||
|
|
a21698e96a | ||
|
|
41f563dd9a | ||
|
|
81f1282cb0 | ||
|
|
875ecdbdb0 | ||
|
|
a3106928e1 | ||
|
|
3b32a5cd16 | ||
|
|
b23e741a5e | ||
|
|
06bd224a20 | ||
|
|
6003099a73 | ||
|
|
03e42f45bb | ||
|
|
d878358984 | ||
|
|
61bf5f8685 | ||
|
|
f435e8d2c2 | ||
|
|
496abc5bfb | ||
|
|
b6c8f7e3b2 | ||
|
|
0c0aed00de | ||
|
|
bbe9b7bb80 | ||
|
|
821e6bce50 | ||
|
|
82219e9b08 | ||
|
|
9a57f9de13 | ||
|
|
15afb3645f | ||
|
|
2932693cda | ||
|
|
552614466a | ||
|
|
25383e3913 | ||
|
|
fbf812f75c | ||
|
|
1ed4bac475 | ||
|
|
48b157fdd0 | ||
|
|
e92a571cac | ||
|
|
2a72a85efd | ||
|
|
dde8d45df3 | ||
|
|
b8245d6631 | ||
|
|
1129ff2847 | ||
|
|
c6432864ab | ||
|
|
c8b729a65d | ||
|
|
0badf690a6 | ||
|
|
41df13b059 | ||
|
|
254ccfccb6 | ||
|
|
3973d173cf | ||
|
|
61e484af1f | ||
|
|
f8b181e8c9 | ||
|
|
115ef77e37 | ||
|
|
57eac99b42 | ||
|
|
7174f8de1e | ||
|
|
c16390e157 | ||
|
|
a0c0384a3d | ||
|
|
e4900351f8 | ||
|
|
0f4e2e28dd | ||
|
|
4b38aaa199 | ||
|
|
9abcf35100 | ||
|
|
66b7a97450 |
16
.env
16
.env
@@ -1,7 +1,9 @@
|
||||
NODE_ENV='production'
|
||||
APP_ID='learner-dashboard'
|
||||
NODE_PATH=./src
|
||||
BASE_URL=''
|
||||
LMS_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
LOGIN_URL=''
|
||||
LOGOUT_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
@@ -30,4 +32,16 @@ ENTERPRISE_MARKETING_URL=''
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE=''
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
LEARNING_MICROFRONTEND_URL=''
|
||||
LEARNING_BASE_URL=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
ACCOUNT_PROFILE_URL=''
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
SHOW_UNENROLL_SURVEY=true
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
NODE_ENV='development'
|
||||
PORT=1993
|
||||
BASE_URL='localhost:1993'
|
||||
APP_ID='learner-dashboard'
|
||||
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 +21,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'
|
||||
@@ -36,4 +37,17 @@ 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_MICROFRONTEND_URL='http://localhost:2000'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION='6'
|
||||
HOTJAR_DEBUG=''
|
||||
ACCOUNT_SETTINGS_URL='http://localhost:1997'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=false
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
SHOW_UNENROLL_SURVEY=true
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
22
.env.test
22
.env.test
@@ -1,13 +1,14 @@
|
||||
NODE_ENV='test'
|
||||
PORT=1993
|
||||
BASE_URL='localhost:1993'
|
||||
APP_ID='learner-dashboard'
|
||||
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 +21,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'
|
||||
@@ -36,4 +37,15 @@ 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_MICROFRONTEND_URL='http://localhost:2000'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
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'
|
||||
CAREER_LINK_URL=''
|
||||
ENABLE_EDX_PERSONAL_DASHBOARD=true
|
||||
ENABLE_PROGRAMS=false
|
||||
NON_BROWSABLE_COURSES=false
|
||||
SHOW_UNENROLL_SURVEY=true
|
||||
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': ['*'] } }],
|
||||
},
|
||||
});
|
||||
|
||||
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"
|
||||
}
|
||||
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
19
.github/workflows/add-depr-ticket-to-depr-board.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Run the workflow that adds new tickets that are either:
|
||||
# - labelled "DEPR"
|
||||
# - title starts with "[DEPR]"
|
||||
# - body starts with "Proposal Date" (this is the first template field)
|
||||
# to the org-wide DEPR project board
|
||||
|
||||
name: Add newly created DEPR issues to the DEPR project board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
routeissue:
|
||||
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "label: " it tries to apply
|
||||
# the label indicated in rest of comment.
|
||||
# If the comment starts with "remove label: ", it tries
|
||||
# to remove the indicated label.
|
||||
# Note: Labels are allowed to have spaces and this script does
|
||||
# not parse spaces (as often a space is legitimate), so the command
|
||||
# "label: really long lots of words label" will apply the
|
||||
# label "really long lots of words label"
|
||||
|
||||
name: Allows for the adding and removing of labels via comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
add_remove_labels:
|
||||
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||
|
||||
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: [12, 14, 16]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- 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
|
||||
@@ -32,6 +30,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@v2
|
||||
|
||||
- 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/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -7,4 +7,4 @@ on:
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
||||
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||
|
||||
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
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "assign me" it assigns the author to the
|
||||
# ticket (case insensitive)
|
||||
|
||||
name: Assign comment author to ticket if they say "assign me"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
self_assign_by_comment:
|
||||
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||
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
|
||||
|
||||
40
Makefile
40
Makefile
@@ -2,19 +2,20 @@ 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 is-es5
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
# Variables for additional translation sources and imports (define in edx-internal if needed)
|
||||
ATLAS_EXTRA_SOURCES ?=
|
||||
ATLAS_EXTRA_INTL_IMPORTS ?=
|
||||
ATLAS_OPTIONS ?=
|
||||
|
||||
.PHONY: test
|
||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||
@@ -44,20 +45,19 @@ 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 -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 \
|
||||
$(ATLAS_EXTRA_SOURCES)
|
||||
|
||||
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-learner-dashboard $(ATLAS_EXTRA_INTL_IMPORTS)
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
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.
|
||||
74
example.env.config.js
Normal file
74
example.env.config.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Learner Dashboard is now able to handle JS-based configuration!
|
||||
|
||||
For the time being, the `.env.*` files are still made available when cloning down this repo or pulling from
|
||||
the master branch. To switch to using `env.config.js`, make a copy of `example.env.config.js` and configure as needed.
|
||||
|
||||
For testing with Jest Snapshot, there is a mock in `/src/setupTest.jsx` for `getConfig` that will need to be
|
||||
uncommented.
|
||||
|
||||
Note: having both .env and env.config.js files will follow a predictable order, in which non-empty values in the
|
||||
JS-based config will overwrite the .env environment variables.
|
||||
|
||||
frontend-platform's getConfig loads configuration in the following sequence:
|
||||
- .env file config
|
||||
- optional handlers (commonly used to merge MFE-specific config in via additional process.env variables)
|
||||
- env.config.js file config
|
||||
- runtime config
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
NODE_ENV: 'development',
|
||||
APP_ID: 'learner-dashboard',
|
||||
NODE_PATH: './src',
|
||||
PORT: 1996,
|
||||
BASE_URL: 'localhost:1996',
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
ECOMMERCE_BASE_URL: 'http://localhost:18130',
|
||||
CREDIT_PURCHASE_URL: 'http://localhost:8140',
|
||||
LOGIN_URL: 'http://localhost:18000/login',
|
||||
LOGOUT_URL: 'http://localhost:18000/logout',
|
||||
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
|
||||
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
|
||||
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
|
||||
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
|
||||
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
|
||||
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
|
||||
USER_INFO_COOKIE_NAME: 'edx-user-info',
|
||||
SITE_NAME: 'localhost',
|
||||
DATA_API_BASE_URL: 'http://localhost:8000',
|
||||
// LMS_CLIENT_ID should match the lms DOT client application in your LMS container
|
||||
LMS_CLIENT_ID: 'login-service-client-id',
|
||||
SEGMENT_KEY: '',
|
||||
FEATURE_FLAGS: {},
|
||||
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
|
||||
SUPPORT_URL: 'http://localhost:18000/support',
|
||||
CONTACT_URL: 'http://localhost:18000/contact',
|
||||
OPEN_SOURCE_URL: 'http://localhost:18000/openedx',
|
||||
TERMS_OF_SERVICE_URL: 'http://localhost:18000/terms-of-service',
|
||||
PRIVACY_POLICY_URL: 'http://localhost:18000/privacy-policy',
|
||||
FACEBOOK_URL: 'https://www.facebook.com',
|
||||
TWITTER_URL: 'https://twitter.com',
|
||||
YOU_TUBE_URL: 'https://www.youtube.com',
|
||||
LINKED_IN_URL: 'https://www.linkedin.com',
|
||||
REDDIT_URL: 'https://www.reddit.com',
|
||||
APPLE_APP_STORE_URL: 'https://www.apple.com/ios/app-store/',
|
||||
GOOGLE_PLAY_URL: 'https://play.google.com/store',
|
||||
ENTERPRISE_MARKETING_URL: 'http://example.com',
|
||||
ENTERPRISE_MARKETING_UTM_SOURCE: 'example.com',
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN: 'example.com Referral',
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: 'Footer',
|
||||
LEARNING_BASE_URL: 'http://localhost:2000',
|
||||
SESSION_COOKIE_DOMAIN: 'localhost',
|
||||
HOTJAR_APP_ID: '',
|
||||
HOTJAR_VERSION: 6,
|
||||
HOTJAR_DEBUG: '',
|
||||
NEW_RELIC_APP_ID: '',
|
||||
NEW_RELIC_LICENSE_KEY: '',
|
||||
ACCOUNT_SETTINGS_URL: 'http://localhost:1997',
|
||||
ACCOUNT_PROFILE_URL: 'http://localhost:1995',
|
||||
CAREER_LINK_URL: '',
|
||||
EXPERIMENT_08_23_VAN_PAINTED_DOOR: true,
|
||||
SHOW_UNENROLL_SURVEY: true
|
||||
};
|
||||
@@ -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}
|
||||
63942
package-lock.json
generated
63942
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
102
package.json
102
package.json
@@ -1,24 +1,26 @@
|
||||
{
|
||||
"name": "@edx/frontend-app-learner-dash",
|
||||
"name": "@edx/frontend-app-learner-dashboard",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-learner-dash.git"
|
||||
"url": "git+https://github.com/edx/frontend-app-learner-dashboard.git"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"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",
|
||||
@@ -27,69 +29,49 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
|
||||
"@edx/browserslist-config": "^1.1.0",
|
||||
"@edx/frontend-component-footer": "11.3.1",
|
||||
"@edx/frontend-platform": "3.0.0",
|
||||
"@edx/paragon": "20.12.0",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^8.0.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",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"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.48.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",
|
||||
"react-intl": "^5.20.9",
|
||||
"react-pdf": "^5.5.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
"redux": "4.1.1",
|
||||
"redux-beacon": "^2.1.0",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"reselect": "^4.0.0",
|
||||
"util": "^0.12.4",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
"prop-types": "15.8.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "6.8.9",
|
||||
"react-router-dom": "6.30.3",
|
||||
"react-share": "^5.2.2",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"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",
|
||||
"codecov": "^3.8.3",
|
||||
"enzyme-adapter-react-16": "^1.15.6",
|
||||
"es-check": "^6.0.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: /
|
||||
76
src/App.jsx
76
src/App.jsx
@@ -1,47 +1,67 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { initializeHotjar } from '@edx/frontend-enterprise-hotjar';
|
||||
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
|
||||
import { thunkActions } from 'data/redux';
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
|
||||
import AppWrapper from 'containers/AppWrapper';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import { useMasquerade } from 'data/context';
|
||||
import messages from './messages';
|
||||
import './App.scss';
|
||||
|
||||
export const App = () => {
|
||||
const dispatch = useDispatch();
|
||||
// TODO: made development-only
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
const { formatMessage } = useIntl();
|
||||
const { masqueradeUser } = useMasquerade();
|
||||
const { data, isError } = useInitializeLearnerHome();
|
||||
const hasNetworkFailure = !masqueradeUser && isError;
|
||||
const supportEmail = data?.platformSettings?.supportEmail || undefined;
|
||||
|
||||
/* istanbul ignore next */
|
||||
React.useEffect(() => {
|
||||
if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') {
|
||||
window.loadEmptyData = () => {
|
||||
dispatch(thunkActions.app.loadData({ ...fakeData.globalData, courses: [] }));
|
||||
};
|
||||
window.loadMockData = () => {
|
||||
dispatch(thunkActions.app.loadData({
|
||||
...fakeData.globalData,
|
||||
courses: [
|
||||
...fakeData.courseRunData,
|
||||
...fakeData.entitlementData,
|
||||
],
|
||||
}));
|
||||
};
|
||||
}
|
||||
if (getConfig().HOTJAR_APP_ID) {
|
||||
try {
|
||||
initializeHotjar({
|
||||
hotjarId: getConfig().HOTJAR_APP_ID,
|
||||
hotjarVersion: getConfig().HOTJAR_VERSION,
|
||||
hotjarDebug: !!getConfig().HOTJAR_DEBUG,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<Router>
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages.pageTitle)}</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<div>
|
||||
<AppWrapper>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<main id="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} />
|
||||
</AppWrapper>
|
||||
</div>
|
||||
</Router>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
95
src/App.scss
95
src/App.scss
@@ -1,17 +1,100 @@
|
||||
// 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
|
||||
// Andal Learning Brand Colors - Override Paragon variables
|
||||
:root {
|
||||
--pgn-color-primary: #ff4f00;
|
||||
--pgn-color-primary-100: #ffe6cc;
|
||||
--pgn-color-primary-200: #ffcc99;
|
||||
--pgn-color-primary-300: #ffb366;
|
||||
--pgn-color-primary-400: #ff9933;
|
||||
--pgn-color-primary-500: #ff4f00;
|
||||
--pgn-color-primary-600: #cc3f00;
|
||||
--pgn-color-primary-700: #992f00;
|
||||
--pgn-color-primary-800: #661f00;
|
||||
--pgn-color-primary-900: #330f00;
|
||||
--pgn-color-action-primary: #ff4f00;
|
||||
--pgn-color-action-primary-hover: #cc3f00;
|
||||
--pgn-color-action-primary-focus: #992f00;
|
||||
--pgn-color-action-primary-active: #992f00;
|
||||
}
|
||||
|
||||
// Override all button variants to use Andal orange
|
||||
.btn-primary {
|
||||
background-color: #ff4f00 !important;
|
||||
border-color: #ff4f00 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #cc3f00 !important;
|
||||
border-color: #cc3f00 !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: #992f00 !important;
|
||||
border-color: #992f00 !important;
|
||||
box-shadow: 0 0 0 2px #ffcc99 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: #ff4f00 !important;
|
||||
border-color: #ff4f00 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #ff4f00 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px #ffcc99 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-brand {
|
||||
background-color: #ff4f00 !important;
|
||||
border-color: #ff4f00 !important;
|
||||
color: #fff !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #cc3f00 !important;
|
||||
border-color: #cc3f00 !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px #ffcc99 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Override link colors
|
||||
a {
|
||||
color: #ff4f00;
|
||||
|
||||
&:hover {
|
||||
color: #cc3f00;
|
||||
}
|
||||
}
|
||||
|
||||
// Override active states
|
||||
.active {
|
||||
background-color: #ff4f00 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
|
||||
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
|
||||
.alert .alert-icon {
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.alert.alert-info .alert-icon {
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
141
src/App.test.jsx
141
src/App.test.jsx
@@ -1,41 +1,132 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||
jest.mock('data/context', () => ({
|
||||
useMasquerade: jest.fn(() => ({ masqueradeUser: null })),
|
||||
}));
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
let router;
|
||||
jest.mock('@edx/frontend-component-footer', () => ({
|
||||
FooterSlot: jest.fn(() => <div>FooterSlot</div>),
|
||||
}));
|
||||
jest.mock('containers/Dashboard', () => jest.fn(() => <div>Dashboard</div>));
|
||||
jest.mock('containers/LearnerDashboardHeader', () => jest.fn(() => <div>LearnerDashboardHeader</div>));
|
||||
jest.mock('containers/AppWrapper', () => jest.fn(({ children }) => <div className="AppWrapper">{children}</div>));
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/react'),
|
||||
ErrorPage: () => 'ErrorPage',
|
||||
}));
|
||||
|
||||
const supportEmail = 'test@support.com';
|
||||
useInitializeLearnerHome.mockReturnValue({
|
||||
data: {
|
||||
platformSettings: {
|
||||
supportEmail,
|
||||
},
|
||||
},
|
||||
isError: false,
|
||||
});
|
||||
|
||||
describe('App router component', () => {
|
||||
test('snapshot: enabled', () => {
|
||||
expect(shallow(<App />)).toMatchSnapshot();
|
||||
});
|
||||
describe('component', () => {
|
||||
const runBasicTests = () => {
|
||||
it('displays title in helmet component', async () => {
|
||||
await waitFor(() => expect(document.title).toEqual(messages.pageTitle.defaultMessage));
|
||||
});
|
||||
it('displays learner dashboard header', () => {
|
||||
const learnerDashboardHeader = screen.getByText('LearnerDashboardHeader');
|
||||
expect(learnerDashboardHeader).toBeInTheDocument();
|
||||
});
|
||||
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');
|
||||
});
|
||||
it('displays footer slot', () => {
|
||||
const footerSlot = screen.getByText('FooterSlot');
|
||||
expect(footerSlot).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
describe('no network failure', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.find(BrowserRouter);
|
||||
jest.clearAllMocks();
|
||||
getConfig.mockReturnValue({});
|
||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('Routing - ListView is only route', () => {
|
||||
expect(router.find('main')).toEqual(shallow(
|
||||
<main><Dashboard /></main>,
|
||||
));
|
||||
runBasicTests();
|
||||
it('loads dashboard', () => {
|
||||
const dashboard = screen.getByText('Dashboard');
|
||||
expect(dashboard).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(router.find(Footer).props().logo).toEqual(logo);
|
||||
describe('no network failure with optimizely url', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
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();
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useInitializeLearnerHome.mockReturnValue({
|
||||
data: null,
|
||||
isError: true,
|
||||
});
|
||||
getConfig.mockReturnValue({});
|
||||
render(<IntlProvider locale="en" messages={messages}><App /></IntlProvider>);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads error page', () => {
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
const errorPage = screen.getByText('ErrorPage');
|
||||
expect(errorPage).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('refresh failure', () => {
|
||||
beforeEach(() => {
|
||||
getConfig.mockReturnValue({});
|
||||
render(<IntlProvider locale="en"><App /></IntlProvider>);
|
||||
});
|
||||
runBasicTests();
|
||||
it('loads error page', () => {
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
const errorPage = screen.getByText('ErrorPage');
|
||||
expect(errorPage).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot: enabled 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<LearnerDashboardHeader />
|
||||
<main>
|
||||
<Dashboard />
|
||||
</main>
|
||||
<Footer
|
||||
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
@@ -1,23 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<IntlProvider
|
||||
locale="en"
|
||||
>
|
||||
<AppProvider
|
||||
store={
|
||||
Object {
|
||||
"redux": "store",
|
||||
}
|
||||
}
|
||||
>
|
||||
<App />
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
`;
|
||||
17
src/assets/complete-logo.svg
Normal file
17
src/assets/complete-logo.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="518" height="87" viewBox="0 0 518 87" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512.654 43.4098C512.654 35.747 509.327 31.2325 503.564 31.2325C497.861 31.2325 494.474 35.747 494.474 43.4098C494.474 51.0726 497.861 55.5871 503.564 55.5871C509.327 55.5871 512.654 51.0726 512.654 43.4098ZM518 56.0029C518 67.111 513.188 72.3978 503.029 72.3978C499.286 72.3978 496.019 71.8632 492.87 70.6751V65.5072C496.019 67.1704 499.049 68.0021 502.198 68.0021C509.148 68.0021 512.654 64.3786 512.654 56.9534V54.3397C510.455 58.1414 507.01 60.0422 502.257 60.0422C498.277 60.0422 495.009 58.5572 492.573 55.5277C490.137 52.4982 488.949 48.4589 488.949 43.4098C488.949 38.3607 490.137 34.3807 492.573 31.3513C495.009 28.3218 498.277 26.7773 502.257 26.7773C507.01 26.7773 510.455 28.6782 512.654 32.4799V27.5496H518V56.0029Z" fill="black"/>
|
||||
<path d="M484.03 50.3004C484.149 55.7653 484.209 59.864 484.209 59.864L478.743 59.9234C478.743 59.9234 478.862 60.7551 478.683 51.0726C478.565 45.4294 478.565 40.6179 478.565 40.6179C478.565 34.4401 476.129 31.4107 471.376 31.4107C465.613 31.4107 462.049 35.3312 462.049 41.6871V60.0422H456.702C456.702 60.0422 456.702 46.7363 456.702 37.1726C456.702 32.5987 453.375 33.0145 453.375 33.0145V27.5496C453.375 27.5496 456.88 27.5496 458.841 29.688C460.564 31.5889 460.623 33.7273 460.623 33.7273C461.811 29.7474 466.802 26.7773 472.03 26.7773C479.456 26.7773 483.911 31.4701 483.911 40.4397C483.911 40.4397 483.911 41.5089 484.03 50.3004Z" fill="black"/>
|
||||
<path d="M443.611 27.5485H448.958C448.958 27.5485 448.661 42.8147 448.779 51.4873C448.779 55.5861 452.166 55.1702 452.166 55.1702V60.2194C452.166 60.2194 443.492 60.0412 443.611 52.2002C443.73 42.6365 443.611 27.5485 443.611 27.5485ZM446.225 14.3613C444.264 14.3613 442.66 15.9652 442.66 17.9254C442.66 19.8857 444.264 21.4895 446.225 21.4895C448.185 21.4895 449.789 19.9451 449.789 17.9254C449.789 15.9058 448.185 14.3613 446.225 14.3613Z" fill="black"/>
|
||||
<path d="M436.376 50.3004C436.495 55.7653 436.554 59.864 436.554 59.864L431.089 59.9234C431.089 59.9234 431.207 60.7551 431.029 51.0726C430.91 45.4294 430.91 40.6179 430.91 40.6179C430.91 34.4401 428.475 31.4107 423.722 31.4107C417.959 31.4107 414.395 35.3312 414.395 41.6871V60.0422H409.048C409.048 60.0422 409.048 46.7363 409.048 37.1726C409.048 32.5987 405.721 33.0145 405.721 33.0145V27.5496C405.721 27.5496 409.226 27.5496 411.186 29.688C412.909 31.5889 412.969 33.7273 412.969 33.7273C414.157 29.7474 419.147 26.7773 424.375 26.7773C431.801 26.7773 436.257 31.4701 436.257 40.4397C436.257 40.4397 436.257 41.5089 436.376 50.3004Z" fill="black"/>
|
||||
<path d="M403.931 32.4208C402.208 32.5396 400.961 32.5396 399 33.49C396.386 34.7969 393.237 37.7076 393.237 42.9349C393.237 49.8849 393.237 60.0426 393.237 60.0426H387.891V27.5499H393.237V32.599C395.257 29.5695 397.337 27.3717 401.911 27.1935C402.684 27.1341 402.446 27.1341 403.931 27.1935V32.4208Z" fill="black"/>
|
||||
<path d="M369.696 43.7068C365.359 43.7068 362.388 44.182 360.725 45.1918C359.061 46.2017 358.23 47.8649 358.23 50.241C358.23 54.0427 360.903 56.4187 365.181 56.4187C371.122 56.4187 374.983 51.8448 374.983 44.8948V43.7068H369.696ZM380.271 51.9636C380.271 56.1811 384.251 55.8247 384.251 55.8247V59.9234C384.251 59.9234 381.221 60.2799 379.142 58.6166C377.241 57.1316 376.112 54.9337 376.112 54.9337C373.676 58.9136 369.042 60.8739 363.755 60.8739C357.101 60.8739 352.883 56.8346 352.883 50.5974C352.883 43.291 357.814 39.5487 367.498 39.5487H374.983V39.0141C374.983 34.0837 372.191 31.2919 366.309 31.2919C363.577 31.2919 360.071 33.3709 360.19 36.935L355.259 37.0538C355.378 28.6782 362.923 26.7773 366.428 26.7773C375.577 26.7773 380.33 31.5889 380.33 41.5089C380.33 41.5089 380.211 47.8055 380.271 51.9636Z" fill="black"/>
|
||||
<path d="M349.423 42.4594V45.073H324.887C325.362 52.4388 329.402 56.3593 336.472 56.3593C340.571 56.3593 344.374 55.4089 348.176 53.3299V58.379C344.374 60.0422 340.274 60.8739 336.175 60.8739C331.006 60.8739 326.848 59.3888 323.818 56.3593C320.788 53.3299 319.303 49.2906 319.303 44.1226C319.303 38.8359 320.729 34.6184 323.58 31.4701C326.491 28.3218 330.353 26.7773 335.224 26.7773C339.621 26.7773 343.067 28.203 345.621 30.9949C348.176 33.7867 349.423 37.6478 349.423 42.4594ZM344.076 40.9149C344.017 35.0342 340.571 31.2919 335.284 31.2919C329.343 31.2919 325.6 34.7966 325.065 40.9149H344.076Z" fill="black"/>
|
||||
<path d="M308.35 14.897H313.696C313.696 14.897 313.875 38.4794 313.875 51.1914C313.875 55.4089 319.459 55.3495 319.459 55.3495L319.4 60.161C319.4 60.161 313.578 60.3392 310.488 56.0029C309.003 53.9832 308.35 51.726 308.35 48.7559C308.35 35.3905 308.35 14.897 308.35 14.897Z" fill="black"/>
|
||||
<path d="M279.439 14.897H284.786C284.786 14.897 284.965 38.4794 284.965 51.1914C284.965 55.4089 290.549 55.3495 290.549 55.3495L290.49 60.161C290.49 60.161 284.667 60.3392 281.578 56.0029C280.093 53.9832 279.439 51.726 279.439 48.7559C279.439 35.3905 279.439 14.897 279.439 14.897Z" fill="black"/>
|
||||
<path d="M261.065 43.7068C256.728 43.7068 253.757 44.182 252.094 45.1918C250.431 46.2017 249.599 47.8649 249.599 50.241C249.599 54.0427 252.272 56.4187 256.55 56.4187C262.491 56.4187 266.352 51.8448 266.352 44.8948V43.7068H261.065ZM271.64 51.9636C271.64 56.1811 275.62 55.8247 275.62 55.8247V59.9234C275.62 59.9234 272.59 60.2799 270.511 58.6166C268.61 57.1316 267.481 54.9337 267.481 54.9337C265.045 58.9136 260.411 60.8739 255.124 60.8739C248.47 60.8739 244.252 56.8346 244.252 50.5974C244.252 43.291 249.183 39.5487 258.867 39.5487H266.352V39.0141C266.352 34.0837 263.56 31.2919 257.678 31.2919C254.946 31.2919 251.44 33.3709 251.559 36.935L246.628 37.0538C246.747 28.6782 254.292 26.7773 257.797 26.7773C266.946 26.7773 271.699 31.5889 271.699 41.5089C271.699 41.5089 271.58 47.8055 271.64 51.9636Z" fill="black"/>
|
||||
<path d="M233.216 32.4799V14.897H238.563C238.563 14.897 238.682 38.42 238.682 50.8943C238.682 55.2901 238.682 59.864 238.682 59.864L233.81 59.9234L233.87 55.4089C231.671 59.27 227.572 60.8738 222.819 60.8738C218.898 60.8738 215.69 59.3294 213.195 56.1811C210.759 53.0328 209.512 48.9341 209.512 43.8256C209.512 38.717 210.759 34.6183 213.195 31.47C215.69 28.3217 218.898 26.7773 222.819 26.7773C227.572 26.7773 231.018 28.6187 233.216 32.4799ZM215.037 43.8256C215.037 51.6666 218.423 56.4187 224.126 56.4187C229.77 56.4187 233.216 51.6666 233.216 43.8256C233.216 35.9845 229.77 31.2324 224.126 31.2324C218.423 31.2324 215.037 35.9845 215.037 43.8256Z" fill="black"/>
|
||||
<path d="M204.591 50.3004C204.71 55.7653 204.769 59.864 204.769 59.864L199.303 59.9234C199.303 59.9234 199.422 60.7551 199.244 51.0726C199.125 45.4294 199.125 40.6179 199.125 40.6179C199.125 34.4401 196.689 31.4107 191.937 31.4107C186.174 31.4107 182.609 35.3312 182.609 41.6871V60.0422H177.262C177.262 60.0422 177.262 46.7363 177.262 37.1726C177.262 32.5987 173.936 33.0145 173.936 33.0145V27.5496C173.936 27.5496 177.441 27.5496 179.401 29.688C181.124 31.5889 181.183 33.7273 181.183 33.7273C182.372 29.7474 187.362 26.7773 192.59 26.7773C200.016 26.7773 204.472 31.4701 204.472 40.4397C204.472 40.4397 204.472 41.5089 204.591 50.3004Z" fill="black"/>
|
||||
<path d="M159.067 43.7068C154.73 43.7068 151.759 44.182 150.096 45.1918C148.432 46.2017 147.601 47.8649 147.601 50.241C147.601 54.0427 150.274 56.4187 154.552 56.4187C160.493 56.4187 164.354 51.8448 164.354 44.8948V43.7068H159.067ZM169.642 51.9636C169.642 56.1811 173.622 55.8247 173.622 55.8247V59.9234C173.622 59.9234 170.592 60.2799 168.513 58.6166C166.612 57.1316 165.483 54.9337 165.483 54.9337C163.047 58.9136 158.413 60.8739 153.126 60.8739C146.472 60.8739 142.254 56.8346 142.254 50.5974C142.254 43.291 147.185 39.5487 156.869 39.5487H164.354V39.0141C164.354 34.0837 161.562 31.2919 155.68 31.2919C152.948 31.2919 149.442 33.3709 149.561 36.935L144.63 37.0538C144.749 28.6782 152.294 26.7773 155.799 26.7773C164.948 26.7773 169.701 31.5889 169.701 41.5089C169.701 41.5089 169.582 47.8055 169.642 51.9636Z" fill="black"/>
|
||||
<path d="M8.40549 86.8917C3.76365 86.8917 0 83.0908 0 78.4008V55.9119C0 51.2229 3.76265 47.421 8.40549 47.421H121.955C121.91 52.4182 121.875 57.4144 121.851 62.4106C121.821 68.5717 121.808 74.7338 121.81 80.8959C124.658 80.8726 127.506 80.8482 130.355 80.8249V86.8907L8.40549 86.8917Z" fill="#FF4F00"/>
|
||||
<path d="M124.912 40.1348H9.02975C4.08481 40.1348 0.0752715 36.3339 0.0752715 31.6439V8.49088C0.0752715 3.80189 4.08481 0 9.02975 0H130.282C130.264 1.47615 130.246 2.95331 130.229 4.42945C129.018 4.43249 127.808 4.43554 126.596 4.43858C125.745 8.73218 124.895 13.0268 124.045 17.3204C122.613 23.2077 121.182 29.0951 119.749 34.9814H124.935C124.927 36.6989 124.919 38.4173 124.912 40.1348Z" fill="#FF4F00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 45 KiB |
BIN
src/assets/verified-ribbon.png
Normal file
BIN
src/assets/verified-ribbon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 498 B |
@@ -1,22 +1,26 @@
|
||||
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 }) => (
|
||||
<Alert variant={variant} className="mb-0" icon={icon}>
|
||||
export const Banner = ({
|
||||
children, variant, icon, className,
|
||||
}) => (
|
||||
<Alert variant={variant} className={className} icon={icon}>
|
||||
{children}
|
||||
</Alert>
|
||||
);
|
||||
Banner.defaultProps = {
|
||||
icon: Info,
|
||||
variant: 'info',
|
||||
className: 'mb-0',
|
||||
};
|
||||
Banner.propTypes = {
|
||||
variant: PropTypes.string,
|
||||
icon: PropTypes.func,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Banner;
|
||||
|
||||
@@ -1,23 +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();
|
||||
describe('Banner component', () => {
|
||||
it('renders children content', () => {
|
||||
render(<Banner>Test content</Banner>);
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument();
|
||||
});
|
||||
test('renders with variants', () => {
|
||||
const wrapper = shallow(<Banner {...props} variant="success" />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(wrapper.find(Alert).prop('variant')).toEqual('success');
|
||||
it('uses default props correctly', () => {
|
||||
render(<Banner>Test content</Banner>);
|
||||
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,21 +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 variants 1`] = `
|
||||
<Alert
|
||||
className="mb-0"
|
||||
icon={[MockFunction icons.Info]}
|
||||
variant="success"
|
||||
>
|
||||
Hello, world!
|
||||
</Alert>
|
||||
`;
|
||||
@@ -1,15 +1,27 @@
|
||||
const configuration = {
|
||||
// BASE_URL: process.env.BASE_URL,
|
||||
APP_ID: process.env.APP_ID,
|
||||
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,
|
||||
// REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
|
||||
// DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
|
||||
// SECURE_COOKIES: process.env.NODE_ENV !== 'development',
|
||||
// SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
SEGMENT_KEY: process.env.SEGMENT_KEY,
|
||||
// ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
|
||||
LEARNING_MICROFRONTEND_URL: process.env.LEARNING_MICROFRONTEND_URL,
|
||||
LEARNING_BASE_URL: process.env.LEARNING_BASE_URL,
|
||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN || '',
|
||||
SUPPORT_URL: process.env.SUPPORT_URL || 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',
|
||||
SHOW_UNENROLL_SURVEY: process.env.SHOW_UNENROLL_SURVEY === '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;
|
||||
15
src/containers/AppWrapper/index.test.tsx
Normal file
15
src/containers/AppWrapper/index.test.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import AppWrapper from './index';
|
||||
|
||||
describe('AppWrapper', () => {
|
||||
it('should render children without modification', () => {
|
||||
render(
|
||||
<AppWrapper>
|
||||
<div>Test Child</div>
|
||||
</AppWrapper>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Child')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,73 @@
|
||||
@import "@edx/paragon/scss/core/core";
|
||||
|
||||
.course-card {
|
||||
.card {
|
||||
.pgn__card-wrapper-image-cap.vertical {
|
||||
display: flex;
|
||||
min-height: var(--pgn-size-card-image-vertical-max-height);
|
||||
}
|
||||
.pgn__card-image-cap {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.overflow-visible {
|
||||
overflow: visible;
|
||||
}
|
||||
.pgn__card-header-content {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.pgn__card-footer {
|
||||
flex-wrap: nowrap;
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pgn__action-row {
|
||||
align-self: flex-end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.course-card-verify-ribbon-container {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
|
||||
.badge {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
> img {
|
||||
width: 40px;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-card-banners {
|
||||
> .alert {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
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: var(--pgn-size-alert-border-radius);
|
||||
border-bottom-right-radius: var(--pgn-size-alert-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
.related-programs-banner {
|
||||
.related-programs-list-container {
|
||||
list-style: none;
|
||||
display: inline;
|
||||
|
||||
> li {
|
||||
line-height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +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>
|
||||
<CourseCardContent
|
||||
cardId="test-card-id"
|
||||
orientation="vertical"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="course-card-banners"
|
||||
data-testid="CourseCardBanners"
|
||||
>
|
||||
<CourseCardBanners
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
<CourseCardContent
|
||||
cardId="test-card-id"
|
||||
orientation="horizontal"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="course-card-banners"
|
||||
data-testid="CourseCardBanners"
|
||||
>
|
||||
<CourseCardBanners
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { useWindowSize, breakpoints } from '@openedx/paragon';
|
||||
|
||||
export const useIsCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
return width < breakpoints.medium.maxWidth && width > breakpoints.small.maxWidth;
|
||||
};
|
||||
|
||||
export default useIsCollapsed;
|
||||
@@ -0,0 +1,48 @@
|
||||
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
|
||||
expect(
|
||||
(breakpoints.large.maxWidth - 1)
|
||||
> (breakpoints.medium.maxWidth - 1)
|
||||
&& (breakpoints.medium.maxWidth - 1)
|
||||
> (breakpoints.small.maxWidth - 1),
|
||||
).toBe(true);
|
||||
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.large.maxWidth - 1 });
|
||||
expect(useIsCollapsed()).toEqual(false);
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.medium.maxWidth - 1 });
|
||||
expect(useIsCollapsed()).toEqual(true);
|
||||
useWindowSize.mockReturnValue({ width: breakpoints.small.maxWidth - 1 });
|
||||
expect(useIsCollapsed()).toEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
import useIsCollapsed from './hooks';
|
||||
|
||||
export const ActionButton = (props) => {
|
||||
const isSmall = useIsCollapsed();
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
{...isSmall && { size: 'sm' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ActionButton from '.';
|
||||
|
||||
import useIsCollapsed from './hooks';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
describe('ActionButton', () => {
|
||||
const props = {
|
||||
className: 'custom-class',
|
||||
children: 'Test',
|
||||
};
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,42 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import track from 'tracking';
|
||||
import { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const BeginCourseButton = ({ cardId }) => {
|
||||
const { homeUrl } = hooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = hooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const { data: learnerData } = useInitializeLearnerHome();
|
||||
const courseData = useCourseData(cardId);
|
||||
const homeUrl = courseData?.courseRun?.homeUrl;
|
||||
const execEdTrackingParam = useMemo(() => {
|
||||
const isExecEd2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(courseData.enrollment.mode);
|
||||
const { authOrgId } = learnerData.enterpriseDashboard || {};
|
||||
return isExecEd2UCourse ? `?org_id=${authOrgId}` : '';
|
||||
}, [courseData.enrollment.mode, learnerData.enterpriseDashboard]);
|
||||
const { disableBeginCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = useCourseTrackingEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl + execEdTrackingParam,
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
disabled={isMasquerading || !hasAccess}
|
||||
<ActionButton
|
||||
disabled={disableBeginCourse}
|
||||
as="a"
|
||||
href={homeUrl}
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{formatMessage(messages.beginCourse)}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
BeginCourseButton.propTypes = {
|
||||
|
||||
@@ -1,54 +1,90 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { hooks } from 'data/redux';
|
||||
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 { useCourseData, useCourseTrackingEvent } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ homeUrl: 'home-url' })),
|
||||
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn().mockReturnValue({
|
||||
enrollment: { mode: 'executive-education' },
|
||||
courseRun: { homeUrl: 'home-url' },
|
||||
}),
|
||||
useCourseTrackingEvent: jest.fn().mockReturnValue({
|
||||
trackCourseEvent: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||
data: {
|
||||
enterpriseDashboard: {
|
||||
authOrgId: 'test-org-id',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
let wrapper;
|
||||
const { homeUrl } = hooks.useCardCourseRunData();
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableBeginCourse: false })));
|
||||
|
||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||
|
||||
const homeUrl = 'home-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.href)).toEqual(homeUrl);
|
||||
describe('initiliaze hooks', () => {
|
||||
it('initializes course run data with cardId', () => {
|
||||
renderComponent();
|
||||
expect(useCourseData).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(hooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
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');
|
||||
});
|
||||
it('initializes enrollment data with cardId', () => {
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(hooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('learner does not have access', () => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
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');
|
||||
});
|
||||
test('masquerading', () => {
|
||||
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<BeginCourseButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
it('should track enter course clicked event on click, with exec ed param', () => {
|
||||
renderComponent();
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button', { name: 'Begin Course' });
|
||||
user.click(button);
|
||||
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
`${homeUrl}?org_id=test-org-id`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,42 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import { EXECUTIVE_EDUCATION_COURSE_MODES } from 'data/constants/course';
|
||||
import track from 'tracking';
|
||||
import { useCourseTrackingEvent, useCourseData } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const ResumeButton = ({ cardId }) => {
|
||||
const { resumeUrl } = hooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess, isAudit, isAuditAccessExpired } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = hooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const { data: learnerData } = useInitializeLearnerHome();
|
||||
const courseData = useCourseData(cardId);
|
||||
const resumeUrl = courseData?.courseRun?.resumeUrl;
|
||||
const execEdTrackingParam = useMemo(() => {
|
||||
const isExecEd2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(courseData.enrollment.mode);
|
||||
const { authOrgId } = learnerData.enterpriseDashboard || {};
|
||||
return isExecEd2UCourse ? `?org_id=${authOrgId}` : '';
|
||||
}, [courseData.enrollment.mode, learnerData.enterpriseDashboard]);
|
||||
const { disableResumeCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = useCourseTrackingEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
resumeUrl + execEdTrackingParam,
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
disabled={isMasquerading || !hasAccess || (isAudit && isAuditAccessExpired)}
|
||||
<ActionButton
|
||||
disabled={disableResumeCourse}
|
||||
as="a"
|
||||
href={resumeUrl}
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{formatMessage(messages.resume)}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
ResumeButton.propTypes = {
|
||||
|
||||
@@ -1,67 +1,91 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useCourseTrackingEvent, useCourseData } from 'hooks';
|
||||
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { hooks } from 'data/redux';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ResumeButton from './ResumeButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseRunData: jest.fn(() => ({ resumeUrl: 'resumeUrl' })),
|
||||
useCardEnrollmentData: jest.fn(() => ({
|
||||
hasAccess: true,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: false,
|
||||
})),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
const authOrgId = 'auth-org-id';
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||
data: {
|
||||
enterpriseDashboard: {
|
||||
authOrgId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn().mockReturnValue({
|
||||
enrollment: { mode: 'executive-education' },
|
||||
courseRun: { homeUrl: 'home-url' },
|
||||
}),
|
||||
useCourseTrackingEvent: jest.fn().mockReturnValue({
|
||||
trackCourseEvent: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
const { resumeUrl } = hooks.useCardCourseRunData();
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableResumeCourse: false })));
|
||||
|
||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||
|
||||
useCourseData.mockReturnValue({
|
||||
enrollment: { mode: 'executive-education' },
|
||||
courseRun: { resumeUrl: 'home-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.href)).toEqual(resumeUrl);
|
||||
describe('initialize hooks', () => {
|
||||
beforeEach(() => render(<IntlProvider locale="en"><ResumeButton {...props} /></IntlProvider>));
|
||||
it('initializes course run data with cardId', () => {
|
||||
expect(useCourseData).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(hooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId);
|
||||
describe('disabled', () => {
|
||||
beforeEach(() => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableResumeCourse: true });
|
||||
});
|
||||
it('initializes course enrollment data based on cardId', () => {
|
||||
shallow(<ResumeButton {...props} />);
|
||||
expect(hooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId);
|
||||
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');
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('masquerading', () => {
|
||||
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('learner does not have access', () => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
hasAccess: false,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: false,
|
||||
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');
|
||||
});
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('audit access expired', () => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
hasAccess: true,
|
||||
isAudit: true,
|
||||
isAuditAccessExpired: true,
|
||||
});
|
||||
const wrapper = shallow(<ResumeButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(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(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||
track.course.enterCourseClicked,
|
||||
props.cardId,
|
||||
`home-url?org_id=${authOrgId}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import { useSelectSessionModal } from 'data/context';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const SelectSessionButton = ({ cardId }) => {
|
||||
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
|
||||
const { canChange, hasSessions } = hooks.useCardEntitlementData(cardId);
|
||||
const { isMasquerading } = hooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const openSessionModal = hooks.useUpdateSelectSessionModalCallback(dispatch, cardId);
|
||||
const { disableSelectSession } = useActionDisabledState(cardId);
|
||||
const { updateSelectSessionModal } = useSelectSessionModal();
|
||||
return (
|
||||
<Button
|
||||
disabled={isMasquerading || !hasAccess || (!canChange || !hasSessions)}
|
||||
onClick={openSessionModal}
|
||||
<ActionButton
|
||||
disabled={disableSelectSession}
|
||||
onClick={() => updateSelectSessionModal(cardId)}
|
||||
>
|
||||
{formatMessage(messages.resume)}
|
||||
</Button>
|
||||
{formatMessage(messages.selectSession)}
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
SelectSessionButton.propTypes = {
|
||||
|
||||
@@ -1,66 +1,47 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useSelectSessionModal } from 'data/context';
|
||||
|
||||
import useActionDisabledState from '../hooks';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })),
|
||||
useCardEntitlementData: jest.fn(() => ({ canChange: true, hasSessions: true })),
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useUpdateSelectSessionModalCallback: () => jest.fn().mockName('mockOpenSessionModal'),
|
||||
},
|
||||
jest.mock('data/context', () => ({
|
||||
useSelectSessionModal: jest.fn().mockReturnValue({
|
||||
updateSelectSessionModal: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
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', () => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it('renders disabled button if masquerading', () => {
|
||||
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('default render', () => {
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.onClick).getMockName())
|
||||
.toEqual(hooks.useUpdateSelectSessionModalCallback().getMockName());
|
||||
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: 'Select Session' });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
describe('disabled states', () => {
|
||||
test('learner does not have access', () => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
describe('if useActionDisabledState is false', () => {
|
||||
it('should disabled Select Session', () => {
|
||||
useActionDisabledState.mockReturnValueOnce({ disableSelectSession: true });
|
||||
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
|
||||
const button = screen.getByRole('button', { name: 'Select Session' });
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
test('learner cannot change sessions', () => {
|
||||
hooks.useCardEntitlementData.mockReturnValueOnce({ canChange: false, hasSessions: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('entitlement does not have available sessions', () => {
|
||||
hooks.useCardEntitlementData.mockReturnValueOnce({ canChange: true, hasSessions: false });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('user is masquerading', () => {
|
||||
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
wrapper = shallow(<SelectSessionButton {...props} />);
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
describe('on click', () => {
|
||||
it('should call openSessionModal', async () => {
|
||||
const mockedUpdateSelectSessionModal = jest.fn();
|
||||
useSelectSessionModal.mockReturnValue({
|
||||
updateSelectSessionModal: mockedUpdateSelectSessionModal,
|
||||
});
|
||||
render(<IntlProvider locale="en"><SelectSessionButton {...props} /></IntlProvider>);
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button', { name: 'Select Session' });
|
||||
await user.click(button);
|
||||
expect(mockedUpdateSelectSessionModal).toHaveBeenCalledWith(props.cardId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Locked } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import messages from './messages';
|
||||
|
||||
export const UpgradeButton = ({ cardId }) => {
|
||||
const { upgradeUrl } = hooks.useCardCourseRunData(cardId);
|
||||
const { canUpgrade } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isMasquerading } = hooks.useMasqueradeData();
|
||||
const { formatMessage } = useIntl();
|
||||
const isEnabled = (!isMasquerading && canUpgrade);
|
||||
return (
|
||||
<Button
|
||||
iconBefore={Locked}
|
||||
variant="outline-primary"
|
||||
disabled={!isEnabled}
|
||||
{...isEnabled && { as: 'a', href: upgradeUrl }}
|
||||
>
|
||||
{formatMessage(messages.upgrade)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
UpgradeButton.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
export default UpgradeButton;
|
||||
@@ -1,40 +0,0 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { hooks } from 'data/redux';
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useMasqueradeData: jest.fn(() => ({ isMasquerading: false })),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('UpgradeButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
const upgradeUrl = 'upgradeUrl';
|
||||
hooks.useCardCourseRunData.mockReturnValue({ upgradeUrl });
|
||||
describe('snapshot', () => {
|
||||
test('can upgrade', () => {
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
});
|
||||
test('cannot upgrade', () => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
test('masquerading', () => {
|
||||
hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true });
|
||||
const wrapper = shallow(<UpgradeButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import track from 'tracking';
|
||||
import { useCourseTrackingEvent, useCourseData } from 'hooks';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ActionButton from './ActionButton';
|
||||
import messages from './messages';
|
||||
|
||||
export const ViewCourseButton = ({ cardId }) => {
|
||||
const { homeUrl } = hooks.useCardCourseRunData(cardId);
|
||||
const { hasAccess } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isEntitlement, isExpired } = hooks.useCardEntitlementData(cardId);
|
||||
const { formatMessage } = useIntl();
|
||||
const courseData = useCourseData(cardId);
|
||||
const homeUrl = courseData?.courseRun?.homeUrl;
|
||||
const { disableViewCourse } = useActionDisabledState(cardId);
|
||||
|
||||
const handleClick = useCourseTrackingEvent(
|
||||
track.course.enterCourseClicked,
|
||||
cardId,
|
||||
homeUrl,
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
disabled={!hasAccess || (isEntitlement && isExpired)}
|
||||
<ActionButton
|
||||
disabled={disableViewCourse}
|
||||
as="a"
|
||||
href={homeUrl}
|
||||
href="#"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{formatMessage(messages.viewCourse)}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
ViewCourseButton.propTypes = {
|
||||
|
||||
@@ -1,58 +1,61 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useCourseTrackingEvent } from 'hooks';
|
||||
|
||||
import { htmlProps } from 'data/constants/htmlKeys';
|
||||
import { hooks } from 'data/redux';
|
||||
import track from 'tracking';
|
||||
import useActionDisabledState from '../hooks';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn().mockReturnValue({
|
||||
courseRun: { homeUrl: 'homeUrl' },
|
||||
}),
|
||||
useCourseTrackingEvent: jest.fn().mockReturnValue({
|
||||
trackCourseEvent: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
course: {
|
||||
enterCourseClicked: jest.fn().mockName('segment.enterCourseClicked'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => jest.fn(() => ({ disableViewCourse: false })));
|
||||
|
||||
jest.mock('./ActionButton/hooks', () => jest.fn(() => false));
|
||||
|
||||
const defaultProps = { cardId: 'cardId' };
|
||||
const homeUrl = 'homeUrl';
|
||||
|
||||
describe('ViewCourseButton', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
const homeUrl = 'homeUrl';
|
||||
hooks.useCardCourseRunData.mockReturnValue({ homeUrl });
|
||||
const createWrapper = ({
|
||||
hasAccess = false,
|
||||
isEntitlement = false,
|
||||
isExpired = false,
|
||||
}) => {
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess });
|
||||
hooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired });
|
||||
return shallow(<ViewCourseButton {...props} />);
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('default button', () => {
|
||||
const wrapper = createWrapper({ hasAccess: true });
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(false);
|
||||
expect(wrapper.prop(htmlProps.href)).toEqual(homeUrl);
|
||||
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');
|
||||
});
|
||||
test('disabled button', () => {
|
||||
const wrapper = createWrapper({});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.prop(htmlProps.disabled)).toEqual(true);
|
||||
expect(wrapper.prop(htmlProps.href)).toEqual(homeUrl);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('disabled button without access', () => {
|
||||
const wrapper = createWrapper({ hasAccess: false, isEntitlement: false, isExpired: false });
|
||||
expect(wrapper.prop('disabled')).toEqual(true);
|
||||
});
|
||||
it('disabled button with access', () => {
|
||||
const wrapper = createWrapper({ hasAccess: true, isEntitlement: true, isExpired: true });
|
||||
expect(wrapper.prop('disabled')).toEqual(true);
|
||||
});
|
||||
it('enabled button', () => {
|
||||
const wrapper = createWrapper({ hasAccess: true, isEntitlement: false, isExpired: false });
|
||||
expect(wrapper.prop('disabled')).toEqual(false);
|
||||
it('calls trackCourseEvent on click', async () => {
|
||||
const mockedTrackCourseEvent = jest.fn();
|
||||
useCourseTrackingEvent.mockReturnValue(mockedTrackCourseEvent);
|
||||
render(<IntlProvider locale="en"><ViewCourseButton {...defaultProps} /></IntlProvider>);
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button', { name: 'View Course' });
|
||||
await user.click(button);
|
||||
expect(useCourseTrackingEvent).toHaveBeenCalledWith(
|
||||
track.course.enterCourseClicked,
|
||||
defaultProps.cardId,
|
||||
homeUrl,
|
||||
);
|
||||
expect(mockedTrackCourseEvent).toHaveBeenCalled();
|
||||
});
|
||||
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,11 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BeginCourseButton snapshot renders default button when learner has access to the course 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="home-url"
|
||||
>
|
||||
Begin Course
|
||||
</Button>
|
||||
`;
|
||||
@@ -1,11 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResumeButton snapshot renders default button when learner has access to the course 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="resumeUrl"
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
`;
|
||||
@@ -1,28 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectSessionButton snapshot renders default button 1`] = `
|
||||
<Button
|
||||
disabled={false}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionButton snapshot renders disabled button if masquerading 1`] = `
|
||||
<Button
|
||||
disabled={true}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`SelectSessionButton snapshot renders disabled button when user does not have access to the course 1`] = `
|
||||
<Button
|
||||
disabled={true}
|
||||
onClick={[MockFunction mockOpenSessionModal]}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
`;
|
||||
@@ -1,33 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UpgradeButton snapshot can upgrade 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="upgradeUrl"
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`UpgradeButton snapshot cannot upgrade 1`] = `
|
||||
<Button
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`UpgradeButton snapshot masquerading 1`] = `
|
||||
<Button
|
||||
disabled={true}
|
||||
iconBefore={[MockFunction icons.Locked]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
`;
|
||||
@@ -1,21 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ViewCourseButton snapshot default button 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
disabled={false}
|
||||
href="homeUrl"
|
||||
>
|
||||
View Course
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`ViewCourseButton snapshot disabled button 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
disabled={true}
|
||||
href="homeUrl"
|
||||
>
|
||||
View Course
|
||||
</Button>
|
||||
`;
|
||||
@@ -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,33 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ActionRow } from '@edx/paragon';
|
||||
import { ActionRow } from '@openedx/paragon';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import { useCourseData, useEntitlementInfo } from 'hooks';
|
||||
|
||||
import UpgradeButton from './UpgradeButton';
|
||||
import CourseCardActionSlot from 'plugin-slots/CourseCardActionSlot';
|
||||
import SelectSessionButton from './SelectSessionButton';
|
||||
import BeginCourseButton from './BeginCourseButton';
|
||||
import ResumeButton from './ResumeButton';
|
||||
import ViewCourseButton from './ViewCourseButton';
|
||||
|
||||
export const CourseCardActions = ({ cardId }) => {
|
||||
const { isEntitlement, isFulfilled } = hooks.useCardEntitlementData(cardId);
|
||||
const { isVerified, hasStarted } = hooks.useCardEnrollmentData(cardId);
|
||||
const { isArchived } = hooks.useCardCourseRunData(cardId);
|
||||
let PrimaryButton;
|
||||
if (isEntitlement) {
|
||||
PrimaryButton = isFulfilled ? ViewCourseButton : SelectSessionButton;
|
||||
} else if (isArchived) {
|
||||
PrimaryButton = ViewCourseButton;
|
||||
} else {
|
||||
PrimaryButton = hasStarted ? ResumeButton : BeginCourseButton;
|
||||
}
|
||||
const cardData = useCourseData(cardId);
|
||||
const hasStarted = cardData.enrollment.hasStarted || false;
|
||||
const { isEntitlement, isFulfilled } = useEntitlementInfo(cardData);
|
||||
const isArchived = cardData.courseRun.isArchived || false;
|
||||
|
||||
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,103 +1,86 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useCourseData } from 'hooks';
|
||||
import CourseCardActions from '.';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
},
|
||||
jest.mock('hooks', () => ({
|
||||
...jest.requireActual('hooks'),
|
||||
useCourseData: 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 mockHooks = ({
|
||||
isEntitlement = false,
|
||||
isFulfilled = false,
|
||||
isArchived = false,
|
||||
hasStarted = false,
|
||||
} = {}) => {
|
||||
useCourseData.mockReturnValueOnce({
|
||||
enrollment: { hasStarted },
|
||||
courseRun: { isArchived },
|
||||
entitlement: isEntitlement !== null ? { isEntitlement, isFulfilled } : null,
|
||||
});
|
||||
};
|
||||
const createWrapper = ({
|
||||
isEntitlement, isFulfilled, isArchived, isVerified, hasStarted,
|
||||
}) => {
|
||||
hooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isFulfilled });
|
||||
hooks.useCardCourseRunData.mockReturnValueOnce({ isArchived });
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ isVerified, hasStarted });
|
||||
return shallow(<CourseCardActions {...props} />);
|
||||
};
|
||||
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 hooks', () => {
|
||||
mockHooks();
|
||||
renderComponent();
|
||||
expect(useCourseData).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();
|
||||
});
|
||||
expect(wrapper.find('UpgradeButton')).toHaveLength(1);
|
||||
it('renders SelectSessionButton if not fulfilled', () => {
|
||||
mockHooks({ isEntitlement: true });
|
||||
renderComponent();
|
||||
const SelectSessionButton = screen.getByText('SelectSessionButton');
|
||||
expect(SelectSessionButton).toBeInTheDocument();
|
||||
});
|
||||
it('show select session button when not verified and entitlement', () => {
|
||||
const wrapper = createWrapper({
|
||||
isEntitlement: true, isFulfilled: false, isArchived: false, isVerified: false, hasStarted: false,
|
||||
});
|
||||
expect(wrapper.find('SelectSessionButton')).toHaveLength(1);
|
||||
describe('not entitlement, verified, or exec ed', () => {
|
||||
it('renders CourseCardActionSlot and ViewCourseButton for archived courses', () => {
|
||||
mockHooks({ isArchived: true, isEntitlement: null });
|
||||
renderComponent();
|
||||
const CourseCardActionSlot = screen.getByText('CourseCardActionSlot');
|
||||
expect(CourseCardActionSlot).toBeInTheDocument();
|
||||
const ViewCourseButton = screen.getByText('ViewCourseButton');
|
||||
expect(ViewCourseButton).toBeInTheDocument();
|
||||
});
|
||||
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({ isEntitlement: null });
|
||||
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, isEntitlement: null });
|
||||
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',
|
||||
|
||||
@@ -1,38 +1,72 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import React, { useMemo } 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 { baseAppUrl } from 'data/services/lms/urls';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import { utilHooks, useCourseData } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const CertificateBanner = ({ cardId }) => {
|
||||
const certificate = appHooks.useCardCertificateData(cardId);
|
||||
const {
|
||||
isAudit,
|
||||
isVerified,
|
||||
hasFinished,
|
||||
} = appHooks.useCardEnrollmentData(cardId);
|
||||
const { isPassing } = appHooks.useCardGradeData(cardId);
|
||||
const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(cardId);
|
||||
const { supportEmail, billingEmail } = appHooks.usePlatformSettingsData();
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
const { useFormatDate } = utilHooks;
|
||||
|
||||
const emailLink = address => address && <MailtoLink to={address}>{address}</MailtoLink>;
|
||||
export const CertificateBanner = ({ cardId }) => {
|
||||
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||
const courseData = useCourseData(cardId);
|
||||
const {
|
||||
certificate = {},
|
||||
isVerified = false,
|
||||
isAudit = false,
|
||||
isPassing = false,
|
||||
isArchived = false,
|
||||
minPassingGrade = 0,
|
||||
progressUrl = '',
|
||||
} = useMemo(() => ({
|
||||
isVerified: courseData?.enrollment?.isVerified,
|
||||
isAudit: courseData?.enrollment?.isAudit,
|
||||
certificate: courseData?.certificate || {},
|
||||
isPassing: courseData?.gradeData?.isPassing,
|
||||
isArchived: courseData?.courseRun?.isArchived,
|
||||
minPassingGrade: Math.floor((courseData?.courseRun?.minPassingGrade ?? 0) * 100),
|
||||
progressUrl: baseAppUrl(courseData?.courseRun?.progressUrl || ''),
|
||||
}), [courseData]);
|
||||
const { supportEmail, billingEmail } = useMemo(
|
||||
() => ({
|
||||
supportEmail: learnerHomeData?.platformSettings?.supportEmail,
|
||||
billingEmail: learnerHomeData?.platformSettings?.billingEmail,
|
||||
}),
|
||||
[learnerHomeData],
|
||||
);
|
||||
const { formatMessage } = useIntl();
|
||||
const formatDate = useFormatDate();
|
||||
|
||||
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>
|
||||
);
|
||||
@@ -45,12 +79,12 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (hasFinished) {
|
||||
if (isArchived) {
|
||||
return (
|
||||
<Banner variant="warning">
|
||||
{formatMessage(messages.notEligibleForCert)}.
|
||||
{formatMessage(messages.notEligibleForCert)}
|
||||
{' '}
|
||||
<Hyperlink destination={progressUrl}>{formatMessage(messages.viewGrades)}</Hyperlink>
|
||||
<Hyperlink isInline destination={progressUrl}>{formatMessage(messages.viewGrades)}</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
@@ -60,18 +94,7 @@ export const CertificateBanner = ({ cardId }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isDownloadable) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
{formatMessage(messages.certReady)}
|
||||
{' '}
|
||||
<Hyperlink destination={certificate.certPreviewUrl}>
|
||||
{formatMessage(messages.viewCertificate)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificate.isEarnedButUnavailable) {
|
||||
if (certificate.isEarned && new Date(certificate.availableDate) > new Date()) {
|
||||
return (
|
||||
<Banner>
|
||||
{formatMessage(
|
||||
|
||||
@@ -1,68 +1,115 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { hooks } from 'data/redux';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import CertificateBanner from './CertificateBanner';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCertificateData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
useCardGradeData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
jest.mock('hooks', () => ({
|
||||
utilHooks: {
|
||||
useFormatDate: jest.fn(() => date => date),
|
||||
},
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('Components/Banner', () => 'Banner');
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('CertificateBanner', () => {
|
||||
const props = {
|
||||
cardId: 'cardId',
|
||||
};
|
||||
hooks.usePlatformSettingsData.mockReturnValue({
|
||||
supportEmail: 'suport@email',
|
||||
billingEmail: 'billing@email',
|
||||
});
|
||||
hooks.useCardCourseRunData.mockReturnValue({
|
||||
minPassingGrade: 0.8,
|
||||
progressUrl: 'progressUrl',
|
||||
});
|
||||
|
||||
const defaultCertificate = {
|
||||
const defaultCertificate = {
|
||||
availableDate: '10/20/3030',
|
||||
isRestricted: false,
|
||||
isDownloadable: false,
|
||||
isEarnedButUnavailable: false,
|
||||
};
|
||||
const defaultEnrollment = {
|
||||
};
|
||||
const defaultEnrollment = {
|
||||
isAudit: false,
|
||||
isVerified: false,
|
||||
hasFinished: false,
|
||||
};
|
||||
const defaultGrade = {
|
||||
isPassing: 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', () => {
|
||||
useCourseData.mockReturnValue({
|
||||
enrollment: {},
|
||||
certificate: {},
|
||||
gradeData: {},
|
||||
courseRun: {
|
||||
minPassingGrade: 0.8,
|
||||
progressUrl: 'progressUrl',
|
||||
},
|
||||
});
|
||||
const createWrapper = ({
|
||||
certificate = {},
|
||||
enrollment = {},
|
||||
grade = {},
|
||||
courseRun = {},
|
||||
platformSettings = {},
|
||||
}) => {
|
||||
hooks.useCardGradeData.mockReturnValueOnce({ ...defaultGrade, ...grade });
|
||||
hooks.useCardCertificateData.mockReturnValueOnce({ ...defaultCertificate, ...certificate });
|
||||
hooks.useCardEnrollmentData.mockReturnValueOnce({ ...defaultEnrollment, ...enrollment });
|
||||
return shallow(<CertificateBanner {...props} />);
|
||||
useCourseData.mockReturnValue({
|
||||
enrollment: { ...defaultEnrollment, ...enrollment },
|
||||
certificate: { ...defaultCertificate, ...certificate },
|
||||
gradeData: { ...defaultGrade, ...grade },
|
||||
courseRun: {
|
||||
...defaultCourseRun,
|
||||
...courseRun,
|
||||
},
|
||||
});
|
||||
const lernearData = { data: { platformSettings: { ...defaultPlatformSettings, ...platformSettings } } };
|
||||
useInitializeLearnerHome.mockReturnValue(lernearData);
|
||||
return render(<IntlProvider locale="en"><CertificateBanner {...props} /></IntlProvider>);
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('is restricted', () => {
|
||||
const wrapper = createWrapper({
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('is restricted', () => {
|
||||
createWrapper({
|
||||
certificate: {
|
||||
isRestricted: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
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);
|
||||
});
|
||||
test('is restricted and verified', () => {
|
||||
const wrapper = createWrapper({
|
||||
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,
|
||||
},
|
||||
@@ -70,82 +117,161 @@ describe('CertificateBanner', () => {
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
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();
|
||||
});
|
||||
test('not passing and audit', () => {
|
||||
const wrapper = createWrapper({
|
||||
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,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
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();
|
||||
});
|
||||
test('not passing and has finished', () => {
|
||||
const wrapper = createWrapper({
|
||||
enrollment: {
|
||||
hasFinished: true,
|
||||
},
|
||||
it('not passing and has finished', () => {
|
||||
createWrapper({
|
||||
courseRun: { isArchived: true },
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const banner = screen.getByRole('alert');
|
||||
expect(banner).toHaveClass('alert-warning');
|
||||
const archivedMsg = screen.getByText('You are not eligible for a certificate.');
|
||||
expect(archivedMsg).toBeInTheDocument();
|
||||
});
|
||||
test('not passing and not audit and not finished', () => {
|
||||
const wrapper = createWrapper({});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
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();
|
||||
});
|
||||
test('is passing and is downloadable', () => {
|
||||
const wrapper = createWrapper({
|
||||
it('is passing and is earned but unavailable', () => {
|
||||
createWrapper({
|
||||
grade: {
|
||||
isPassing: true,
|
||||
},
|
||||
certificate: {
|
||||
isDownloadable: true,
|
||||
isEarned: true,
|
||||
availableDate: '10/20/3030',
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
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();
|
||||
});
|
||||
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({
|
||||
it('is passing and not downloadable render empty', () => {
|
||||
createWrapper({
|
||||
grade: {
|
||||
isPassing: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
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 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.queryByRole('alert');
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
it('should use default values when courseData is empty or undefined', () => {
|
||||
useCourseData.mockReturnValue({});
|
||||
const lernearData = { data: { platformSettings: { supportEmail } } };
|
||||
useInitializeLearnerHome.mockReturnValue(lernearData);
|
||||
render(<IntlProvider locale="en"><CertificateBanner cardId="test-card" /></IntlProvider>);
|
||||
|
||||
const mockedUseMemo = jest.spyOn(React, 'useMemo');
|
||||
const useMemoCall = mockedUseMemo.mock.calls.find(call => call[1].some(dep => dep === undefined || dep === null));
|
||||
|
||||
if (useMemoCall) {
|
||||
const result = useMemoCall[0]();
|
||||
|
||||
expect(result.certificate).toEqual({});
|
||||
expect(result.isVerified).toBe(false);
|
||||
expect(result.isAudit).toBe(false);
|
||||
expect(result.isPassing).toBe(false);
|
||||
expect(result.isArchived).toBe(false);
|
||||
expect(result.minPassingGrade).toBe(0);
|
||||
expect(result.progressUrl).toBeDefined();
|
||||
}
|
||||
|
||||
mockedUseMemo.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import React, { useMemo } 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 { hooks as appHooks } from 'data/redux';
|
||||
import { utilHooks, useCourseData } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
import messages from './messages';
|
||||
|
||||
export const CourseBanner = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const courseData = useCourseData(cardId);
|
||||
const {
|
||||
isVerified,
|
||||
isAuditAccessExpired,
|
||||
canUpgrade,
|
||||
isVerified = false,
|
||||
isAuditAccessExpired = false,
|
||||
coursewareAccess = {},
|
||||
} = appHooks.useCardEnrollmentData(cardId);
|
||||
const courseRun = appHooks.useCardCourseRunData(cardId);
|
||||
const course = appHooks.useCardCourseData(cardId);
|
||||
const { formatMessage, formatDate } = useIntl();
|
||||
} = useMemo(() => ({
|
||||
isVerified: courseData.enrollment?.isVerified,
|
||||
isAuditAccessExpired: courseData.enrollment?.isAuditAccessExpired,
|
||||
coursewareAccess: courseData.enrollment?.coursewareAccess || {},
|
||||
}), [courseData]);
|
||||
const courseRun = courseData?.courseRun || {};
|
||||
const formatDate = utilHooks.useFormatDate();
|
||||
|
||||
const { hasUnmetPrerequisites, isStaff, isTooEarly } = coursewareAccess;
|
||||
|
||||
@@ -26,31 +30,13 @@ export const CourseBanner = ({ cardId }) => {
|
||||
return (
|
||||
<>
|
||||
{isAuditAccessExpired
|
||||
&& (canUpgrade ? (
|
||||
&& (
|
||||
<Banner>
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
{formatMessage(messages.upgradeToAccess)}
|
||||
</Banner>
|
||||
) : (
|
||||
<Banner>
|
||||
{formatMessage(messages.auditAccessExpired)}
|
||||
{' '}
|
||||
{
|
||||
<Hyperlink destination="">
|
||||
<Hyperlink isInline destination="">
|
||||
{formatMessage(messages.findAnotherCourse)}
|
||||
</Hyperlink>
|
||||
}
|
||||
</Banner>
|
||||
))}
|
||||
|
||||
{courseRun.isActive && !canUpgrade && (
|
||||
<Banner>
|
||||
{formatMessage(messages.upgradeDeadlinePassed)}
|
||||
{' '}
|
||||
<Hyperlink destination={course.website || ''}>
|
||||
{formatMessage(messages.exploreCourseDetails)}
|
||||
</Hyperlink>
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
@@ -61,6 +47,7 @@ export const CourseBanner = ({ cardId }) => {
|
||||
})}
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
{(!isStaff && hasUnmetPrerequisites) && (
|
||||
<Banner>{formatMessage(messages.prerequisitesNotMet)}</Banner>
|
||||
)}
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
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 { hooks as appHooks } from 'data/redux';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { CourseBanner } from './CourseBanner';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useCardCourseData: jest.fn(),
|
||||
useCardCourseRunData: jest.fn(),
|
||||
useCardEnrollmentData: jest.fn(),
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
utilHooks: {
|
||||
useFormatDate: () => date => date,
|
||||
},
|
||||
}));
|
||||
|
||||
const cardId = 'my-test-course-number';
|
||||
|
||||
let el;
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
const enrollmentData = {
|
||||
isVerified: false,
|
||||
canUpgrade: false,
|
||||
isAuditAccessExpired: false,
|
||||
coursewareAccess: {
|
||||
hasUnmetPrerequisites: false,
|
||||
@@ -34,138 +28,83 @@ const enrollmentData = {
|
||||
const courseRunData = {
|
||||
isActive: false,
|
||||
startDate: '11/11/3030',
|
||||
};
|
||||
const courseData = {
|
||||
website: 'test-course-website',
|
||||
marketingUrl: 'marketing-url',
|
||||
};
|
||||
|
||||
const render = (overrides = {}) => {
|
||||
const renderCourseBanner = (overrides = {}) => {
|
||||
const {
|
||||
course = {},
|
||||
courseRun = {},
|
||||
enrollment = {},
|
||||
} = overrides;
|
||||
appHooks.useCardCourseData.mockReturnValueOnce({
|
||||
...courseData,
|
||||
...course,
|
||||
});
|
||||
appHooks.useCardCourseRunData.mockReturnValueOnce({
|
||||
useCourseData.mockReturnValue({
|
||||
courseRun: {
|
||||
...courseRunData,
|
||||
...courseRun,
|
||||
});
|
||||
appHooks.useCardEnrollmentData.mockReturnValueOnce({
|
||||
},
|
||||
enrollment: {
|
||||
...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();
|
||||
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
|
||||
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
|
||||
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
|
||||
it('initializes data with course number from enrollment, course and course run data', () => {
|
||||
renderCourseBanner();
|
||||
expect(useCourseData).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 } });
|
||||
it('should use default values when enrollment data is undefined', () => {
|
||||
renderCourseBanner({
|
||||
enrollment: undefined,
|
||||
courseRun: {},
|
||||
});
|
||||
test('snapshot: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
|
||||
expect(useCourseData).toHaveBeenCalledWith('test-card-id');
|
||||
});
|
||||
test('messages: (auditAccessExpired, upgradeToAccess)', () => {
|
||||
expect(el.text()).toContain(messages.auditAccessExpired.defaultMessage);
|
||||
expect(el.text()).toContain(messages.upgradeToAccess.defaultMessage);
|
||||
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();
|
||||
});
|
||||
});
|
||||
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(courseData.website);
|
||||
});
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
import { useCourseData } from 'hooks';
|
||||
|
||||
import ApprovedContent from './views/ApprovedContent';
|
||||
import EligibleContent from './views/EligibleContent';
|
||||
import MustRequestContent from './views/MustRequestContent';
|
||||
import PendingContent from './views/PendingContent';
|
||||
import RejectedContent from './views/RejectedContent';
|
||||
|
||||
export const statusComponents = StrictDict({
|
||||
pending: PendingContent,
|
||||
approved: ApprovedContent,
|
||||
rejected: RejectedContent,
|
||||
});
|
||||
|
||||
export const useCreditBannerData = (cardId) => {
|
||||
const courseData = useCourseData(cardId);
|
||||
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||
const supportEmail = useMemo(
|
||||
() => (learnerHomeData?.platformSettings?.supportEmail),
|
||||
[learnerHomeData],
|
||||
);
|
||||
|
||||
const credit = useMemo(() => {
|
||||
const creditData = courseData?.credit;
|
||||
if (!creditData || Object.keys(creditData).length === 0) {
|
||||
return { isEligible: false };
|
||||
}
|
||||
return {
|
||||
isEligible: true,
|
||||
providerStatusUrl: creditData.providerStatusUrl,
|
||||
providerName: creditData.providerName,
|
||||
providerId: creditData.providerId,
|
||||
error: creditData.error,
|
||||
purchased: creditData.purchased,
|
||||
requestStatus: creditData.requestStatus,
|
||||
};
|
||||
}, [courseData]);
|
||||
if (!credit.isEligible || !courseData?.credit?.isEligible) { return null; }
|
||||
|
||||
const { error, purchased, requestStatus } = credit;
|
||||
let ContentComponent = EligibleContent;
|
||||
if (purchased) {
|
||||
if (requestStatus == null) {
|
||||
ContentComponent = MustRequestContent;
|
||||
} else if (Object.keys(statusComponents).includes(requestStatus)) {
|
||||
ContentComponent = statusComponents[requestStatus];
|
||||
}
|
||||
// Current behavior is to show Elligible State if unknown request status is returned
|
||||
}
|
||||
return {
|
||||
ContentComponent,
|
||||
error,
|
||||
supportEmail,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
useCreditBannerData,
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import { keyStore } from 'utils';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
|
||||
import ApprovedContent from './views/ApprovedContent';
|
||||
import EligibleContent from './views/EligibleContent';
|
||||
import MustRequestContent from './views/MustRequestContent';
|
||||
import PendingContent from './views/PendingContent';
|
||||
import RejectedContent from './views/RejectedContent';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useMemo: (fn) => fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./views/ApprovedContent', () => 'ApprovedContent');
|
||||
jest.mock('./views/EligibleContent', () => 'EligibleContent');
|
||||
jest.mock('./views/MustRequestContent', () => 'MustRequestContent');
|
||||
jest.mock('./views/PendingContent', () => 'PendingContent');
|
||||
jest.mock('./views/RejectedContent', () => 'RejectedContent');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const statuses = keyStore(hooks.statusComponents);
|
||||
const supportEmail = 'test-support-email';
|
||||
let out;
|
||||
|
||||
const defaultProps = {
|
||||
isEligible: true,
|
||||
error: false,
|
||||
isPurchased: false,
|
||||
requestStatus: null,
|
||||
};
|
||||
|
||||
const loadHook = (creditData = {}) => {
|
||||
useCourseData.mockReturnValue({ credit: { ...defaultProps, ...creditData } });
|
||||
out = hooks.useCreditBannerData(cardId);
|
||||
};
|
||||
|
||||
describe('useCreditBannerData hook', () => {
|
||||
beforeEach(() => {
|
||||
useInitializeLearnerHome.mockReturnValue({ data: { platformSettings: { supportEmail } } });
|
||||
});
|
||||
it('loads card credit data with cardID and loads platform settings data', () => {
|
||||
loadHook({ isEligible: false });
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
expect(useInitializeLearnerHome).toHaveBeenCalledWith();
|
||||
});
|
||||
describe('non-credit-eligible learner', () => {
|
||||
it('returns null if the learner is not credit eligible', () => {
|
||||
loadHook({ isEligible: false });
|
||||
expect(out).toEqual(null);
|
||||
});
|
||||
});
|
||||
describe('credit-eligible learner', () => {
|
||||
it('returns error object from credit', () => {
|
||||
loadHook();
|
||||
expect(out.error).toEqual(defaultProps.error);
|
||||
loadHook({ error: true });
|
||||
expect(out.error).toEqual(true);
|
||||
});
|
||||
describe('ContentComponent', () => {
|
||||
it('returns EligibleContent if not purchased', () => {
|
||||
loadHook();
|
||||
expect(out.ContentComponent).toEqual(EligibleContent);
|
||||
});
|
||||
it('returns MustRequestContent if purchased but not requested', () => {
|
||||
loadHook({ purchased: true });
|
||||
expect(out.ContentComponent).toEqual(MustRequestContent);
|
||||
});
|
||||
it('returns PendingContent if purchased and request is pending', () => {
|
||||
loadHook({ purchased: true, requestStatus: statuses.pending });
|
||||
expect(out.ContentComponent).toEqual(PendingContent);
|
||||
});
|
||||
it('returns ApprovedContent if purchased and request is approved', () => {
|
||||
loadHook({ purchased: true, requestStatus: statuses.approved });
|
||||
expect(out.ContentComponent).toEqual(ApprovedContent);
|
||||
});
|
||||
it('returns RejectedContent if purchased and request is rejected', () => {
|
||||
loadHook({ purchased: true, requestStatus: statuses.rejected });
|
||||
expect(out.ContentComponent).toEqual(RejectedContent);
|
||||
});
|
||||
it('returns EligibleContent if purchased and request status is invalid', () => {
|
||||
loadHook({ purchased: true, requestStatus: 'fake-status' });
|
||||
expect(out.ContentComponent).toEqual(EligibleContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
import { MailtoLink } from '@openedx/paragon';
|
||||
import hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const CreditBanner = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const hookData = hooks.useCreditBannerData(cardId);
|
||||
if (hookData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ContentComponent, error, supportEmail } = hookData;
|
||||
const supportEmailLink = (<MailtoLink to={supportEmail}>{supportEmail}</MailtoLink>);
|
||||
return (
|
||||
<Banner {...(error && { variant: 'danger' })}>
|
||||
{error && (
|
||||
<p className="credit-error-msg" data-testid="credit-error-msg">
|
||||
{supportEmail ? formatMessage(messages.error, { supportEmailLink }) : formatMessage(messages.errorNoEmail)}
|
||||
</p>
|
||||
)}
|
||||
<ContentComponent cardId={cardId} />
|
||||
</Banner>
|
||||
);
|
||||
};
|
||||
CreditBanner.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CreditBanner;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import hooks from './hooks';
|
||||
import { CreditBanner } from '.';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useCreditBannerData: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('CreditBanner', () => {
|
||||
const mockCardId = 'test-card-id';
|
||||
const mockContentComponent = () => <div data-testid="mock-content">Test Content</div>;
|
||||
const mockSupportEmail = 'support@test.com';
|
||||
|
||||
const renderCreditBanner = () => render(
|
||||
<IntlProvider locale="en">
|
||||
<CreditBanner cardId={mockCardId} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const ApprovedContent = ({ cardId }) => {
|
||||
const courseData = useCourseData(cardId);
|
||||
const { providerStatusUrl: href, providerName } = useMemo(() => {
|
||||
const creditData = courseData?.credit;
|
||||
return {
|
||||
providerStatusUrl: creditData.providerStatusUrl,
|
||||
providerName: creditData.providerName,
|
||||
};
|
||||
}, [courseData]);
|
||||
const isMasquerading = useIsMasquerading();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{ href, message: formatMessage(messages.viewCredit), disabled: isMasquerading }}
|
||||
message={formatMessage(
|
||||
messages.approved,
|
||||
{
|
||||
congratulations: <b>{formatMessage(messages.congratulations)}</b>,
|
||||
linkToProviderSite: <ProviderLink cardId={cardId} />,
|
||||
providerName,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
ApprovedContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ApprovedContent;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import messages from './messages';
|
||||
import ApprovedContent from './ApprovedContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
useIsMasquerading: jest.fn(),
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
providerStatusUrl: 'test-credit-provider-status-url',
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
useCourseData.mockReturnValue({ credit });
|
||||
useIsMasquerading.mockReturnValue(false);
|
||||
|
||||
describe('ApprovedContent component', () => {
|
||||
describe('hooks', () => {
|
||||
it('initializes credit data with cardId', () => {
|
||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('rendered CreditContent component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||
});
|
||||
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));
|
||||
});
|
||||
it('action.href from credit.providerStatusUrl', () => {
|
||||
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
|
||||
expect(actionButton).toHaveAttribute('href', credit.providerStatusUrl);
|
||||
});
|
||||
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(() => {
|
||||
useIsMasquerading.mockReturnValue(true);
|
||||
render(<IntlProvider locale="en"><ApprovedContent cardId={cardId} /></IntlProvider>);
|
||||
});
|
||||
|
||||
it('disables the action button', () => {
|
||||
const actionButton = screen.getByRole('link', { name: messages.viewCredit.defaultMessage });
|
||||
expect(actionButton).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(actionButton).toHaveClass('disabled');
|
||||
});
|
||||
|
||||
it('still renders provider name and link correctly', () => {
|
||||
const creditMsg = screen.getByTestId('credit-msg');
|
||||
expect(creditMsg.textContent).toContain(credit.providerName);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useCourseData } from 'hooks';
|
||||
import track from 'tracking';
|
||||
|
||||
import CreditContent from './components/CreditContent';
|
||||
import messages from './messages';
|
||||
|
||||
export const EligibleContent = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const courseData = useCourseData(cardId);
|
||||
const providerName = courseData?.credit?.providerName;
|
||||
const courseId = courseData?.courseRun?.courseId;
|
||||
|
||||
const onClick = track.credit.purchase(courseId);
|
||||
const getCredit = formatMessage(messages.getCredit);
|
||||
const message = providerName
|
||||
? formatMessage(messages.eligibleFromProvider, { providerName })
|
||||
: formatMessage(messages.eligible, { getCredit: (<b>{getCredit}</b>) });
|
||||
|
||||
return (
|
||||
<CreditContent
|
||||
action={{ onClick, message: getCredit }}
|
||||
message={message}
|
||||
/>
|
||||
);
|
||||
};
|
||||
EligibleContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default EligibleContent;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useCourseData } from 'hooks';
|
||||
import track from 'tracking';
|
||||
|
||||
import messages from './messages';
|
||||
import EligibleContent from './EligibleContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('tracking', () => ({
|
||||
credit: {
|
||||
purchase: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const courseId = 'test-course-id';
|
||||
const credit = {
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
useCourseData.mockReturnValue({ credit, courseRun: { courseId } });
|
||||
|
||||
const renderEligibleContent = () => render(<IntlProvider locale="en" messages={{}}><EligibleContent cardId={cardId} /></IntlProvider>);
|
||||
|
||||
describe('EligibleContent component', () => {
|
||||
describe('hooks', () => {
|
||||
it('initializes credit data with cardId', () => {
|
||||
renderEligibleContent();
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('rendered CreditContent component', () => {
|
||||
it('action message is formatted getCredit message', () => {
|
||||
renderEligibleContent();
|
||||
const button = screen.getByRole('button', { name: messages.getCredit.defaultMessage });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
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);
|
||||
});
|
||||
it('message is formatted eligible message if provider', () => {
|
||||
renderEligibleContent();
|
||||
const eligibleMessage = screen.getByTestId('credit-msg');
|
||||
expect(eligibleMessage).toBeInTheDocument();
|
||||
expect(eligibleMessage).toHaveTextContent(credit.providerName);
|
||||
});
|
||||
it('message is formatted eligible message if no provider', () => {
|
||||
useCourseData.mockReturnValue({ credit: {}, courseRun: { courseId } });
|
||||
renderEligibleContent();
|
||||
const eligibleMessage = screen.getByTestId('credit-msg');
|
||||
expect(eligibleMessage).toBeInTheDocument();
|
||||
expect(eligibleMessage).toHaveTextContent(messages.getCredit.defaultMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useIsMasquerading } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import hooks from './hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const MustRequestContent = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { requestData, createCreditRequest } = hooks.useCreditRequestData(cardId);
|
||||
const isMasquerading = useIsMasquerading();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
message: formatMessage(messages.requestCredit),
|
||||
onClick: createCreditRequest,
|
||||
disabled: isMasquerading,
|
||||
}}
|
||||
message={formatMessage(messages.mustRequest, {
|
||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||
requestCredit: (<b>{formatMessage(messages.requestCredit)}</b>),
|
||||
})}
|
||||
requestData={requestData}
|
||||
/>
|
||||
);
|
||||
};
|
||||
MustRequestContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default MustRequestContent;
|
||||
@@ -0,0 +1,103 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import messages from './messages';
|
||||
import hooks from './hooks';
|
||||
import MustRequestContent from './MustRequestContent';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useCreditRequestData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
useIsMasquerading: jest.fn(),
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
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(() => {
|
||||
jest.clearAllMocks();
|
||||
hooks.useCreditRequestData.mockReturnValue({
|
||||
requestData,
|
||||
createCreditRequest,
|
||||
});
|
||||
useIsMasquerading.mockReturnValue(false);
|
||||
useCourseData.mockReturnValue({
|
||||
credit: {
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('hooks', () => {
|
||||
it('initializes credit request data with cardId', () => {
|
||||
renderMustRequestContent();
|
||||
expect(hooks.useCreditRequestData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
describe('rendered content', () => {
|
||||
beforeEach(() => {
|
||||
renderMustRequestContent();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it('shows request credit button that is enabled', () => {
|
||||
const button = screen.getByRole('button', { name: /request credit/i });
|
||||
expect(button).toBeEnabled();
|
||||
});
|
||||
|
||||
it('displays must request message with provider link', () => {
|
||||
expect(screen.getByTestId('credit-msg')).toHaveTextContent(/request credit/i);
|
||||
});
|
||||
|
||||
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(() => {
|
||||
useIsMasquerading.mockReturnValue(true);
|
||||
renderMustRequestContent();
|
||||
});
|
||||
|
||||
it('disables the request credit button', () => {
|
||||
const button = screen.getByRole('button', { name: /request credit/i });
|
||||
expect(button).toHaveClass('disabled');
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import messages from './messages';
|
||||
|
||||
export const PendingContent = ({ cardId }) => {
|
||||
const courseData = useCourseData(cardId);
|
||||
const { providerStatusUrl: href, providerName } = courseData?.credit || {};
|
||||
const isMasquerading = useIsMasquerading();
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
action={{
|
||||
href,
|
||||
message: formatMessage(messages.viewDetails),
|
||||
disabled: isMasquerading,
|
||||
}}
|
||||
message={formatMessage(messages.received, { providerName })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
PendingContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PendingContent;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useCourseData, useIsMasquerading } from 'hooks';
|
||||
|
||||
import messages from './messages';
|
||||
import PendingContent from './PendingContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
useIsMasquerading: jest.fn(),
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const providerName = 'test-credit-provider-name';
|
||||
const providerStatusUrl = 'test-credit-provider-status-url';
|
||||
useIsMasquerading.mockReturnValue(false);
|
||||
useCourseData.mockReturnValue({
|
||||
credit: {
|
||||
providerName,
|
||||
providerStatusUrl,
|
||||
},
|
||||
});
|
||||
|
||||
const renderPendingContent = () => render(
|
||||
<IntlProvider messages={{}} locale="en">
|
||||
<PendingContent cardId={cardId} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
describe('PendingContent component', () => {
|
||||
describe('hooks', () => {
|
||||
it('initializes card credit data with cardId', () => {
|
||||
renderPendingContent();
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('rendered CreditContent component', () => {
|
||||
it('action message is formatted requestCredit message', () => {
|
||||
renderPendingContent();
|
||||
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
it('action href will go to provider status site', () => {
|
||||
renderPendingContent();
|
||||
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
|
||||
expect(button).toHaveAttribute('href', providerStatusUrl);
|
||||
});
|
||||
it('action.disabled is false', () => {
|
||||
renderPendingContent();
|
||||
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
|
||||
expect(button).not.toHaveClass('disabled');
|
||||
});
|
||||
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', () => {
|
||||
useIsMasquerading.mockReturnValue(true);
|
||||
renderPendingContent();
|
||||
const button = screen.getByRole('link', { name: messages.viewDetails.defaultMessage });
|
||||
expect(button).toHaveClass('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useCourseData } from 'hooks';
|
||||
import CreditContent from './components/CreditContent';
|
||||
import ProviderLink from './components/ProviderLink';
|
||||
import messages from './messages';
|
||||
|
||||
export const RejectedContent = ({ cardId }) => {
|
||||
const courseData = useCourseData(cardId);
|
||||
const credit = courseData?.credit;
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<CreditContent
|
||||
message={formatMessage(messages.rejected, {
|
||||
providerName: credit?.providerName,
|
||||
linkToProviderSite: (<ProviderLink cardId={cardId} />),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
RejectedContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RejectedContent;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useCourseData } from 'hooks';
|
||||
import RejectedContent from './RejectedContent';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
providerStatusUrl: 'test-credit-provider-status-url',
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
useCourseData.mockReturnValue({
|
||||
credit,
|
||||
});
|
||||
|
||||
const renderRejectedContent = () => render(<IntlProvider><RejectedContent cardId={cardId} /></IntlProvider>);
|
||||
|
||||
describe('RejectedContent component', () => {
|
||||
describe('hooks', () => {
|
||||
it('initializes credit data with cardId', () => {
|
||||
renderRejectedContent();
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('rendered CreditContent component', () => {
|
||||
it('no action is passed', () => {
|
||||
renderRejectedContent();
|
||||
const action = screen.queryByTestId('action-row-btn');
|
||||
expect(action).not.toBeInTheDocument();
|
||||
});
|
||||
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.`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ActionRow, Button } from '@openedx/paragon';
|
||||
import CreditRequestForm from './CreditRequestForm';
|
||||
|
||||
export const CreditContent = ({ action, message, requestData }) => (
|
||||
<>
|
||||
<div className="message-copy credit-msg" data-testid="credit-msg">
|
||||
{message}
|
||||
</div>
|
||||
{action && (
|
||||
<ActionRow className="mt-4">
|
||||
<Button
|
||||
as="a"
|
||||
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>
|
||||
</ActionRow>
|
||||
)}
|
||||
<CreditRequestForm requestData={requestData} />
|
||||
</>
|
||||
);
|
||||
CreditContent.defaultProps = {
|
||||
action: null,
|
||||
requestData: null,
|
||||
};
|
||||
CreditContent.propTypes = {
|
||||
action: PropTypes.shape({
|
||||
href: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
message: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
}),
|
||||
message: PropTypes.node.isRequired,
|
||||
requestData: PropTypes.shape({
|
||||
url: PropTypes.string,
|
||||
parameters: PropTypes.objectOf(PropTypes.string),
|
||||
}),
|
||||
};
|
||||
|
||||
export default CreditContent;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import CreditContent from './CreditContent';
|
||||
|
||||
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', () => {
|
||||
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', () => {
|
||||
renderCreditContent(props);
|
||||
const creditMsg = screen.getByTestId('credit-msg');
|
||||
expect(creditMsg).toBeInTheDocument();
|
||||
expect(creditMsg.innerHTML).toEqual(message);
|
||||
});
|
||||
it('loads CreditRequestForm with passed 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', () => {
|
||||
it('loads message into credit-msg div', () => {
|
||||
renderCreditContent({ message, requestData });
|
||||
const creditMsg = screen.getByTestId('credit-msg');
|
||||
expect(creditMsg).toBeInTheDocument();
|
||||
expect(creditMsg.innerHTML).toEqual(message);
|
||||
});
|
||||
it('loads CreditRequestForm with passed 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
export const useCreditRequestFormData = (requestData) => {
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (requestData !== null) {
|
||||
ref.current.click();
|
||||
}
|
||||
}, [requestData]);
|
||||
return { ref };
|
||||
};
|
||||
|
||||
export default useCreditRequestFormData;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
|
||||
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() },
|
||||
};
|
||||
React.useRef.mockReturnValue(ref);
|
||||
|
||||
describe('useCreditRequestFormData hook', () => {
|
||||
describe('behavior', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('initializes ref with null', () => {
|
||||
useCreditRequestFormData(requestData);
|
||||
expect(React.useRef).toHaveBeenCalledWith(null);
|
||||
});
|
||||
let cb;
|
||||
let prereqs;
|
||||
it('does not click current ref when request data changes and is null', () => {
|
||||
useCreditRequestFormData(null);
|
||||
([[cb, prereqs]] = React.useEffect.mock.calls);
|
||||
expect(prereqs).toEqual([null]);
|
||||
cb();
|
||||
expect(ref.current.click).not.toHaveBeenCalled();
|
||||
});
|
||||
it('clicks current ref when request data changes and is not null', () => {
|
||||
useCreditRequestFormData(requestData);
|
||||
([[cb, prereqs]] = React.useEffect.mock.calls);
|
||||
expect(prereqs).toEqual([requestData]);
|
||||
cb();
|
||||
expect(ref.current.click).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
it('returns ref for submit button', () => {
|
||||
out = useCreditRequestFormData(requestData);
|
||||
expect(out.ref).toEqual(ref);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Form, FormControl } from '@openedx/paragon';
|
||||
|
||||
import useCreditRequestFormData from './hooks';
|
||||
|
||||
export const CreditRequestForm = ({ requestData }) => {
|
||||
const { ref } = useCreditRequestFormData(requestData);
|
||||
if (requestData === null) {
|
||||
return null;
|
||||
}
|
||||
const { parameters, url } = requestData;
|
||||
return (
|
||||
<Form
|
||||
accept-method="UTF-8"
|
||||
action={url}
|
||||
className="hidden"
|
||||
method="POST"
|
||||
>
|
||||
{Object.keys(parameters).map((key) => (
|
||||
<FormControl
|
||||
as="textarea"
|
||||
key={key}
|
||||
name={key}
|
||||
value={parameters[key]}
|
||||
/>
|
||||
))}
|
||||
<Button type="submit" ref={ref} />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
CreditRequestForm.defaultProps = {
|
||||
requestData: null,
|
||||
};
|
||||
CreditRequestForm.propTypes = {
|
||||
requestData: PropTypes.shape({
|
||||
parameters: PropTypes.objectOf(PropTypes.string),
|
||||
url: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
export default CreditRequestForm;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { keyStore } from 'utils';
|
||||
|
||||
import useCreditRequestFormData from './hooks';
|
||||
import CreditRequestForm from '.';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const ref = { current: { click: jest.fn() }, useRef: jest.fn() };
|
||||
|
||||
const requestData = {
|
||||
url: 'test-request-data-url',
|
||||
parameters: {
|
||||
key1: 'val1',
|
||||
key2: 'val2',
|
||||
key3: 'val3',
|
||||
},
|
||||
};
|
||||
|
||||
const paramKeys = keyStore(requestData.parameters);
|
||||
|
||||
useCreditRequestFormData.mockReturnValue({ ref });
|
||||
|
||||
const renderForm = (data) => render(<CreditRequestForm requestData={data} />);
|
||||
describe('CreditRequestForm component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('hooks', () => {
|
||||
it('initializes ref from hook with requestData', () => {
|
||||
renderForm(requestData);
|
||||
expect(useCreditRequestFormData).toHaveBeenCalledWith(requestData);
|
||||
});
|
||||
});
|
||||
describe('render output', () => {
|
||||
describe('null requestData', () => {
|
||||
it('returns null', () => {
|
||||
const { container } = renderForm(null);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('valid requestData', () => {
|
||||
it('loads Form with 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 { 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import useCreditRequestFormData from './hooks';
|
||||
import CreditRequestForm from '.';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
const ref = { current: { click: jest.fn() }, useRef: jest.fn() };
|
||||
const requestData = {
|
||||
url: 'test-request-data-url',
|
||||
parameters: {
|
||||
key1: 'val1',
|
||||
key2: 'val2',
|
||||
key3: 'val3',
|
||||
},
|
||||
};
|
||||
|
||||
useCreditRequestFormData.mockReturnValue({ ref });
|
||||
|
||||
let el;
|
||||
describe('CreditRequestForm component ref behavior', () => {
|
||||
it('loads submit button with ref from hook', () => {
|
||||
el = render(<CreditRequestForm requestData={requestData} />);
|
||||
const button = el.getByRole('button');
|
||||
expect(ref.current).toEqual(button);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useCourseData } from 'hooks';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
|
||||
export const ProviderLink = ({ cardId }) => {
|
||||
const courseData = useCourseData(cardId);
|
||||
const credit = courseData?.credit || {};
|
||||
return (
|
||||
<Hyperlink
|
||||
href={credit.providerStatusUrl}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
{credit.providerName}
|
||||
</Hyperlink>
|
||||
);
|
||||
};
|
||||
ProviderLink.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ProviderLink;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useCourseData } from 'hooks';
|
||||
|
||||
import ProviderLink from './ProviderLink';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
const credit = {
|
||||
providerStatusUrl: 'test-credit-provider-status-url',
|
||||
providerName: 'test-credit-provider-name',
|
||||
};
|
||||
|
||||
const renderProviderLink = () => render(
|
||||
<IntlProvider locale="en"><ProviderLink cardId={cardId} /></IntlProvider>,
|
||||
);
|
||||
|
||||
describe('ProviderLink component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useCourseData.mockReturnValue({ credit });
|
||||
renderProviderLink();
|
||||
});
|
||||
describe('hooks', () => {
|
||||
it('initializes credit hook with cardId', () => {
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
it('passes credit.providerStatusUrl to the hyperlink href', () => {
|
||||
const providerLink = screen.getByRole('link', { href: credit.providerStatusUrl });
|
||||
expect(providerLink).toBeInTheDocument();
|
||||
});
|
||||
it('passes providerName for the link message', () => {
|
||||
const providerLink = screen.getByRole('link', { href: credit.providerStatusUrl });
|
||||
expect(providerLink).toHaveTextContent(credit.providerName);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { StrictDict } from 'utils';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { useCreateCreditRequest } from 'data/hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
creditRequestData: (val) => React.useState(val), // eslint-disable-line
|
||||
});
|
||||
|
||||
export const useCreditRequestData = (cardId) => {
|
||||
const [requestData, setRequestData] = module.state.creditRequestData(null);
|
||||
const courseData = useCourseData(cardId);
|
||||
const providerId = courseData?.credit?.providerId;
|
||||
const { authenticatedUser: { username } } = React.useContext(AppContext);
|
||||
const courseId = courseData?.courseRun?.courseId;
|
||||
const { mutate: createCreditMutation } = useCreateCreditRequest();
|
||||
|
||||
const createCreditRequest = (e) => {
|
||||
e.preventDefault();
|
||||
createCreditMutation({ providerId, courseId, username }, {
|
||||
onSuccess: (response) => {
|
||||
setRequestData(response.data);
|
||||
},
|
||||
});
|
||||
};
|
||||
return { requestData, createCreditRequest };
|
||||
};
|
||||
|
||||
export default {
|
||||
useCreditRequestData,
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import * as api from 'data/services/lms/api';
|
||||
import { useCourseData } from 'hooks';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('data/services/lms/api', () => ({
|
||||
createCreditRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
useCourseData: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/logging'),
|
||||
logError: jest.fn(),
|
||||
}));
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContext.Provider value={{
|
||||
authenticatedUser: { username: 'test-user' },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
describe('useCreditRequestData', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper();
|
||||
(useCourseData as jest.Mock).mockReturnValue({
|
||||
credit: { providerId: 'provider-123' },
|
||||
courseRun: { courseId: 'course-456' },
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('initializes requestData as null', () => {
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
expect(result.current.requestData).toBeNull();
|
||||
});
|
||||
|
||||
it('returns createCreditRequest function', () => {
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
expect(typeof result.current.createCreditRequest).toBe('function');
|
||||
});
|
||||
|
||||
it('prevents default event behavior', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(api.createCreditRequest as jest.Mock).mockResolvedValue({ data: 'success' });
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls API with correct parameters', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(api.createCreditRequest as jest.Mock).mockResolvedValue({ data: 'success' });
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||
providerId: 'provider-123',
|
||||
courseId: 'course-456',
|
||||
username: 'test-user',
|
||||
});
|
||||
});
|
||||
|
||||
it('sets requestData with response data on success', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
const responseData = { data: { id: 'credit-123', status: 'pending' } };
|
||||
(api.createCreditRequest as jest.Mock).mockResolvedValue(responseData);
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||
providerId: 'provider-123',
|
||||
courseId: 'course-456',
|
||||
username: 'test-user',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.requestData).toEqual(responseData.data);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing providerId gracefully', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(useCourseData as jest.Mock).mockReturnValue({
|
||||
credit: null,
|
||||
courseRun: { courseId: 'course-456' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||
providerId: undefined,
|
||||
courseId: 'course-456',
|
||||
username: 'test-user',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing courseId gracefully', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(useCourseData as jest.Mock).mockReturnValue({
|
||||
credit: { providerId: 'provider-123' },
|
||||
courseRun: null,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(api.createCreditRequest).toHaveBeenCalledWith({
|
||||
providerId: 'provider-123',
|
||||
courseId: undefined,
|
||||
username: 'test-user',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles API errors without crashing', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(api.createCreditRequest as jest.Mock).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
expect(result.current.requestData).toBeNull();
|
||||
});
|
||||
|
||||
it('uses cardId to fetch course data', () => {
|
||||
renderHook(() => hooks.useCreditRequestData('different-card'), { wrapper });
|
||||
|
||||
expect(useCourseData).toHaveBeenCalledWith('different-card');
|
||||
});
|
||||
|
||||
it('handles undefined response data', async () => {
|
||||
const event = { preventDefault: jest.fn() };
|
||||
(api.createCreditRequest as jest.Mock).mockResolvedValue({ status: 200 });
|
||||
|
||||
const { result } = renderHook(() => hooks.useCreditRequestData('card-123'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
result.current.createCreditRequest(event);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.requestData).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
approved: {
|
||||
id: 'learner-dash.courseCard.banners.credit.approved',
|
||||
description: '',
|
||||
defaultMessage: '{congratulations} {providerName} has approved your request for course credit. To see your course credit, visit the {linkToProviderSite} website.',
|
||||
},
|
||||
congratulations: {
|
||||
id: 'learner-dash.courseCard.banners.credit.congratulations',
|
||||
description: '',
|
||||
defaultMessage: 'Congratulations!',
|
||||
},
|
||||
eligible: {
|
||||
id: 'learner-dash.courseCard.banners.credit.eligible',
|
||||
description: '',
|
||||
defaultMessage: 'You have completed this course and are eligible to purchase course credit. Select {getCredit} to get started.',
|
||||
},
|
||||
eligibleFromProvider: {
|
||||
id: 'learner-dash.courseCard.banners.credit.eligibleFromProvider',
|
||||
description: '',
|
||||
defaultMessage: 'You are now eligible for credit from {providerName}. Congratulations!',
|
||||
},
|
||||
getCredit: {
|
||||
id: 'learner-dash.courseCard.banners.credit.getCredit',
|
||||
description: '',
|
||||
defaultMessage: 'Get Credit',
|
||||
},
|
||||
mustRequest: {
|
||||
id: 'learner-dash.courseCard.banners.credit.mustRequest',
|
||||
description: '',
|
||||
defaultMessage: 'Thank you for your payment. To receive course credit, you must request credit at the {linkToProviderSite} website. Select {requestCredit} to get started',
|
||||
},
|
||||
received: {
|
||||
id: 'learner-dash.courseCard.banners.credit.received',
|
||||
description: '',
|
||||
defaultMessage: '{providerName} has received your course credit request. We will update you when credit processing is complete.',
|
||||
},
|
||||
rejected: {
|
||||
id: 'learner-dash.courseCard.banners.credit.rejected',
|
||||
description: '',
|
||||
defaultMessage: '{providerName} did not approve your request for course credit. For more information, contact {linkToProviderSite} directly.',
|
||||
},
|
||||
requestCredit: {
|
||||
id: 'learner-dash.courseCard.banners.credit.requestCredit',
|
||||
description: '',
|
||||
defaultMessage: 'Request Credit',
|
||||
},
|
||||
viewCredit: {
|
||||
id: 'learner-dash.courseCard.banners.credit.viewCredit',
|
||||
description: '',
|
||||
defaultMessage: 'View Credit',
|
||||
},
|
||||
viewDetails: {
|
||||
id: 'learner-dash.courseCard.banners.credit.viewDetails',
|
||||
description: '',
|
||||
defaultMessage: 'View Details',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,18 +1,21 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, MailtoLink } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { dateFormatter } from 'utils';
|
||||
import { Button, MailtoLink } from '@openedx/paragon';
|
||||
|
||||
import { utilHooks, useCourseData, useEntitlementInfo } from 'hooks';
|
||||
import { useSelectSessionModal } from 'data/context';
|
||||
import Banner from 'components/Banner';
|
||||
import { useInitializeLearnerHome } from 'data/hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const EntitlementBanner = ({ cardId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { formatMessage } = useIntl();
|
||||
const { data: learnerHomeData } = useInitializeLearnerHome();
|
||||
const courseData = useCourseData(cardId);
|
||||
|
||||
const {
|
||||
isEntitlement,
|
||||
hasSessions,
|
||||
@@ -20,10 +23,13 @@ export const EntitlementBanner = ({ cardId }) => {
|
||||
changeDeadline,
|
||||
showExpirationWarning,
|
||||
isExpired,
|
||||
} = appHooks.useCardEntitlementData(cardId);
|
||||
const { supportEmail } = appHooks.usePlatformSettingsData();
|
||||
const openSessionModal = appHooks.useUpdateSelectSessionModalCallback(dispatch, cardId);
|
||||
const { formatDate, formatMessage } = useIntl();
|
||||
} = useEntitlementInfo(courseData);
|
||||
const supportEmail = useMemo(
|
||||
() => learnerHomeData?.platformSettings?.supportEmail,
|
||||
[learnerHomeData],
|
||||
);
|
||||
const { updateSelectSessionModal } = useSelectSessionModal();
|
||||
const formatDate = utilHooks.useFormatDate();
|
||||
|
||||
if (!isEntitlement) {
|
||||
return null;
|
||||
@@ -42,9 +48,9 @@ export const EntitlementBanner = ({ cardId }) => {
|
||||
return (
|
||||
<Banner>
|
||||
{formatMessage(messages.entitlementExpiringSoon, {
|
||||
changeDeadline: dateFormatter(formatDate, changeDeadline),
|
||||
changeDeadline: formatDate(changeDeadline),
|
||||
selectSessionButton: (
|
||||
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
|
||||
<Button variant="link" size="inline" className="m-0 p-0" onClick={() => updateSelectSessionModal(cardId)}>
|
||||
{formatMessage(messages.selectSession)}
|
||||
</Button>
|
||||
),
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { formatMessage } from 'testUtils';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { useCourseData } from 'hooks';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('components/Banner', () => 'Banner');
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
usePlatformSettingsData: jest.fn(),
|
||||
useCardEntitlementData: jest.fn(),
|
||||
useUpdateSelectSessionModalCallback: jest.fn(
|
||||
(_, cardId) => jest.fn().mockName(`updateSelectSessionModalCallback(${cardId})`),
|
||||
),
|
||||
},
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useMemo: (fn) => fn(),
|
||||
}));
|
||||
|
||||
const cardId = 'my-test-course-number';
|
||||
jest.mock('data/hooks', () => ({
|
||||
useInitializeLearnerHome: jest.fn().mockReturnValue({
|
||||
data: {
|
||||
platformSettings: {
|
||||
supportEmail: 'test-support-email',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
const mockUpdateSelectSessionModal = jest.fn().mockName('updateSelectSessionModal');
|
||||
jest.mock('data/context/SelectSessionProvider', () => ({
|
||||
useSelectSessionModal: () => ({
|
||||
updateSelectSessionModal: mockUpdateSelectSessionModal,
|
||||
}),
|
||||
}));
|
||||
|
||||
let el;
|
||||
jest.mock('hooks', () => ({
|
||||
...jest.requireActual('hooks'),
|
||||
useCourseData: jest.fn(),
|
||||
utilHooks: {
|
||||
useFormatDate: () => date => date?.toDateString(),
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
const entitlementData = {
|
||||
isEntitlement: true,
|
||||
@@ -29,35 +48,78 @@ const entitlementData = {
|
||||
};
|
||||
const platformData = { supportEmail: 'test-support-email' };
|
||||
|
||||
const render = (overrides = {}) => {
|
||||
const renderComponent = (overrides = {}) => {
|
||||
const { entitlement = {} } = overrides;
|
||||
appHooks.useCardEntitlementData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
|
||||
appHooks.usePlatformSettingsData.mockReturnValueOnce(platformData);
|
||||
el = shallow(<EntitlementBanner cardId={cardId} />);
|
||||
useCourseData.mockReturnValue({
|
||||
entitlement: { ...entitlementData, ...entitlement },
|
||||
platformSettings: platformData,
|
||||
});
|
||||
return render(<IntlProvider locale="en"><EntitlementBanner cardId={cardId} /></IntlProvider>);
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
describe('EntitlementBanner', () => {
|
||||
test('initializes data with course number from entitlement', () => {
|
||||
render();
|
||||
expect(appHooks.useCardEntitlementData).toHaveBeenCalledWith(cardId);
|
||||
expect(appHooks.useUpdateSelectSessionModalCallback).toHaveBeenCalledWith(dispatch, cardId);
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('no display if not an entitlement', () => {
|
||||
render({ entitlement: { isEntitlement: false } });
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
it('initializes data with course number from entitlement', () => {
|
||||
renderComponent();
|
||||
expect(useCourseData).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
test('snapshot: no sessions available', () => {
|
||||
render({ entitlement: { isFulfilled: false, hasSessions: false } });
|
||||
expect(el).toMatchSnapshot();
|
||||
it('no display if not an entitlement', () => {
|
||||
renderComponent({ entitlement: { isEntitlement: false } });
|
||||
const banner = screen.queryByRole('alert');
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
test('snapshot: expiration warning', () => {
|
||||
render({ entitlement: { showExpirationWarning: true } });
|
||||
expect(el).toMatchSnapshot();
|
||||
it('renders when no sessions available', () => {
|
||||
renderComponent({ entitlement: { isFulfilled: false, hasSessions: false } });
|
||||
const banner = screen.getByRole('alert');
|
||||
expect(banner).toBeInTheDocument();
|
||||
expect(banner).toHaveClass('alert-warning');
|
||||
expect(banner.innerHTML).toContain(platformData.supportEmail);
|
||||
});
|
||||
test('no display if sessions available and not displaying warning', () => {
|
||||
render();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
it('renders when expiration warning', () => {
|
||||
const deadline = new Date();
|
||||
deadline.setDate(deadline.getDate() + 4);
|
||||
const deadlineStr = `${deadline.getMonth() + 1}/${deadline.getDate()}/${deadline.getFullYear()}`;
|
||||
renderComponent({ entitlement: { changeDeadline: deadlineStr, isFulfilled: false, availableSessions: [1, 2, 3] } });
|
||||
const banner = screen.getByRole('alert');
|
||||
expect(banner).toBeInTheDocument();
|
||||
expect(banner).toHaveClass('alert-info');
|
||||
const button = screen.getByRole('button', { name: formatMessage(messages.selectSession) });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
it('renders expired banner', () => {
|
||||
renderComponent({ entitlement: { isExpired: true, availableSessions: [1, 2, 3] } });
|
||||
const banner = screen.getByRole('alert');
|
||||
expect(banner).toBeInTheDocument();
|
||||
expect(banner.innerHTML).toContain(formatMessage(messages.entitlementExpired));
|
||||
});
|
||||
it('should call updateSelectSessionModal with cardId when select session button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const deadline = new Date();
|
||||
deadline.setDate(deadline.getDate() + 4);
|
||||
const deadlineStr = `${deadline.getMonth() + 1}/${deadline.getDate()}/${deadline.getFullYear()}`;
|
||||
renderComponent({ entitlement: { changeDeadline: deadlineStr, isFulfilled: false, availableSessions: [1, 2, 3] } });
|
||||
const banner = screen.getByRole('alert');
|
||||
expect(banner).toBeInTheDocument();
|
||||
expect(banner).toHaveClass('alert-info');
|
||||
const button = screen.getByRole('button', { name: formatMessage(messages.selectSession) });
|
||||
expect(button).toBeInTheDocument();
|
||||
await user.click(button);
|
||||
|
||||
expect(mockUpdateSelectSessionModal).toHaveBeenCalledWith(cardId);
|
||||
});
|
||||
it('should return null when isExpired is false and showExpirationWarning is false', () => {
|
||||
renderComponent({
|
||||
entitlement: {
|
||||
isEntitlement: true,
|
||||
hasSessions: true,
|
||||
isFulfilled: true,
|
||||
showExpirationWarning: false,
|
||||
isExpired: false,
|
||||
},
|
||||
});
|
||||
const banner = screen.queryByRole('alert');
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user